过来的工作中,咱们应用微服务、容器化以及服务编排构建了技术平台。为了晋升开发团队的研发效率,咱们同时还提供了 CICD 平台,用来将代码疾速的部署到 Openshift(企业级的 Kubernetes)集群。
部署的第一步就是应用程序的容器化,继续集成的交付物从以往的 jar 包、webpack 等变成了容器镜像。容器化将软件代码和所需的所有组件(库、框架、运行环境)打包到一起,进而能够在任何环境任何基础架构上统一地运行,并与其余利用“隔离”。
咱们的代码须要从源码到编译到最终可运行的镜像,甚至部署,这所有在 CICD 的流水线中实现。最后,咱们在每个代码仓库中都退出了三个文件,也通过我的项目生成器(相似 Spring Initializer)在新我的项目中注入:
- Jenkinsfile.groovy:用来定义 Jenkins 的 Pipeline,针对不同的语言还会有多种版本
- Manifest YAML:用于定义 Kubernetes 资源,也就是工作负载及其运行的相干形容
- Dockerfile:用于构建对象
这个三个文件也须要在工作中一直的演进,起初我的项目较少(十几个)的时候咱们根底团队还能够去各个代码仓库去保护降级。随着我的项目爆发式的增长,保护的老本越来越高。咱们对 CICD 平台进行了迭代,将“Jenkinsfile.groovy”和“manifest YAML”从我的项目中移出,变更较少的 Dockerfile 就保留了下来。
随着平台的演进,咱们须要思考将这惟一的“钉子户”Dockerfile 与代码解耦,必要的时候也须要对 Dockerfile 进行降级。因而调研了一下 buildpacks,就有了明天的这篇文章。
什么是 Dockerfile
Docker 通过读取 Dockerfile 中的阐明主动构建镜像。Dockerfile 是一个文本文件,蕴含了由 Docker 能够执行用于构建镜像的指令。咱们拿之前用于测试 Tekton 的 Java 我的项目的 Dockerfile 为例:
FROM openjdk:8-jdk-alpine
RUN 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
文件。
2. 构建阶段
在构建阶段会进行如下操作:
- 设置构建环境和运行时环境
- 下载依赖并编译源码(如果需要的话)
- 设置正确的 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 上手
那到底如何在没有 Dockerfile 的状况下应用 builderpack 构建镜像的。看了下面这些,大家基本上也都能理解到这个外围就在 buildpack 的编写和应用的。
其实当初有很多开源的 buildpack 能够用,没有特定定制的状况下无需本人手动编写。比方上面的几个大厂开源并保护的 Buildpacks:
- Heroku Buildpacks
- Google Buildpacks
- Paketo
然而正式具体介绍开源的 buildpacks 之前,咱们还是通过本人创立 buildpack 的形式来深刻理解 Buildpacks 的工作形式。测试项目呢,咱们还是用测试 Tekton 的 Java 我的项目。
上面所有的内容都提交到了 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 bash
if [[! -f pom.xml]]; then
exit 100
fi
plan_path=$2
cat >> "${plan_path}" <<EOL
[[provides]]
name = "jdk"
[[requires]]
name = "jdk"
EOL
bin/build
:
#!/usr/bin/env bash
set -euo pipefail
layers_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/.m2
echo "---> Running Maven"
mvn clean install -B -DskipTests
target_dir="target"
for jar_file in $(find "$target_dir" -maxdepth 1 -name "*.jar" -type f); do
cat >> "${layers_dir}/launch.toml" <<EOL
[[processes]]
type = "web"
command = "java -jar ${jar_file}"
EOL
break;
done
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
两个目录:
build/Dockerfile
FROM maven:3.5.4-jdk-8-slim
ARG cnb_uid=1000
ARG cnb_gid=1000
ARG stack_id
ENV 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 cnb
USER ${CNB_USER_ID}:${CNB_GROUP_ID}
run/Dockerfile
FROM openjdk:8-jdk-slim
ARG 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.maven
docker 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
下面几个 buildpacks 库内容比拟全面,实现上会有些许不同。比方 Heroku 的执行阶段应用 Shell 脚本,而 Paketo 应用 Golang。后者的扩展性较强,由 Cloud Foundry 基金会反对,并领有由 VMware 资助的全职外围开发团队。这些小型模块化的 buildpack,能够通过组合扩大应用不同的场景。
当然还是那句话,本人上手写一个会更容易了解 Buildpack 的工作形式。
文章对立公布在公众号
云原生指北