无需 Dockerfile 的镜像构建(BuildPack vs Dockerfile)
过去的工作中,我们使用微服务、容器化以及服务编排构建了技术平台。为了提升开发团队的研发效率,我们同时还提供了 CICD 平台,用来将代码快速的部署到 Openshift(企业级的 Kubernetes) 集群。
部署的第一步就是应用程序的容器化,持续集成的交付物从以往的 jar 包、webpack 等变成了容器镜像。容器化将软件代码和所需的所有组件(库、框架、运行环境)打包到一起,进而可以在任何环境任何基础架构上一致地运行,并与其他应用“隔离”。
我们的代码需要从源码到编译到最终可运行的镜像,甚至部署,这一切在 CICD 的流水线中完成。最初,我们在每个代码仓库中都加入了三个文件,也通过项目生成器(类似 Spring Initializer)在新项目中注入:
- Jenkinsfile.groovy:用来定义 Jenkins 的 Pipeline,针对不同的语言还会有多种版本
- Manifest YAML:用于定义 Kubernetes 资源,也就是工作负载及其运行的相关描述
- Dockerfile:用于构建对象
随着平台的演进,我们需要考虑将这唯一的“钉子户” Dockerfile 与代码解耦,必要的时候也需要对 Dockerfile 进行升级。因此调研了一下 buildpacks,就有了今天的这篇文章。
什么是 Dockerfile Docker 通过读取 Dockerfile 中的说明自动构建镜像。Dockerfile 是一个文本文件,包含了由 Docker 可以执行用于构建镜像的指令。我们拿之前用于测试 Tekton 的 Java 项目的 Dockerfile 为例:
FROM openjdk:8-jdk-alpineRUN mkdir /app
WORKDIR /app
COPY target/*.jar /app/app.jar
ENTRYPOINT ["sh", "-c", "java -Xmx128m -Xms64m -jar app.jar"]
镜像分层
你可能会听过 Docker 镜像包含了多个层。每个层与 Dockerfile 中的每个命令对应,比如
RUN
、COPY
、ADD
。某些特定的指令会创建一个新的层,在镜像构建过程中,假如某些层没有发生变化,就会从缓存中获取。在下面的 Buildpack 中也同样通过镜像分层和 cache 来加速镜像的构建。
什么是 Buildpack BuildPack 是一个程序,它能将源代码转换成容器镜像的并可以在任意云环境中运行。通常 buildpack 封装了单一语言的生态工具链。适用于 Java、Ruby、Go、NodeJs、Python 等。
文章图片
Builder 是什么?
一些 buildpacks 按顺序组合之后就是 builder,除了 buildpacks, builder 中还加入了 生命周期 和 stack 容器镜像。
文章图片
stack 容器镜像由两个镜像组成:用于运行 buildpack 的镜像 build image,以及构建应用镜像的基础镜像 run image。如上图,就是 builder 中的运行环境。
Buildpack 的工作方式
文章图片
每个 buildpack 运行时都包含了两个阶段:
文章图片
1. 检测阶段 通过检查源代码中的某些特定文件/数据,来判断当前 buildpack 是否适用。如果适用,就会进入构建阶段;否则就会退出。比如:
- Java maven 的 buildpack 会检查源码中是否有
pom.xml
- Python 的 buildpack 会检查源码中是否有
requirements.txt
或者setup.py
文件 - Node buildpack 会查找
package-lock.json
文件。
- 设置构建环境和运行时环境
- 下载依赖并编译源码(假如需要的话)
- 设置正确的 entrypoint 和启动脚本。
- Java maven buildpack 在检查到有
pom.xml
文件之后,会执行mvn clean install -DskipTests
- Python buildpack 检查到有
requrements.txt
之后,会执行pip install -r requrements.txt
- Node build pack 检查到有
package-lock.json
后执行npm install
其实现在有很多开源的 buildpack 可以用,没有特定定制的情况下无需自己手动编写。比如下面的几个大厂开源并维护的 Buildpacks:
- Heroku Buildpacks
- Google Buildpacks
- Paketo
下面所有的内容都提交到了 Github 上,可以访问:https://github.com/addozhang/... 获取相关代码。
最终的目录
buildpacks-sample
结构如下:├── builders
│└── builder.toml
├── buildpacks
│└── buildpack-maven
│├── bin
││├── build
││└── detect
│└── buildpack.toml
└── stacks
├── build
│└── Dockerfile
├── build.sh
└── run
└── Dockerfile
创建 buildpack
pack buildpack new examples/maven \
--api 0.5 \
--path buildpack-maven \
--version 0.0.1 \
--stacks io.buildpacks.samples.stacks.bionic
看下生成的
buildpack-maven
目录:buildpack-maven
├── bin
│├── build
│└── detect
└── buildpack.toml
各个文件中都是默认的初试数据,并没有什么用处。需要添加些内容:
bin/detect
:#!/usr/bin/env bashif [[ ! -f pom.xml ]];
then
exit 100
fiplan_path=$2cat >> "${plan_path}" <
bin/build
:#!/usr/bin/env bashset -euo pipefaillayers_dir="$1"
env_dir="$2/env"
plan_path="$3"m2_layer_dir="${layers_dir}/maven_m2"
if [[ ! -d ${m2_layer_dir} ]];
then
mkdir -p ${m2_layer_dir}
echo "cache = true" > ${m2_layer_dir}.toml
fi
ln -s ${m2_layer_dir} $HOME/.m2echo "---> Running Maven"
mvn clean install -B -DskipTeststarget_dir="target"
for jar_file in $(find "$target_dir" -maxdepth 1 -name "*.jar" -type f);
do
cat >> "${layers_dir}/launch.toml" <
buildpack.toml
:api = "0.5"[buildpack]
id = "examples/maven"
version = "0.0.1"[[stacks]]
id = "com.atbug.buildpacks.example.stacks.maven"
创建 stack
构建 Maven 项目,首选需要 Java 和 Maven 的环境,我们使用
maven:3.5.4-jdk-8-slim
作为 build image 的 base 镜像。应用的运行时需要 Java 环境即可,因此使用 openjdk:8-jdk-slim
作为 run image 的 base 镜像。在
stacks
目录中分别创建 build
和 run
两个目录:【无需 Dockerfile 的镜像构建(BuildPack vs Dockerfile)】
build/Dockerfile
FROM maven:3.5.4-jdk-8-slimARG cnb_uid=1000
ARG cnb_gid=1000
ARG stack_idENV CNB_STACK_ID=${stack_id}
LABEL io.buildpacks.stack.id=${stack_id}ENV CNB_USER_ID=${cnb_uid}
ENV CNB_GROUP_ID=${cnb_gid}# Install packages that we want to make available at both build and run time
RUN apt-get update && \
apt-get install -y xz-utils ca-certificates && \
rm -rf /var/lib/apt/lists/*# Create user and group
RUN groupadd cnb --gid ${cnb_gid} && \
useradd --uid ${cnb_uid} --gid ${cnb_gid} -m -s /bin/bash cnbUSER ${CNB_USER_ID}:${CNB_GROUP_ID}
run/Dockerfile
FROM openjdk:8-jdk-slimARG stack_id
ARG cnb_uid=1000
ARG cnb_gid=1000
LABEL io.buildpacks.stack.id="${stack_id}"USER ${cnb_uid}:${cnb_gid}
然后使用如下命令构建出两个镜像:
export STACK_ID=com.atbug.buildpacks.example.stacks.mavendocker build --build-arg stack_id=${STACK_ID} -t addozhang/samples-buildpacks-stack-build:latest ./build
docker build --build-arg stack_id=${STACK_ID} -t addozhang/samples-buildpacks-stack-run:latest ./run
创建 Builder
有了 buildpack 和 stack 之后就是创建 Builder 了,首先创建
builder.toml
文件,并添加如下内容:[[buildpacks]]
id = "examples/maven"
version = "0.0.1"
uri = "../buildpacks/buildpack-maven"[[order]]
[[order.group]]
id = "examples/maven"
version = "0.0.1"[stack]
id = "com.atbug.buildpacks.example.stacks.maven"
run-image = "addozhang/samples-buildpacks-stack-run:latest"
build-image = "addozhang/samples-buildpacks-stack-build:latest"
然后执行命令,注意这里我们使用了
--pull-policy if-not-present
参数,就不需要将 stack 的两个镜像推送到镜像仓库了:pack builder create example-builder:latest --config ./builder.toml --pull-policy if-not-present
测试
有了 builder 之后,我们就可以使用创建好的 builder 来构建镜像了。
这里同样加上了
--pull-policy if-not-present
参数来使用本地的 builder 镜像:# 目录 buildpacks-sample与 tekton-test 同级,并在 buildpacks-sample中执行如下命令
pack build addozhang/tekton-test --builder example-builder:latest --pull-policy if-not-present --path ../tekton-test
如果看到类似如下内容,就说明镜像构建成功了(第一次构建镜像由于需要下载 maven 依赖耗时可能会比较久,后续就会很快,可以执行两次验证下):
...
===> EXPORTING
[exporter] Adding 1/1 app layer(s)
[exporter] Reusing layer 'launcher'
[exporter] Reusing layer 'config'
[exporter] Reusing layer 'process-types'
[exporter] Adding label 'io.buildpacks.lifecycle.metadata'
[exporter] Adding label 'io.buildpacks.build.metadata'
[exporter] Adding label 'io.buildpacks.project.metadata'
[exporter] Setting default process type 'web'
[exporter] Saving addozhang/tekton-test...
[exporter] *** Images (0d5ac1158bc0):
[exporter]addozhang/tekton-test
[exporter] Adding cache layer 'examples/maven:maven_m2'
Successfully built image addozhang/tekton-test
启动容器,会看到 spring boot 应用正常启动:
docker run --rm addozhang/tekton-test:latest
._______ _ _
/\\ / ___'_ __ _ _(_)_ ____ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/___)| |_)| | | | | || (_| |) ) ) )
'|____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot ::(v2.2.3.RELEASE) ...
总结 其实现在有很多开源的 buildpack 可以用,没有特定定制的情况下无需自己手动编写。比如下面的几个大厂开源并维护的 Buildpacks:
- Heroku Buildpacks
- Google Buildpacks
- Paketo
当然还是那句话,自己上手写一个会更容易理解 Buildpack 的工作方式。
文章统一发布在公众号云原生指北
文章图片
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量