作者:米開朗基楊,方闐
云原生正在吞并軟體世界,容器改變了傳統的應用開發模式,如今研發人員不僅要構建應用,還要使用 Dockerfile 來完成應用的容器化,將應用及其依賴關系打包,從而獲得更可靠的產品,提高研發效率,
隨著專案的迭代,達到一定的規模后,就需要運維團隊和研發團隊之間相互協作,運維團隊的視角與研發團隊不同,他們對鏡像的需求是安全和標準化,比如:
- 不同的應用應該選擇哪種基礎鏡像?
- 應用的依賴有哪些版本?
- 應用需要暴露的埠有哪些?
為了優化運維效率,提高應用安全性,研發人員需要不斷更新 Dockerfile 來實作上述目標,同時運維團隊也會干預鏡像的構建,如果基礎鏡像中有 CVE 被修復了,運維團隊就需要更新 Dockerfile,使用較新版本的基礎鏡像,總之,運維與研發都需要干預 Dockerfile,無法實作解耦,
為了解決這一系列的問題,涌現出了更加優秀的產品來構建鏡像,其中就包括 Cloud Native Buildpacks (СNB),CNB 基于模塊化提供了一種更加快速、安全、可靠的方式來構建符合 OCI 規范的鏡像,實作了研發與運維團隊之間的解耦,
在介紹 CNB 之前,我們先來闡述幾個基本概念,
符合 OCI 規范的鏡像
如今,容器運行時早就不是 Docker 一家獨大了,為了確保所有的容器運行時都能運行任何構建工具生成的鏡像,Linux 基金會與 Google,華為,惠普,IBM,Docker,Red Hat,VMware 等公司共同宣布成立開放容器專案(OCP),后更名為開放容器倡議(OCI),OCI 定義了圍繞容器鏡像格式和運行時的行業標準,給定一個 OCI 鏡像,任何實作 OCI 運行時標準的容器運行時都可以使用該鏡像運行容器,
如果你要問 Docker 鏡像與 OCI 鏡像之間有什么區別,如今的答案是:幾乎沒有區別,有一部分舊的 Docker 鏡像在 OCI 規范之前就已經存在了,它們被成為 Docker v1 規范,與 Docker v2 規范是不兼容的,而 Docker v2 規范捐給了 OCI,構成了 OCI 規范的基礎,如今所有的容器鏡像倉庫、Kubernetes 平臺和容器運行時都是圍繞 OCI 規范建立的,
什么是 Buildpacks
Buildpacks 專案最早由 Heroku 在 2011 年發起, 被以 Cloud Foundry 為代表的 PaaS 平臺廣泛采用,
一個 buildpack 指的就是一個將源代碼變成 PaaS 平臺可運行的壓縮包的程式,通常情況下,每個 buildpack 封裝了單一的語言生態系統的工具鏈,例如 Ruby、Go、NodeJs、Java、Python 等都有專門的 buildpack,
你可以將 buildpack 理解成一坨腳本,這坨腳本的作用是將應用的可執行檔案及其依賴的環境、配置、啟動腳本等打包,然后上傳到 Git 等倉庫中,打好的壓縮包被稱為 droplet,
然后 Cloud Foundry 會通過調度器選擇一個可以運行這個應用的虛擬機,然后通知這個機器上的 Agent 下載應用壓縮包,按照 buildpack 指定的啟動命令,啟動應用,
到了 2018 年 1 月,Pivotal 和 Heroku 聯合發起了 Cloud Native Buildpakcs(CNB) 專案,并在同年 10 月讓這個專案進入了 CNCF,

