在 Kubernetes 環境下,容器化構建已成為 CI/CD 流程中不可或缺的一環。Buildah 作為一個強大的容器映像檔構建工具,其彈性與安全性使其成為在容器環境中構建映像檔的理想選擇。本文將探討如何在容器內執行 Buildah,並著重於安全性與效能的最佳實踐。我們將探討如何在無 Root 許可權的容器中執行 Buildah,並利用 Volume 儲存構建結果。同時,我們也會探討如何使用 SELinux 的 Multi-Category Security (MCS) 功能來強化安全性,並分享如何在 Tekton Pipelines 中整合 Buildah,實作雲原生 CI/CD 流程。最後,我們將示範如何使用 Go 語言開發自定義的容器映像檔構建器,並整合 Buildah 的核心功能。
在 Kubernetes 上,容器化構建通常仰賴第三方工具,例如 OpenShift 的 S2I 或 Google 的 kaniko。然而,Buildah 提供了更高度的自定義性和彈性,允許開發者根據自身需求開發構建流程。結合 Tekton Pipelines,更能將 Buildah 的能力融入雲原生 CI/CD 流程中。透過 Tekton 的 Task 和 Pipeline,我們可以定義複雜的構建流程,並將 Buildah 作為其中一個關鍵步驟。
為了確保安全性,我們建議在無 Root 許可權的容器中執行 Buildah。這可以有效降低安全風險,並防止惡意程式碼入侵主機系統。同時,使用 Volume 儲存構建結果,可以方便地管理和分享構建產物。更進一步地,我們可以使用 SELinux 的 MCS 功能來隔離 Volume,確保只有授權的容器才能存取構建結果。
除了使用預先建置的 Buildah 映像檔,我們也可以使用 Go 語言開發自定義的構建器,並整合 Buildah 的核心功能。這使得我們可以更精細地控制構建流程,並根據實際需求進行客製化。透過 Go 語言的 containers/buildah 和 containers/storage 函式庫,我們可以輕鬆地操作容器映像檔,並實作各種構建邏輯。
在容器中執行 Buildah:安全、隔離與實戰
在雲原生時代,容器化構建已成為應用程式開發和佈署的關鍵環節。Buildah 和 Podman 的 fork/exec 架構,使得在容器內執行構建任務變得輕而易舉,即使在無 root 許可權的環境中也能順利進行。本文將探討在容器中執行 Buildah 的各種情境,著重於安全性考量,並分享實際操作經驗。
Kubernetes 上的容器化構建需求
Kubernetes 作為容器協調引擎,雖然本身不具備原生構建功能,但其靈活的設計允許我們整合各種構建工具。許多解決方案應運而生,例如 Red Hat OpenShift 的 Source-to-Image (S2I) 和 Google 的 kaniko。這些工具各有優勢,但我們也可以利用 Buildah 在 Kubernetes 上實作自訂的容器化構建流程。
Tekton Pipelines:雲原生 CI/CD 的利器
Tekton Pipelines 是一個 CNCF 專案,提供了一種雲原生的 CI/CD 解決方案。它根據 Kubernetes 的自訂資源(Custom Resources)來驅動 Pipeline,讓構建流程更加靈活和可擴充套件。Tekton Pipeline 由多個 Task 組成,你可以從 Tekton Hub (https://hub.tekton.dev/) 取得現成的 Task,例如 Buildah 的 Task (https://hub.tekton.dev/tekton/task/buildah),或者根據自身需求建立自訂 Task。
無 Root 許可權的 Buildah 容器與 Volume 儲存
接下來,我們將示範如何在無 root 許可權的容器中執行 Buildah,並將構建結果儲存在 Volume 中。
首先,使用 quay.io/buildah/stable 映象啟動一個容器:
podman run --device /dev/fuse \
-v ~/build:/build:z \
-v storevol:/var/lib/containers quay.io/buildah/stable \
buildah build -t build_test1 /build
這個指令包含幾個值得注意的選項:
--device /dev/fuse:在容器中載入 fuse 核心模組,這是執行 fuse-overlay 命令所必需的。-v ~/build:/build:z:將主機的~/build目錄掛載到容器的/build目錄,:z字尾用於設定 SELinux 標籤。-v storevol:/var/lib/containers:建立一個名為storevol的 Volume,並將其掛載到容器的預設儲存路徑/var/lib/containers。所有構建產生的 Layer 都會儲存在這個 Volume 中。
驗證構建結果
構建完成後,可以使用以下指令來檢視構建出的 Image:
podman run --rm -v storevol:/var/lib/containers quay.io/buildah/stable buildah images
輸出結果會顯示 build_test1 Image 已經成功建立,並且 Layer 儲存在 storevol Volume 中。
若要檢視 Volume 的內容,可以使用 podman volume inspect 指令取得 Volume 的掛載點,然後使用 ls -alR 指令遞迴列出 Volume 內容:
ls -alR $(podman volume inspect storevol --format '{{.Mountpoint}}')
推播 Image 到遠端 Registry
接下來,我們可以將構建出的 Image 推播到遠端 Registry。首先,使用以下指令標記 Image:
podman run --rm -v storevol:/var/lib/containers \
quay.io/buildah/stable \
sh -c 'buildah tag build_test1 \
registry.example.com/build_test1'
然後,登入 Registry:
podman run --rm -v storevol:/var/lib/containers \
quay.io/buildah/stable \
sh -c 'buildah login -u=<USERNAME> -p=<PASSWORD> \
registry.example.com'
最後,推播 Image:
podman run --rm -v storevol:/var/lib/containers \
quay.io/buildah/stable \
sh -c 'buildah push registry.example.com/build_test1'
成功推播 Image 後,可以安全地移除 Volume:
podman volume rm storevol
安全性考量:SELinux 與 MCS
雖然上述方法可以正常運作,但存在一個潛在的安全風險:storevol Volume 並未被隔離,任何其他容器都可以存取其內容。為瞭解決這個問題,可以使用 SELinux 的 Multi-Category Security (MCS) 功能,透過 :Z 字尾將 Volume 標記為僅允許執行中的容器存取。
使用容器化 Buildah 整合現有應用程式建置流程
在整合 Buildah 到現有的應用程式建置流程時,可以考慮使用容器化的 Buildah。以下將探討如何透過不同的方式在容器中執行 Buildah,以及各種方法的優缺點。
單一容器建置、標記與推播
最簡單的方法是在單一容器中執行建置、標記和推播命令。這種方法可以減少複雜性,但需要仔細處理安全問題。
$ podman run --device /dev/fuse \
-v ~/build:/build \
-v secure_storevol:/var/lib/containers:Z \
quay.io/buildah/stable \
sh -c 'buildah build -t test2 /build && \
buildah tag test2 registry.example.com/build_test2 && \
buildah login -u=<USERNAME> \
-p=<PASSWORD> \
registry.example.com && \
buildah push registry.example.com/build_test2'
內容解密:
podman run:使用 Podman 執行容器。--device /dev/fuse:允許容器使用 FUSE(Userspace file system)檔案系統。-v ~/build:/build:將主機的~/build目錄掛載到容器的/build目錄。-v secure_storevol:/var/lib/containers:Z:掛載一個卷宗到容器的/var/lib/containers目錄,:Z選項確保 SELinux 的多類別安全 (MCS) 隔離。quay.io/buildah/stable:使用的 Buildah 映像檔。sh -c '...':在容器內執行的 shell 命令。buildah build -t test2 /build:使用/build目錄中的 Dockerfile 建置映像檔,並標記為test2。buildah tag test2 registry.example.com/build_test2:將映像檔標記為registry.example.com/build_test2,準備推播。buildah login -u=<USERNAME> -p=<PASSWORD> registry.example.com:登入到映像檔倉函式庫。請注意,直接在命令列中傳遞使用者名稱和密碼是不安全的。buildah push registry.example.com/build_test2:將映像檔推播到映像檔倉函式庫。
更安全的身份驗證方式
直接在命令列中傳遞敏感資料(如使用者名稱和密碼)是不安全的。更安全的做法是掛載包含有效會話權杖的身份驗證檔案到容器中。
$ podman run --device /dev/fuse \
-v ~/build:/build \
-v /run/user/<UID>/containers/auth.json:/auth.json:z \
-v secure_storevol:/var/lib/containers:Z \
quay.io/buildah/stable \
sh -c 'buildah build -t test3 /build && \
buildah tag test3 registry.example.com/build_test3 && \
buildah push --authfile /auth.json \
registry.example.com/build_test3'
內容解密:
-v /run/user/<UID>/containers/auth.json:/auth.json:z:掛載包含身份驗證資訊的auth.json檔案到容器的/auth.json。--authfile /auth.json:告訴buildah push命令使用/auth.json檔案進行身份驗證。
可以使用以下 Podman 命令來進行身份驗證:
$ podman login –u <USERNAME> -p <PASSWORD> <REGISTRY>
如果身份驗證成功,取得的權杖會儲存在 /run/user/<UID>/containers/auth.json 檔案中,該檔案儲存一個 JSON 編碼的物件,其結構類別似於以下範例:
{
"auths": {
"registry.example.com": {
"auth": "<base64_encoded_token>"
}
}
}
安全警示!
如果掛載到容器內的身份驗證檔案包含多個不同倉函式庫的身份驗證記錄,這些記錄將會在建置容器內公開。這可能會導致潛在的安全問題,因為容器將能夠使用檔案中指定的權杖對這些倉函式庫進行身份驗證。
使用繫結掛載儲存執行 Buildah 容器
在最高隔離情境中,每個建置容器都應該有自己的隔離儲存,該儲存在建置開始時填充,並在完成後銷毀。使用 SELinux MCS 安全性可以輕鬆實作隔離。
# BUILD_STORE=/var/lib/containers-$(echo $RANDOM | md5sum | head -c 8)
# mkdir $BUILD_STORE
# podman run --device /dev/fuse \
-v ./build:/build:z \
-v $BUILD_STORE:/var/lib/containers:Z \
-v /run/containers/0/auth.json:/auth.json \
quay.io/buildah/stable \
bash -c 'set -euo pipefail; \
buildah build -t registry.example.com/test4 /build; \
buildah push --authfile /auth.json \
registry.example.com/test4'
內容解密:
BUILD_STORE=/var/lib/containers-$(echo $RANDOM | md5sum | head -c 8):建立一個臨時目錄,用於儲存建置層。mkdir $BUILD_STORE:建立該目錄。-v $BUILD_STORE:/var/lib/containers:Z:將該目錄繫結掛載到容器的/var/lib/containers目錄,:Z選項確保多類別安全隔離。
MCS 隔離保證與其他容器的隔離。每個建置容器都將有自己的自訂儲存,這意味著每次執行都需要重新提取基礎映像檔層,因為它們永遠不會被快取。
效能與安全性的權衡
最安全的隔離方式也提供最慢的效能,因為在建置執行時會持續提取映像檔。另一方面,安全性較低的方法不期望任何儲存隔離,並且所有建置容器都將預設的主機儲存掛載在 /var/lib/containers 下。這種方法提供更好的效能,因為它允許重複使用來自主機儲存的快取層。
SELinux 不允許容器化行程存取主機儲存;因此,需要放寬 SELinux 安全限制,才能使用 --security-opt label=disable 選項執行以下範例。
# podman run --device /dev/fuse \
-v ./build:/build:z
-v /var/lib/containers:/var/lib/containers \
--security-opt label=disable \
-v /run/containers/0/auth.json:/auth.json \
quay.io/buildah/stable \
bash -c 'set -euo pipefail; \
buildah build -t registry.example.com/test5 /build; \
buildah push --authfile /auth.json \
registry.example.com/test5'
使用唯讀映像檔儲存實作效能與安全性的平衡
一個好的折衷方案是使用輔助的唯讀映像檔儲存來提供對快取層的存取。Buildah 支援使用多個映像檔儲存,並且 Buildah stable 映像檔內的 /etc/containers/storage.conf 檔案已經為此目的組態了 /var/lib/shared 資料夾。
# AdditionalImageStores is used to pass paths to additional
# Read/Only image stores
# Must be comma separated list.
additionalimagestores = ["/var/lib/shared",]
玄貓認為,選擇哪種方法取決於您的安全需求和效能考量。
總結,在容器中執行 Buildah 提供了許多優勢,包括更好的隔離、標準化的建置環境和簡化的建置流程。透過仔細考慮安全性與效能之間的權衡,您可以選擇最適合您需求的解決方案。
使用 Buildah 整合到現有應用程式建構流程中
```bash
```
- 將
Chapter07/*目錄中的檔案複製到新建立的~/custombuilder/目錄中。
此時,你的目錄中應該有以下檔案:
$ cd ~/custombuilder/src/builder
$ ls -latotal 148
drwxrwxr-x. 1 alex alex 74 9 nov 15.22 .
drwxrwxr-x. 1 alex alex 14 9 nov 14.10 ..
-rw-rw-r--. 1 alex alex 1466 9 nov 14.10 custombuilder.go
-rw-rw-r--. 1 alex alex 161 9 nov 15.22 go.mod
-rw-rw-r--. 1 alex alex 135471 9 nov 15.22 go.sum
-rw-rw-r--. 1 alex alex 337 9 nov 14.17 script.js
現在,我們可以執行以下指令,讓 Go 工具取得所有需要的相依性,以準備好模組的執行:
$ go mod tidy
go: finding module for package github.com/containers/storage/pkg/unshare
go: finding module for package github.com/containers/image/v5/storage
go: finding module for package github.com/containers/storage
go: finding module for package github.com/containers/image/v5/types
go: finding module for package github.com/containers/buildah/define
go: finding module for package github.com/containers/buildah
go: found github.com/containers/buildah in github.com/containers/buildah v1.23.1
go: found github.com/containers/buildah/define in github.com/containers/buildah v1.23.1
go: found github.com/containers/image/v5/storage in github.com/containers/image/v5 v5.16.1
go: found github.com/containers/image/v5/types in github.com/containers/image/v5 v5.16.1
go: found github.com/containers/storage in github.com/containers/storage v1.37.0
go: found github.com/containers/storage/pkg/unshare in github.com/containers/storage v1.37.0
這個工具會分析提供的 custombuilder.go 檔案,並找到所有需要的函式庫,然後填充 go.mod 檔案。
重要提示
請注意,先前的指令會驗證模組是否可用,如果不可用,工具會開始從網際網路下載。所以,在這個步驟請耐心等候!
我們可以檢查先前的指令是否下載了所有需要的套件,方法是檢查我們稍早建立的目錄結構:
$ cd ~/custombuilder
[custombuilder]$ ls
pkg src
[custombuilder]$ ls -la pkg/
total 0
drwxrwxr-x. 1 alex alex 28 9 nov 18.27 .
drwxrwxr-x. 1 alex alex 12 9 nov 18.18 ..
drwxrwxr-x. 1 alex alex 20 9 nov 18.27 linux_amd64
drwxrwxr-x. 1 alex alex 196 9 nov 18.27 mod
[custombuilder]$ ls -la pkg/mod/
total 0
drwxrwxr-x. 1 alex alex 196 9 nov 18.27 .
drwxrwxr-x. 1 alex alex 28 9 nov 18.27 ..
drwxrwxr-x. 1 alex alex 22 9 nov 18.18 cache
drwxrwxr-x. 1 alex alex 918 9 nov 18.27 github.com
drwxrwxr-x. 1 alex alex 24 9 nov 18.27 go.etcd.io
drwxrwxr-x. 1 alex alex 2 9 nov 18.27 golang.org
[... omitted output]
[custombuilder]$ ls -la pkg/mod/github.com/
[... omitted output]
drwxrwxr-x. 1 alex alex 98 9 nov 18.27 containerd
drwxrwxr-x. 1 alex alex 20 9 nov 18.27 containernetworking
drwxrwxr-x. 1 alex alex 184 9 nov 18.27 containers
drwxrwxr-x. 1 alex alex 110 9 nov 18.27 coreos
[... omitted output]
現在我們準備好執行我們的自訂建構器模組了,但在繼續之前,讓玄貓帶領大家看看 Go 原始檔中包含的關鍵元素。
如果我們開始檢視 custombuilder.go 檔案,就在定義套件和要使用的函式庫之後,我們定義了模組的 main 函式。
在 main 函式中,在定義的開頭,我們插入了一個基本的程式碼區塊:
if buildah.InitReexec() {
return
}
unshare.MaybeReexecUsingUserNamespace(false)
這段程式碼透過使用 github.com/containers/storage/pkg/unshare 提供的 Go unshare 套件,來啟用以非 root 使用者模式執行。
為了利用 Buildah 的建構功能,我們必須例項化 buildah.Builder。這個物件有所有的方法可以定義建構步驟、設定建構,並最終執行它。
為了建立 Builder,我們需要一個來自 github.com/containers/storage 套件的 storage.Store 物件。這個元素負責儲存中間和最終的容器映像。讓玄貓來看看我們正在討論的程式碼區塊:
buildStoreOptions, err := storage.DefaultStoreOptions(unshare.IsRootless(), unshare.GetRootlessUID())
buildStore, err := storage.GetStore(buildStoreOptions)
從先前的範例可以看到,我們正在取得預設選項,並將它們傳遞給 storage 模組,以請求一個 Store 物件。
另一個我們需要建立 Builder 的元素是 BuilderOptions 物件。這個元素包含所有預設和自訂選項,我們可以將其分配給 Buildah 的 Builder。讓玄貓來看看如何定義它:
builderOpts := buildah.BuilderOptions{
FromImage: "node:12-alpine", // Starting image
Isolation: define.IsolationChroot, // Isolation environment
CommonBuildOpts: &define.CommonBuildOptions{},
ConfigureNetwork: define.NetworkDefault,
SystemContext: &types.SystemContext {},
}
在先前的程式碼區塊中,我們定義了一個 BuilderOptions 物件,其中包含以下內容:
一個初始映像,我們將用它來建構我們的目標容器映像:
- 在這個例子中,我們選擇了根據 Alpine Linux 發行版的 Node.js 映像。這是因為在我們的範例中,我們正在模擬 Node.js 應用程式的建構過程。
一旦建構開始,要採用的隔離模式。在這個例子中,我們將使用
chroot隔離,它非常適合許多建構情境——較少的隔離,但要求也較少。一些用於建構、網路和系統環境的預設選項:
SystemContext物件將組態檔案中包含的資訊定義為引數。
現在我們有了例項化 Builder 所需的所有資料,讓我們來做吧:
builder, err := buildah.NewBuilder(context.TODO(), buildStore, builderOpts)
可以看到,我們正在呼叫 NewBuilder 函式,並使用我們稍早在本文程式碼中建立的所有必要選項,以準備好 Builder 來建立我們的自訂容器映像。
程式碼解密
buildah.InitReexec(): 檢查是否需要在不同的名稱空間中重新執行程式。這對於在容器環境中安全地執行建構過程非常重要。unshare.MaybeReexecUsingUserNamespace(false): 允許在使用者名稱空間中執行,而無需 root 許可權。storage.DefaultStoreOptions(...): 取得預設的儲存選項,這些選項會根據是否以 rootless 模式執行而有所不同。storage.GetStore(...): 建立一個儲存物件,用於儲存容器映像及其相關的中繼資料。buildah.BuilderOptions{...}: 定義建構過程的各種選項,例如基礎映像、隔離模式和網路設定。buildah.NewBuilder(...): 使用提供的選項建立一個新的 Buildah 建構器例項。
總結來說,玄貓帶領大家學習瞭如何使用 Buildah 整合到現有的應用程式建構流程中,包括設定環境、取得相依性、以及如何使用 Go 語言程式碼來定義建構過程。