无需 Dockerfile 的镜像构建:BuildPack vs Dockerfile
作者 |?Addo Zhang
來源 |?云原生指北
過去的工作中,我們使用微服務、容器化以及服務編排構建了技術平臺。為了提升開發團隊的研發效率,我們同時還提供了 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 項目[1]的 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[2]?是一個程序,它能將源代碼轉換成容器鏡像的并可以在任意云環境中運行。通常 buildpack 封裝了單一語言的生態工具鏈。適用于 Java、Ruby、Go、NodeJs、Python 等。
buildpacks.ioBuilder 是什么?
一些 buildpacks 按順序組合之后就是?builder,除了 buildpacks, builder 中還加入了?生命周期[3]?和 stack 容器鏡像。
stack 容器鏡像由兩個鏡像組成:用于運行 buildpack 的鏡像 build image,以及構建應用鏡像的基礎鏡像 run image。如上圖,就是 builder 中的運行環境。
Buildpack 的工作方式
how buildpack works每個 buildpack 運行時都包含了兩個階段:
phases1. 檢測階段
通過檢查源代碼中的某些特定文件/數據,來判斷當前 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[4]?Google Buildpacks[5]?Paketo[6]
但是正式詳細介紹開源的 buildpacks 之前,我們還是通過自己創建 buildpack 的方式來深入了解 Buildpacks 的工作方式。測試項目呢,我們還是用測試 Tekton 的 Java 項目[7]。
下面所有的內容都提交到了?Github[8]?上,可以訪問:https://github.com/addozhang/buildpacks-sample 獲取相關代碼。
最終的目錄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 exit100 fi plan_path=$2 cat >>"${plan_path}"<<EOL [[provides]] name ="jdk" [[requires]] name ="jdk" EOLbin/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}]];thenmkdir -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);docat >>"${layers_dir}/launch.toml"<<EOL [[processes]] type ="web" command ="java -jar ${jar_file}" EOL break; donebuildpack.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]Adding1/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]Settingdefault 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 . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ (()\___ |'_ | '_||'_ \/ _`| \ \ \ \\\/ ___)||_)|||||||(_||)))) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ ::SpringBoot::(v2.2.3.RELEASE) ...總結
其實現在有很多開源的 buildpack 可以用,沒有特定定制的情況下無需自己手動編寫。比如下面的幾個大廠開源并維護的 Buildpacks:
?Heroku Buildpacks[9]?Google Buildpacks[10]?Paketo[11]
上面幾個 buildpacks 庫內容比較全面,實現上會有些許不同。比如 Heroku 的執行階段使用 Shell 腳本,而 Paketo 使用 Golang。后者的擴展性較強,由 Cloud Foundry 基金會支持,并擁有由 VMware 贊助的全職核心開發團隊。這些小型模塊化的 buildpack,可以通過組合擴展使用不同的場景。
當然還是那句話,自己上手寫一個會更容易理解 Buildpack 的工作方式。
引用鏈接
[1]?測試 Tekton 的 Java 項目:?https://github.com/addozhang/tekton-test
[2]?BuildPack:?https://buildpacks.io/
[3]?生命周期:?https://buildpacks.io/docs/concepts/components/lifecycle/
[4]?Heroku Buildpacks:?https://github.com/heroku/
[5]?Google Buildpacks:?https://github.com/GoogleCloudPlatform/buildpacks
[6]?Paketo:?https://github.com/paketo-buildpacks
[7]?測試 Tekton 的 Java 項目:?https://github.com/addozhang/tekton-test
[8]?Github:?https://github.com/addozhang/buildpacks-sample
[9]?Heroku Buildpacks:?https://github.com/heroku/
[10]?Google Buildpacks:?https://github.com/GoogleCloudPlatform/buildpacks
[11]?Paketo:?https://github.com/paketo-buildpacks
往期推薦
云計算到底是誰發明的?
從Docker的信號機制看容器的優雅停止
Redis會遇到的坑,你踩過幾個?
內容整理志愿者招募了!
點分享
點收藏
點點贊
點在看
總結
以上是生活随笔為你收集整理的无需 Dockerfile 的镜像构建:BuildPack vs Dockerfile的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 北森iTalentX 3.0:聚焦场景一
- 下一篇: “融合、智能、绿色”施耐德电气线上工博以