七張圖理解 Dockerfiles vs Buildpacks,二者如何選擇?
與腳本化 Dockerfile 相比,聲明式云原生 buildpack 支持一些新場景。
容器無處不在
容器在大多數軟件交付管道中無處不在,有時不是直接可見的,但它們存在于幕后。無論我們是在 Kubernetes、普通 Docker 主機、serverless functions 還是許多其他編排平臺上運行軟件,容器都代表了不可變的可運行軟件工件。
將應用程序源代碼轉換為正在運行的應用程序需要一個中間容器構建階段,而將軟件轉換為容器的一種非常流行的方法是通過 Dockerfile。
Dockerfiles
從 Dockerfile 構建容器是一種腳本化方法,Dockerfile 中的大部分 內容基本上是構建軟件、安裝依賴項等所需的命令。這也意味著學習如何使用 Dockerfile 的學習曲線很淺,并且現(xiàn)有構建腳本可以毫不費力地將其移植到 Dockerfile 中。
然而,事實證明,制作高質量的容器鏡像并非易事?;ヂ?lián)網上充滿了制作小型、安全、最佳實踐鏡像的指南。通常圍繞:
- 確保正確處理信號,例如SIGTERM是容器合約的一部分。
- 不要使用 root 用戶運行應用程序。
- 不要在容器中包含不必要的工具、機密或構建工件。
- 按照優(yōu)化緩存的順序添加層,例如首先更改最少的層。
- 正確標記和簽署鏡像。
下面說明了一個 Dockerfile,它為 NodeJS 應用程序實現(xiàn)了其中的一些建議;使用兩階段構建生成一個小鏡像,以非 root 身份運行,并仔細排序操作以改進緩存:
FROM node:16.13.1-alpine3.14 AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json .
COPY src src
RUN npm run build
RUN npm prune — production
FROM node:16.13.1-alpine3.14
WORKDIR /usr/src/app
USER node
ENV NODE_ENV production
COPY — from=builder /usr/src/app/node_modules/ ./node_modules
COPY — from=builder /usr/src/app/dist/ ./dist
EXPOSE 8000
CMD [ "node", "/usr/src/app/dist/main.js" ]
編寫高質量的 Dockerfile 需要相當多的努力,有時 Dockerfile 通常只是其他項目的 Dockerfile 的副本。這會導致 Dockerfile 碎片化,對于擁有多個容器的組織來說,這很快就會變得難以管理。
Buildpacks
Buildpacks 源于這樣一種想法,即對于給定類型的大多數應用程序,將應用程序源代碼轉換為容器或多或少是相同的。這意味著我們可以為這個過程設計可重用的程序。這個概念自 Heroku 發(fā)起并被 Cloud Foundry、Google App engine、Gitlab、CircleCI 等采用以來,已經發(fā)展超過 10 年。
社區(qū)努力確保它們提供高質量的容器鏡像構建,而不是為每個應用程序使用碎片化 Dockerfiles,并具有不同級別的安全和最佳實踐。
構建容器鏡像的聲明式方法
使用 buildpacks 時,我們需要了解 buildpacks 如何構建容器。與其編寫腳本如何使用 Dockerfiles 構建容器,不如聲明我們期望在容器中打包的內容,并讓 buildpacks 找出細節(jié)。
Buildpacks 實現(xiàn)了許多階段,其中兩個是:
- Detect:每個 buildpack 檢測它是否可以參與容器構建。例如,對于 NodeJS 應用程序,_npm_ buildpack 可能會查找package.json文件。如果找到,buildpack 將通知構建器它可以參與構建并貢獻依賴項。Python buildpack 會查找requirements.txt文件,但是,不會在 NodeJS 應用程序中找到它,因此不會參與構建。
- Build:在這個階段,所有表明它們可以參與構建的 Buildpacks 都將被執(zhí)行以實現(xiàn)構建。如上所述,重要的是我們知道 Buildpacks 如何檢測應該構建的內容。即 NodeJS 應用程序的開發(fā)人員應確保項目包含一個 package.json 文件。此外,如果我們期望某個版本的 Node 運行時或其他依賴項,則應在 package.json 文件中明確說明,例如:
{
"engines": {
"node": "16.13.1",
"npm": "8.1.2"
},
"dependencies": {
"express": "4.17.2"
}
}
容器鏡像攜帶多種形式的元數據,Buildpacks 通常使用環(huán)境變量來聲明元數據設置。以下是使用環(huán)境變量設置標準org.opencontainers.description標簽的示例,該標簽使用Paketo image-labels buildpack在容器鏡像上。環(huán)境變量的配置是通過 project.toml 文件完成的,這是聲明構建配置的常用格式:
[[build.env]]
name = "BP_OCI_DESCRIPTION"
value = "Sample NodeJS from https://github.com/MichaelVL/buildpacks"
了解 Buildpacks 的這種聲明性 API 對開發(fā)人員來說是必不可少的。開發(fā)人員不應該關心 Dockerfile,他們應該知道 buildpacks 的聲明式 API。
可復制的構建、交付鏈安全
Buildpacks 努力實現(xiàn)可重現(xiàn)的構建。構建過程是完全確定的,并且在使用相同的輸入執(zhí)行時會產生相同的輸出。這使我們能夠準確地驗證將哪個應用程序二進制文件或源打包到容器中,并且可以保護軟件交付鏈免受惡意應用程序被打包到容器中。
當使用 Dockerfiles 構建鏡像時,容器鏡像(及其哈希值 sha256 摘要)在每次重建鏡像時都會發(fā)生變化,即使提供完全相同的輸入也是如此。
可重現(xiàn)的構建是軟件工件供應鏈級別第 4 級的要求,而 Buildpacks 是提高容器鏡像供應鏈安全性的重要工具。
可重現(xiàn)的構建也是一種避免不必要的容器層重建的有效機制。
改進的重建速度和緩存
Dockerfile 中的每一行基本上都會為最終的容器鏡像貢獻一層。除非先前的圖層發(fā)生更改,否則圖層會被緩存并重用。當前面的層更改時,所有后續(xù)層都將重新構建和更改,因為 Docker 不應用可重現(xiàn)的構建。
使用 buildpacks,每個 buildpack 都會為容器鏡像貢獻一個層。如果 buildpack 的輸入沒有改變,則 buildpack 層不會改變,無論前面的層是否改變。此外,使用 buildpack 可以替換每個層,而不會影響其他 buildpack 生成的層。
如果需要更新容器基礎鏡像(例如由于安全問題),我們可以在容器鏡像中重新設置該層,而無需重建任何其他應用程序層。這將很難使用基于 Dockerfile 的工作流來實現(xiàn)。
單層 rebase 不僅是容器鏡像構建時間的顯著改進,而且對于容器的部署也是如此。想象一下有 10 個服務使用某個基礎鏡像。如果安全問題導致我們更新此基礎鏡像,則基于 Dockerfile 的構建將導致重建所有鏡像的所有層。這會消耗構建時間并下載完整的 10 個新鏡像。使用 buildpack rebase 方法,我們只需要檢索一個新的共享基礎鏡像。
容器已經 42 歲了?
如果使用 buildpacks 構建容器,您將遇到意想不到的時間戳:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
sample-app latest 4713a8f7d9bb 42 years ago 202MB
這是可重現(xiàn)構建的副作用。為了實現(xiàn)可重現(xiàn)的構建,時間被凍結在一個固定點,以避免任何與時間相關的數據導致容器鏡像差異。Buildpacks 構建具有1980 年 00:00:01 的時間戳來源的鏡像,這是為了與舊文件格式的兼容而選擇的。
使用 Buildpack 時的角色
當使用 buildpacks 構建容器時,有兩個角色:
- 應用程序開發(fā)人員:開發(fā)人員的重點應該是以應用程序為中心,他們與容器構建相關的主要職責是提供應用程序源、依賴版本(例如 NodeJS 應用程序的package.json)和應用程序名稱和版本等元數據。對于構建容器,他們將完全控制權委托給組織策劃的構建器/Buildpacks。要將大多數問題委托給平臺運營商,開發(fā)人員應該使用最新構建器/Buildpacks,而不是修復構建器版本。
- 構建平臺運營商:構建平臺運營商的職責是為開發(fā)人員提供一個精選的構建器和一組 Buildpacks,以申請構建容器。這主要包括利用像Paketo buildpacks這樣的社區(qū)構建器,但運營商可以選擇添加額外的,可能是自行設計的 buildpacks 用于公司特定目的。這可以是例如添加與公司相關的元數據。構建平臺運營商的主要關注點還在于:容器運行鏡像(容器基礎鏡像)的更新和安全性。
這種關注點分離對于維護多個容器鏡像的組織非常有價值,因為這允許平臺構建操作員控制容器鏡像。例如,如果在運行鏡像中發(fā)現(xiàn)安全問題,則可以通過更改構建器運行鏡像,使用新的運行鏡像重建所有容器。如上所述,這將是一個非常有效的層變基。使用 Dockerfile,這將需要更新所有應用程序 Dockerfile 中的基礎鏡像,并會觸發(fā)所有容器鏡像層的重建。
容器里有什么軟件材料清單
buildpacks 增強 Dockerfile 構建過程的一個示例是如何將構建相關的元數據附加到容器鏡像。軟件項目包含許多依賴項,并且有關這些依賴項的信息在鏡像構建期間嵌入到容器鏡像中。
軟件物料清單 (SBOM) 是進入容器鏡像的組件的結構化列表。這可用于通過將 SBOM 與已知安全問題進行比較,來確保僅使用安全軟件。SBOM 還可用于驗證哪個軟件許可證管理軟件等。CycloneDX和SPDX是結構化 SBOM 數據的兩個通用標準。
下面是來自 NodeJS 項目的 SBOM 的摘錄,我們可以從 SBOM 中看到所使用的 Node 引擎的確切版本,以及提供它的 buildpack:
{
{
"name": "Node Engine",
"metadata": {
"source": {
"checksum": {
"hash": "34b23965457fb0587cda6fa898e5d030211f5f374cb6"
},
"uri": "https://nodejs.org/.../node-v16.13.1.tar.gz"
},
"version": "16.13.1"
},
"buildpacks": {
"id": "paketo-buildpacks/node-engine",
"version": "0.11.2"
}
}
}
使用 SBOM 來保護軟件是對使用容器鏡像掃描儀的有力補充。由于 SBOM 是由 buildpacks 創(chuàng)建的,因此它將精確且清楚地識別交付鏈。容器鏡像掃描儀將不得不從容器的實際內容中扣除這個版本,這將不太精確。
如何使用 Buildpacks?
使用云原生 Buildpacks,構建過程和 Buildpacks 包含在兩個容器鏡像中:一個構建器鏡像和一個運行鏡像。在某種程度上,它可以被看作是一個改進的 Dockerfile 兩階段構建。下圖說明了兩階段 Dockerfile 構建如何映射到 Buildpacks。請注意 NodeJS 應用程序的構建邏輯如何映射到buildpack(通常這種應用程序構建將分布在多個 buildpack 中)
構建器鏡像包含一組有序的 Buildpacks 構建事物的邏輯、一個生命周期組件 Buildpacks 的編排器和對運行鏡像的引用。Dockerfile 與生命周期組件沒有并行性,因為 Dockerfile 是線性處理的。在檢測階段,Buildpacks 將選擇加入或退出構建,生命周期組件對此進行管理。
最后,需要一個工具來觸發(fā)生命周期組件:非常類似于 docker build 命令。為此,存在許多工具,其中最廣為人知的是 pack 和 Tekton,然而,像 CircleCI 和 Gitlab 這樣的商業(yè)持續(xù)集成供應商也支持使用 buildpacks 進行構建。
可以在此處找到使用pack 的 Github actions 工作流示例。
結論
雖然經驗豐富的 Dockerfile 作者在從 Dockerfiles 遷移到 buildpacks 時可能會感到失去了對細節(jié)的控制,但上面概述的優(yōu)勢有望意味著使用 buildpacks 時會以開放的心態(tài)來對待。我相信大多數開發(fā)人員都會喜歡 buildpacks。構建容器和維護 Dockerfile 從來不是他們的主要關注點,而是部署應用程序所需的必要步驟。構建平臺操作員、SRE 和安全團隊應該喜歡 buildpacks,因為它恢復了對容器鏡像和容器鏡像中內置工件的控制。
用云原生 buildpack 替換 Dockerfile 會改變我們構建容器鏡像的方式:
對于開發(fā)人員和組織而言,重點關注聲明性部分很重要。學習如何聲明容器應該如何構建以及正在構建什么將是最重要的。
buildpacks 完美嗎?
不完全是。在寫這篇文章時,我嘗試復制一開始提出的兩階段 Dockerfile 的精益容器構建,但是,這并不完全可行(使用 Paketo 構建器)生成的容器的大小要大一些。但是,我預計 buildpacks 會在未來改進。
還有一些應用程序很難用 buildpacks 打包到容器中。例如,如果沒有自定義 Buildpacks,打包在容器(VM 風格的容器)中的遺留單體應用程序將難以實現(xiàn)。此類應用程序不能很好地與 buildpacks 配合使用,并且可能需要對 Dockerfile 進行低級控制,這些應用程序可能是容器打包應該注意的問題。