乐趣区

关于云计算:无需-Dockerfile-的镜像构建BuildPack-vs-Dockerfile

过来的工作中,咱们应用微服务、容器化以及服务编排构建了技术平台。为了晋升开发团队的研发效率,咱们同时还提供了 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 中的每个命令对应,比方 RUNCOPYADD。某些特定的指令会创立一个新的层,在镜像构建过程中,如果某些层没有发生变化,就会从缓存中获取。

在上面的 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. 构建阶段

在构建阶段会进行如下操作:

  1. 设置构建环境和运行时环境
  2. 下载依赖并编译源码(如果需要的话)
  3. 设置正确的 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 目录中别离创立 buildrun 两个目录:

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 的工作形式。

文章对立公布在公众号 云原生指北

退出移动版