无需 dockerfile,使用 buildpacks 打包镜像
书接上文,聪明如你已经发现项目中没有定义 dockerfile
,但我们依然能打镜像,是如何做到的呢?正如上面提到的 gradle 的 spring 插件创建了 bootBuildImage
,通过 buildpacks 构建 OCI 镜像。
概念
buildpacks 让你可以把源文件转换为安全、高效、预生产的容器镜像。
buildpacks 是什么?
buildpacks 为应用程序提供框架和运行时支持。buildpacks 检查你的应用以决定需要哪些依赖,并恰当的配置这些应用以便能在各种云环境中运行。
工作方式
每个 buildpack 由两个阶段组成。
检测(detect)阶段
检测阶段会检查你的源码是否适合使用 buildpack。如果适合,就直接进入构建(build)阶段。如果不适合,则直接跳过 buildpack 中的构建阶段。
例如:
- buildpack 如何在 Python 项目中找到
requirements.txt
或setup.py
文件,则通过检测 - buildpack 如果在 Node 项目中找到
package-lock.json
文件,则通过检测
构建(build)阶段
构建阶段会检测你的代码以完成以下操作:
- 设置构建时和运行时环境
- 下载依赖然后编译你的源码(如果有必要的话)
- 设置恰当的程序入口和启动脚本
例如:
- Python 项目中的 buildpack 如果检测到
requirements.txt
文件,会执行pip install -r requirements.txt
命令,以安装requirements.txt
文件中的依赖。 - Node 项目中的 buildpack 如果检测到
package-lock.json
文件,会执行npm install
命令。
构建者是什么样子?
构建者是多个 buildpack 文件、基础构建(build)镜像、运行(run)镜像的有序组合。这些构建者参与到你的源码中然后构建,输出 app 镜像。构建镜像为构建者提供基础环境(例如,一个带有构建工具的 Ubuntu Bionic 操作系统镜像),运行镜像为 app 镜像在运行期间提供基础环境。构建镜像和运行镜像的组合叫做栈。
究其根本,构建者使用生命周期(lifecycle)为所有 buildpack 运行检测阶段,以便为所有通过检测阶段的 buildpack 运行构建阶段。
这让我们可以通过一个构建者就可以自动检测和构建各种各样的应用程序。
例如,假如 demo-builder 包含 Python 和 Node buildpack。那么
- 如果你的项目只包含
requirements.txt
文件,demo-builder 将只运行 Python 构建步骤。 - 如果你的项目只包含
package-lock.json
文件,demo-builder 将只允许 Node 构建步骤。 - 如果你的项目同时包含
package-lock.json
和requirements.txt
文件,demo-builder 将同时运行 Python 和 Node 构建步骤。 - 如果你的项目既不包含
requirements.txt
文件,也不包含package-lock.json
文件,那么 demo-builder 将检测失败,并退出构建。
组件
构建者
构建者是什么?
构建者是一个包含执行构建时需要的各种组件的镜像。构建者镜像由构建镜像、生命周期、多个 buildpack、各方面的构建配置文件(包含 buildpack 检测顺序和运行镜像的位置)组成。
解剖构建者
构建者由以下组件组成:
- 多个 buildpack
- 生命周期
- 栈构建镜像
buildpack
buildpack 是什么?
buildpack 是一个工作单元,该工作单元仔细检查你的源代码然后制定构建、运行应用程序的计划。
通常多个 buildpack 文件是一个至少包含 3 个文件的集合:
buildpack.toml
——提供 buildpack 的元数据bin/detect
——决定是否可以应用 buildpackbin/build
——执行 buildpack 逻辑
元 buildpack
还有一种不同类型的 buildpack,通常称为元 buildpack。它只有一个 buildpack.toml 文件,该文件包含有序的配置,而配置的内容是对其它 buildpack 的引用。当组合比较复杂的检测策略时,元 buildpack 特别有用。
解剖 buildpack
有两个必不可少的阶段可以让多个 buildpack 文件创建可运行的镜像。
检测
平台根据你的源码顺序测试 buildpack 组。第一个认为自己适合你的源码的 buildpack 组将成为你的 app 的 buildpack 文件集合。每个 buildpack 的检测标准不同——例如,NPM buildpack 查找package.json
文件,Go buildpack 查找 Go 源文件。
构建
在构建过程中,buildpack 文件对最终应用程序镜像有所贡献。贡献内容包括设置镜像的环境变量、创建包含二进制(例如:node、python、ruby)的层、添加应用程序依赖(例如:运行npm install
、pip install -r requirements.txt
、bundle install
)。
分布
buildpack 文件可以在镜像注册表或者 Docker 后台进程的基础上打包为 OCI 镜像。元 buildpack 文件也可以做到。
buildpack 组
buildpack 组是什么?
buildpack 组是一个特定的 buildpack 文件列表,该列表以适合构建应用程序的顺序组合在一起。由于 buildpack 文件是模块化并且可重用的,因此 builpack 组可以让你把多个模块化 buildpack 文件连在一起。
例如,你有一个 buildpack 文件用于安装 Java,一个 buildpack 文件使用 Maven 构建应用程序。这两个 builpack 文件可以合并到一个组中实现更高级的功能,特别是第一个文件安装 Java,第二个文件使用 Java 运行 Maven,这不就是 Java 的构建构建工具吗。
因为你在构建者或元 buildpack 中可以有多个 buildpack 组,并且你可以重用 buildpack 文件,所以你可以再用一个 buildpack 组,该组重用提供 Java 的 buildpack,但是使用 Gradle 提供的 buildpack 构建你的应用程序。如此一来,你在创建高级功能的时候就不需要复制了。
解剖 buildpack 组
buildpack 组是一个 buildpack 条目列表,按顺序定义了 buildpack 的顺序执行。
buildpack 条目通过 id 和版本进行定义。该条目可以被标记为是可选的。虽然在一个 buildpack 组中可能有一个或多个 buildpack,但在一个构建者或元 buildpack 中可以有多个 buildpack 组。
使用多个 buildpack 组进行检测
构建者或元 buildpack 可能包含多个 buildpack 组。当生命周期执行检测过程时,它会按指定的顺序执行每一组 buildpack。对于每个 buildpack 组来说,生命周期会执行组中的每个 buildpack 的检测阶段(可以并行执行)然后聚合结果。生命周期会选择第一个所有必须的 buildpack 检测通过的组。
例如,如果构建者有 A、B和C buildpack 组。生命周期将通过 A 运行检测。如果 A 中所有必需的 buildpack 都通过检测,那么生命周期就会选择 A。在那种情况下,B 和 C 不会进行处理。如果 A 在必须的 buildpack 中有任何失败,生命周期将转向处理 B。如果 B 在必须的 buildpack 中有任何失败,那么生命周期将转向处理 C。如果 C 有失败,那么整体检测处理将失败。
如果 buildpack 组只有元 buildpack,那么元 buildpack 可能反过来包含更多的 buildpack 组,这些组通过Order Resolution规则展开,所以元 buildpack 中的每个 buildpack 组将与其它 buildpack 组中 buildpack 一起工作。
例如:
- 构建者有 buildpack 组 A,包含 buildpack X,Y 和 Z
- Y 是元 buildpack 包含 buildpack 组 B 和 C
- buildpack 组 B 包含 buildpack T 和 U
- buildpack 组 C 包含 buildpack V 和 W
生命周期将此展开为以下 buildpack 组:
- X、T、U、Z
- X、V、W、Z
未包含 Y 的原因是元 buildpack 只提供组,元 buildpack 不参与构建过程或者构建、检测程序。
生命周期
生命周期协调 buildpack 执行,然后将生成的文件的打包进最终 app 镜像中。
检测
寻找一个有序的 buildpack 组,以便在构建阶段使用。
检测是生命周期的第一个阶段。由检测仪完成。在本阶段中,检测仪寻找有序的 buildpack 组,以便在构建阶段使用。在构建环境中调用检测仪不需要参数,并且不能使用 root 权限运行。输入文件是order.toml,两个输出文件分别是group.toml和plan.toml。
除非传入某些标志,否则检测仪使用以下默认配置:
- 定义顺序的路径:
/cnb/order.toml
- 定义输出组的路径:
<layers>/group.toml
- 输出构建计划的路径:
<layers>/plan.toml
完整的标志列表和配置看这里。
order.toml
order.toml 是一个包含组列表的文件。每个组是一个 buildpack 列表。检测仪读取 order.toml 然后寻找第一个通过检测的组。如果所有的组都失败了,那么检测就失败了。
组中的 buildpack 要么被标记会可选的,要么被标记为必须的。为了通过检测处理,必须满足两个条件:
- 检测脚本必须成功(退出代码是 0)通过所有必须的 buildpack。
- 检测仪可以创建构建计划(写入 plan.toml),包含所有组中必须的 buildpack。
第一个通过以上两个步骤的组将写入 group.toml 中,并将其构建计划写入 plan.toml 中。
注意:如果检测脚本执行可选 buildpack 失败了,buildpack 组仍然可以通过检测处理并且可以被选中。该组要被选中,至少要有一个 buildpack (无论是可选的还是必须的)成功的通过检测。
group.toml
选中的组如果可以创建 plan.toml,则会被写入 group.toml 中。buildpack 将会与 order.toml 中相同的顺序,并且过滤掉所有可选的失败的 buildpack,然后写入 group.toml 中。
plan.toml
每个 buildpack 可以定义两个列表,分别是提供依赖列表和要求依赖列表(或者通过 or 隔离的键值对列表)。这些列表(如果不为空)叫做构建计划。检测仪从选中的组中读取 buildpack(在过滤掉执行探测脚本失败的 buildpack 之后)。检测仪检查所有的可选项然后尝试创建一个包含条目列表的文件,每个条目都有提供和要求列表以满足所有 buildpack 的需求。每个可选项都称为一次试验,输出文件叫做 plan.toml。
提供和要求有两个限制条件:
- buildpack 提供的依赖,无论是 buildpack 本身还是 buildpack 所在的组都是必须的。
- buildpack 要求的依赖,必须通过 buildpack 本身提供或者 buildpack 所在的组的其它 buildpack 提供。
对于必要的 buildpack 来说,上面两个条件如果有一个失败,实验也会失败并且检测仪会寻找下一个实验。对于可选的 buildpack 来说,上面两个条件如果有一个失败,那边在最终计划中应该排除该 buildpack。如果所有的试验都失败了,代表着 buildpack 所在的组失败了(检测仪将转向下一个组)。
退出代码
退出码 | 结果 |
---|---|
0 | 成功 |
11 | 平台 API 不兼容错误 |
12 | buildpack API 不兼容错误 |
1-10,13-19 | 普通生命周期错误 |
20 | 所有 buildpack 组都未检测到 w/o 错误 |
21 | 所有 buildpack 组都未检测到 buildpack 发送错误 |
22-29 | 检测特定生命周期错误 |
分析
恢复 buildpack 用于优化构建和导出阶段的文件。
退出代码
退出码 | 结果 |
---|---|
0 | 成功 |
11 | 平台 API 不兼容错误 |
12 | buildpack API 不兼容错误 |
1-10,13-19 | 普通生命周期错误 |
20-39 | 分析特定生命周期错误 |
恢复
从缓存中恢复层。
退出代码
退出码 | 结果 |
---|---|
0 | 成功 |
11 | 平台 API 不兼容错误 |
12 | buildpack API 不兼容错误 |
1-10,13-19 | 普通生命周期错误 |
40-49 | 恢复特定生命周期错误 |
构建
将应用程序源代码转换为可运行的构件,这些构件可以打包到容器中。
退出代码
退出码 | 结果 |
---|---|
0 | 成功 |
11 | 平台 API 不兼容错误 |
12 | buildpack API 不兼容错误 |
1-10,13-19 | 普通生命周期错误 |
51 | Buildpack 构建错误 |
50,52-59 | 构建特定生命周期错误 |
导出
创建最终 OCI 镜像。
退出代码
退出码 | 结果 |
---|---|
0 | 成功 |
11 | 平台 API 不兼容错误 |
12 | buildpack API 不兼容错误 |
1-10,13-19 | 普通生命周期错误 |
60-69 | 导出特定生命周期错误 |
镜像
导出器通过参数的方式接收标志,该标志引用 OCI 镜像注册表或 Docker 守护进程,并将其写入 app 镜像中。
report.toml
导出器还将写一个 report.toml 文件,该文件包含导出镜像的相关信息,比如该镜像的概要以及清单的大小(如果导出的是 OCI 注册表)或识别器,以及 buildpack 提供的构建 BOM。输出的报告的位置可以通过 -report 标志进行指定;默认地址是<layers>/report.toml
——注意它不会在导出的镜像的文件系统中出现。
创建
在单个命令中运行检测、分析、恢复、构建以及导出。
退出代码
退出码 | 结果 |
---|---|
0 | 成功 |
11 | 平台 API 不兼容错误 |
12 | buildpack API 不兼容错误 |
1-10,13-19 | 普通生命周期错误 |
20-29 | 检测特定生命周期错误 |
30-39 | 分析特定生命周期错误 |
40-49 | 恢复特定生命周期错误 |
50-59 | 构建特定生命周期错误 |
60-69 | 导出特定生命周期错误 |
启动
最终 OCI 镜像的入口。负责启动应用程序进程。
退出代码
退出码 | 结果 |
---|---|
11 | 平台 API 不兼容错误 |
12 | buildpack API 不兼容错误 |
80-89 | 启动特定生命周期错误 |
变基(rebase)
把应用程序的层变基到新的运行镜像。
退出代码
退出码 | 结果 |
---|---|
0 | 成功 |
11 | 平台 API 不兼容错误 |
12 | buildpack API 不兼容错误 |
1-10,13-19 | 普通生命周期错误 |
70-79 | 变基特定生命周期错误 |
平台
平台是什么?
平台使用生命周期、buildpack(打包在构建者中)以及应用程序源码生成一个 OCI 镜像。
示例
平台可能包括的示例:
- 本地命令行工具使用 buildpack 创建 OCI 镜像。比如 Pack CLI 工具
- 持续集成服务的插件,该插件使用 buildpack 创建 OCI 镜像。比如 tekton 提供的 buildpack 插件
- 云应用程序平台在部署之前使用 buildpack 构建源码。比如 kpack 平台
API
平台技术规范详细的描述了平台的能力,以及平台如何和生命周期、构建者交互。当前平台的 API 版本是 0.4。
栈
栈是什么?
栈是把两个打算一起工作的镜像组合在一起:
- 栈中的构建镜像提供了基础镜像,该镜像构造了构建环境。构建环境是容器化环境,生命周期(从而打包)在该环境中执行。
- 栈中的运行镜像提供了基础镜像,应用程序通过该镜像进行构建。
如果你使用 pack 命令行界面,运行
pack stack suggest
会推荐一个栈(同时还有每个栈相关的构建、运行镜像)列表,这些栈可以用于执行pack builder create
命令。
使用栈
构建者使用栈,并通过构建者的配置文件进行配置:
[[buildpacks]]
# ...
[[order]]
# ...
[stack]
id = "com.example.stack"
build-image = "example/build"
run-image = "example/run"
run-image-mirrors = ["gcr.io/example/run", "registry.example.com/example/run"]
通过必要的 [stack]
小节,构建者作者可以配置栈的 ID、构建镜像、运行镜像(包括任何镜像)。
run-image-mirrors
run-image-mirrors
为运行镜像提供了可选位置,以便在构建
(或变基
)期间使用。当通过构建者容器运行构建
时,打包
会选择使用 app 镜像指定的位置(如果在镜像名称中没有指定注册表的主机地址,则将使用 DockerHub)。这在发布生成的 app 镜像时很有用(通过 --publish
标志或通过 docker push
),app 的基础镜像和 app 的镜像在同一个注册表中,这将减少推送 app 镜像所需的数据传输量。
在以下示例中,假设构建者配置文件和上面的一样,被选中运行的镜像是registry.example.com/example/run
。
$ pack build registry.example.com/example/app
当命名 app 不指定注册表时,比如 example/app
,将导致example/run
作为 app 的运行镜像。
$ pack build example/app
操作
构建
构建解释
构建是指通过你的源码执行一个或多个 buildpack,然后生成一个可运行的 OCI 镜像。每个 buildpack 检查都检查源码然后提供相关的依赖。然后根据 app 的源码和那些依赖生成镜像。
buildpack 兼容一个或多个栈。一个栈指定了一个构建镜像和一个运行镜像。在构建过程中,构建镜像是 buildpack 执行的环境,运行镜像是最终 app 镜像的基础镜像。
多个 buildpack 可以通过指定的栈的构建镜像绑定在一起,即构建者(注意,是构建者)镜像。构建者为指定栈的 buildpack 提供最便捷的分布。
变基
变基解释
当 app 的栈的运行镜像变更时,变基可以让 app 开发者或运营商快速地更新 app 镜像。通过对分层的镜像变基,我们避免了重新构建 app。
镜像变基的核心部分是一个简单的工程。通过检查 app 镜像,变基
可以判定 app 的基础镜像是否有新版本(无论是本地还是注册表中)。如果有,变基
通过更新 app 镜像的元数据层以引用新的基础镜像版本。
示例:app 镜像变基
考虑有一个 app 镜像 my-app:my-tag
使用默认的构造者进行初次构建。该构建者的栈使用的运行镜像为 pack/run
。运行以下命令更新 my-app:my-tag
的基础镜像为 pack/run
的最新版本。
$ pack rebase my-app:my-tag
提示:
pack rebase
有一个--publish
标志可以用于发布更新后的 app 镜像到注册表中。当使用注册表时,与 Docker 守护进程相比,--publish
是最优的。