2020 年 11 月,CNCF 技術監督委員會(TOC)投票決定將 CNB 從沙箱專案晉升為范訓專案,是時候好好研究一下 CNB 了,
為什么需要 Cloud Native Buildpacks
Cloud Native Buildpacks(CNB) 可以看成是基于云原生的 Buildpacks 技術,它支持現代語言生態系統,對開發者屏蔽了應用構建、部署的細節,如選用哪種作業系統、撰寫適應鏡像作業系統的處理腳本、優化鏡像大小等等,并且會產出 OCI 容器鏡像,可以運行在任何兼容 OCI 鏡像標準的集群中,CNB 還擁抱了很多更加云原生的特性,例如跨鏡像倉庫的 blob 掛載和鏡像層級 rebasing,
由此可見 CNB 的鏡像構建方式更加標準化、自動化,與 Dockerfile 相比,Buildpacks 為構建應用提供了更高層次的抽象,Buildpacks 對 OCI 鏡像構建的抽象,就類似于 Helm 對 Deployment 編排的抽象,
2020 年 10 月,Google Cloud 開始宣布全面支持 Buildpacks,包含 Cloud Run、Anthos 和 Google Kubernetes Engine (GKE),目前 IBM Cloud、Heroku 和 Pivital 等公司皆已采用 Buildpacks,如果不出意外,其他云供應商很快就會效仿,
Buildpacks 的優點:
- 針對同一構建目的的應用,不用重復撰寫構建檔案(只需要使用一個 Builder),
- 不依賴 Dockerfile,
- 可以根據豐富的元資料資訊(buildpack.toml)輕松地檢查到每一層(buildpacks)的作業內容,
- 在更換了底層作業系統之后,不需要重新改寫鏡像構建程序,
- 保證應用構建的安全性和合規性,而無需開發者干預,

Buildpacks 社區還給出了一個表格來對比同類應用打包工具:

可以看到 Buildpacks 與其他打包工具相比,支持的功能更多,包括:快取、源代碼檢測、插件化、支持 rebase、重用、CI/CD 多種生態,
Cloud Native Buildpacks 作業原理
Cloud Native Buildpacks 主要由 3 個組件組成: Builder、Buildpack 和 Stack,
Buildpack
Buildpack 本質是一個可執行單元的集合,一般包括檢查程式源代碼、構建代碼、生成鏡像等,一個典型的 Buildpack 需要包含以下三個檔案:
- buildpack.toml – 提供 buildpack 的元資料資訊,
- bin/detect – 檢測是否應該執行這個 buildpack,
- bin/build – 執行 buildpack 的構建邏輯,最終生成鏡像,
Builder
Buildpacks 會通過“檢測”、“構建”、“輸出”三個動作完成一個構建邏輯,通常為了完成一個應用的構建,我們會使用到多個 Buildpacks,那么 Builder 就是一個構建邏輯的集合,包含了構建所需要的所有組件和運行環境的鏡像,
我們通過一個假設的流水線來嘗試理解 Builder 的作業原理:

- 最初,我們作為應用的開發者,準備了一份應用源代碼,這里我們將其標識為 “0”,
- 然后應用 “0” 來到了第一道工序,我們使用 Buildpacks1 對其進行加工,在這個工序中,Buildpacks1 會檢查應用是否具有 “0” 標識,如果有,則進入構建程序,即為應用標識添加 “1”,使應用標識變更為 “01”,
- 同理,第二道、第三道工序也會根據自身的準入條件判斷是否需要執行各自的構建邏輯,
在這個例子中,應用滿足了三道工序的準入條件,所以最終輸出的 OCI 鏡像的內容為 “01234” 的標識,
對應到 Buildpacks 的概念中,Builders 就是 Buildpacks 的有序組合,包含一個基礎鏡像叫 build image、一個 lifecycle 和對另一個基礎鏡像 run image 的應用,Builders 負責將應用源代碼構建成應用鏡像(app image),

build image 為 Builders 提供基礎環境(例如 帶有構建工具的 Ubuntu Bionic OS 鏡像),而 run image 在運行時為應用鏡像(app image)提供基礎環境,build image 和 run image 的組合被稱為 Stack,
Stack
上面提到,build image 和 run image 的組合被稱為 Stack,也就是說,它定義了 Buildpacks 的執行環境和最終應用的基礎鏡像,
你可以將 build image 理解為 Dockerfile 多階段構建中第一階段的 base 鏡像,將 run image 理解為第二階段的 base 鏡像,
上述 3 個組件都是以 Docker 鏡像的形式存在,并且提供了非常靈活的配置選項,還擁有控制所生成鏡像的每一個 layer 的能力,結合其強大的 caching 和 rebasing 能力,定制的組件鏡像可以被多個應用重復利用,并且每一個 layer 都可以根據需要單獨更新,
Lifecycle 是 Builder 中最重要的概念,它將由應用源代碼到鏡像的構建步驟抽象出來,完成了對整個程序的編排,并最終產出應用鏡像,下面我們單獨用一個章節來介紹 Lifecycle,
構建生命周期(Lifecyle)
Lifecycle 將所有 Buildpacks 的探測、構建程序抽離出來,分成兩個大的步驟聚合執行:Detect 和 Build,這樣一來就降低了 Lifecycle 的架構復雜度,便于實作自定義的 Builder,
除了 Detect 和 Build 這兩個主要步驟,Lifecycle 還包含了一些額外的步驟,我們一起來解讀,
Detect
我們之前提到,在 Buildpack 中包含了一個用于探測的 /bin/detect 檔案,那么在 Detect 程序中,Lifecycle 會指導所有 Buildpacks 中的 /bin/detect 按順序執行,并從中獲取執行結果,
那么 Lifecycle 把 Detect 和 Build 分開后,又是怎么維系這兩個程序中的關聯關系呢?
Buildpacks 在 Detect 和 Build 階段,通常都會告知在自己這個程序中會需要哪些前提,以及自己會提供哪些結果,

在 Lifecycle 中,提供了一個叫做 Build Plan 的結構體用于存放每個 Buildpack 的所需物和產出物,
type BuildPlanEntry struct {
Providers `toml:“providers”`
Requires `toml:"requires"`
同時,Lifecycle 也規定,只有當所有產出物都匹配有一個對應的所需物時,這些 Buildpacks 才能組合成一個 Builder,
Analysis
Buildpacks 在運行中會創建一些目錄,在 Lifecycle 中這些目錄被稱為 layer,那么為了這些 layer 中,有一些是可以作為快取提供給下一個 Buildpacks 使用的,有一些則是需要在應用運行時起作用的,還有的則是需要被清理掉,怎么才能更靈活地控制這些 layer ?
Lifecycle 提供了三個開關引數,用于表示每一個 layer 期望的處理方式:
- launch 表示這個 layer 是否將在應用運行時起作用,
- build 表示這個 layer 是否將在后續的構建程序中被訪問,
- cache 則表示這個 layer 是否將作為快取,
之后,Lifecycle 再根據一個關系矩陣來判斷 layer 的最終歸宿,我們也可以簡單的理解為,Analysis 階段為構建、應用運行提供了快取,

Build
Build 階段會利用 Detect 階段產出的 build plan,以及環境中的元資料資訊,配合保留至本階段的 layers,對應用原始碼執行 Buildpacks 中的構建邏輯,最終生成可運行的應用工件,

Export
Export 階段比較好理解,在完成了上述構建之后,我們需要將最后的構建結果產出為一個 OCI 標準鏡像,這樣一來,這個 App 工件就可以運行在任何兼容 OCI 標準的集群中,

Rebase
在 CNB 的設計中,最后 app 工件實際是運行在 stack 的 run image 之上的,可以理解為 run image 以上的工件是一個整體,它與 run image 以 ABI(application binary interface) 的形式對接,這就使得這個工件可以靈活切換到另一個 run image 上,
這個動作其實也是 Lifecycle 的一部分,叫做 rebase,在構建鏡像的程序中也有一次 rebase,發生在 app 工件由 build image 切換到 run image 上,

這種機制也是 CNB 對比 Dockerfile 最具優勢的地方,比如在一個大型的生產環境中,如果容器鏡像的 OS 層出現問題,需要更換鏡像的 OS 層,那么針對不同型別的應用鏡像就需要重寫他們的 dockerfile 并驗證新的 dockerfile 是否可行,以及新增加的層與已存在的層之間是否有沖突,等等,而使用 CNB 只需要做一次 rebase 即可,簡化了大規模生產中鏡像的升級作業,
以上就是關于 CNB 構建鏡像的流程分析,總結來說:
- Buildpacks 是最小構建單元,執行具體的構建操作;
- Lifecycle 是 CNB 提供的鏡像構建生命周期介面;
- Builder 是若干 Buildpacks 加上 Lifecycle 以及 stack 形成的具備特定構建目的的構建器,

再精減一下:
- build image + run image = stack
- stack(build image) + buildpacks + lifecycle = builder
- stack(run image) + app artifacts = app
那么現在問題來了,這個工具怎么使用呢?
Platform
這時候就需要一個 Platform,Platform 其實是 Lifecycle 的執行者,它的作用是將 Builder 作用于給定的源代碼上,完成 Lifecycle 的指令,

在這個程序中,Builder 會將源代碼構建為 app,這個時候 app 是在 build image 中的,這個時候根據 Lifecycle 中的 rebase 介面,底層邏輯是是用 ABI(application binary interface) 將 app 工件從 build image 轉換到 run image 上,這就是最后的 OCI 鏡像,
常用的 Platform 有 Tekton 和 CNB 的 Pack,接下來我們將使用 Pack 來體驗如何使用 Buildpacks 構建鏡像,
安裝 Pack CLI 工具
目前 Pack CLI 支持 Linux、MacOS 和 Windows 平臺,以 Ubuntu 為例,安裝命令如下:
$ sudo add-apt-repository ppa:cncf-buildpacks/pack-cli
$ sudo apt-get update
$ sudo apt-get install pack-cli
查看版本:
$ pack version
0.22.0+git-26d8c5c.build-2970
注意:在使用 Pack 之前,需要先安裝并運行 Docker,
目前 Pack CLI 只支持 Docker,不支持其他容器運行時(比如 Containerd 等),但 Podman 可以通過一些 hack 來變相支持,以 Ubuntu 為例,大概步驟如下:
先安裝 podman,
$ . /etc/os-release
$ echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
$ curl -L "https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key" | sudo apt-key add -
$ sudo apt-get update
$ sudo apt-get -y upgrade
$ sudo apt-get -y install podman
然后啟用 Podman Socket,
$ systemctl enable --user podman.socket
$ systemctl start --user podman.socket
指定 DOCKER_HOST 環境變數,
$ export DOCKER_HOST="unix://$(podman info -f "{{.Host.RemoteSocket.Path}}")"
最終就可以實作在 Podman 容器運行時中使用 Pack 來構建鏡像,詳細配置步驟可參考 Buildpacks 官方檔案,
使用 Pack 構建 OCI 鏡像
安裝完 Pack 之后,我們可以通過 CNB 官方提供的 samples 加深對 Buildpacks 原理的理解,這是一個 Java 示例,構建程序中無需安裝 JDK、運行 Maven 或其他構建環境,Buildpacks 會為我們處理好這些,
首先克隆示例倉庫:
$ git clone https://github.com/buildpacks/samples.git
后面我們將使用 bionic 這個 Builder 來構建鏡像,先來看下該 Builder 的配置:
$ cat samples/builders/bionic/builder.toml
# Buildpacks to include in builder
[[buildpacks]]
id = "samples/java-maven"
version = "0.0.1"
uri = "../../buildpacks/java-maven"
[[buildpacks]]
id = "samples/kotlin-gradle"
version = "0.0.1"
uri = "../../buildpacks/kotlin-gradle"
[[buildpacks]]
id = "samples/ruby-bundler"
version = "0.0.1"
uri = "../../buildpacks/ruby-bundler"
[[buildpacks]]
uri = "docker://cnbs/sample-package:hello-universe"
# Order used for detection
[[order]]
[[order.group]]
id = "samples/java-maven"
version = "0.0.1"
[[order]]
[[order.group]]
id = "samples/kotlin-gradle"
version = "0.0.1"
[[order]]
[[order.group]]
id = "samples/ruby-bundler"
version = "0.0.1"
[[order]]
[[order.group]]
id = "samples/hello-universe"
version = "0.0.1"
# Stack that will be used by the builder
[stack]
id = "io.buildpacks.samples.stacks.bionic"
run-image = "cnbs/sample-stack-run:bionic"
build-image = "cnbs/sample-stack-build:bionic"
builder.toml 檔案中完成了對 Builder 的定義,配置結構可以劃分為 3 個部分:
- [[buildpacks]] 語法標識用于定義 Builder 所包含的 Buildpacks,
- [[order]] 用于定義 Builder 所包含的 Buildpacks 的執行順序,
- [[stack]] 用于定義 Builder 將運行在哪個基礎環境之上,
我們可以使用這個 builder.toml 來構建自己的 builder 鏡像:
$ cd samples/builders/bionic
$ pack builder create cnbs/sample-builder:bionic --config builder.toml
284055322776: Already exists
5b7c18d5e17c: Already exists
8a0af02bbad1: Already exists
0aa0fb9222a5: Download complete
3d56f4bc2c9a: Already exists
5b7c18d5e17c: Already exists
284055322776: Already exists
8a0af02bbad1: Already exists
a967314b5694: Already exists
a00d148009e5: Already exists
dbb2c49b44e3: Download complete
53a52c7f9926: Download complete
0cceee8a8cb0: Download complete
c238db6a02a5: Download complete
e925caa83f18: Download complete
Successfully created builder image cnbs/sample-builder:bionic
Tip: Run pack build <image-name> --builder cnbs/sample-builder:bionic to use this builder
接著,進入 samples/apps 目錄,使用 pack 工具和 builder 鏡像,完成應用的構建,當構建成功后,會產出一個名為 sample-app 的 OCI 鏡像,
$ cd ../..
$ pack build --path apps/java-maven --builder cnbs/sample-builder:bionic sample-app
最后使用 Docker 運行這個 sample-app 鏡像:
$ docker run -it -p 8080:8080 sample-app
訪問 http://localhost:8080,如果一切正常,你可以在瀏覽器中看見如下的界面:

現在我們再來觀察一下之前構建的鏡像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cnbs/sample-package hello-universe e925caa83f18 42 years ago 4.65kB
sample-app latest 7867e21a60cd 42 years ago 300MB
cnbs/sample-builder bionic 83509780fa67 42 years ago 181MB
buildpacksio/lifecycle 0.13.1 76412e6be4e1 42 years ago 16.4MB
鏡像的創建時間竟然都是固定的時間戳:42 years ago,這是為什么呢?如果時間戳不固定,每次構建鏡像的 hash 值都是不同的,一旦 hash 值不一樣,就不太容易判斷鏡像的內容是否相同了,使用固定的時間戳,就可以重復利用之前的構建程序中創建的 layers,
總結
Cloud Native Buildpacks 代表了現代軟體開發的一個重大進步,在大部份場景下相對于 Dockerfile 的好處是立桿見影的,雖然大型企業需要投入精力重新調整 CI/CD 流程或撰寫自定義 Builder,但從長遠來看可以節省大量的時間和維護成本,
本文介紹了 Cloud Native Buildpacks(CNB) 的起源以及相對于其他工具的優勢,并詳細闡述了 CNB 的作業原理,最后通過一個簡單的示例來體驗如何使用 CNB 構建鏡像,后續的文章將會介紹如何創建自定義的 Builder、Buildpack、Stack,以及函式計算平臺(例如,OpenFunction、Google Cloud Functions)如何利用 CNB 提供的 S2I 能力,實作從用戶的函數代碼到最終應用的轉換程序,
本文由博客一文多發平臺 OpenWrite 發布!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/404286.html
標籤:其他
