多階段建置技術有效減小容器映像檔體積並提升安全性,允許在不同階段使用不同基礎映像檔,例如在第一階段使用包含完整建置工具的映像檔編譯應用程式,然後在第二階段使用精簡的執行環境映像檔執行應用程式。本文將探討如何使用 Buildah 原生命令實作更靈活的多階段容器建置,並提供相關最佳實踐。相較於 Dockerfile,Buildah 提供更細緻的控制,能以指令碼語言(如 Bash)控制建置流程,並加入條件判斷、迴圈等邏輯。文章也涵蓋瞭如何結合語意化版本控制,以及在容器中執行 Buildah 的方法與安全性考量,例如使用適當的 SELinux 標籤和身份驗證檔案,並比較了多種儲存策略,提供程式碼範例。

自訂名稱與靈活性

我們可以透過使用 AS 關鍵字為基礎映像檔新增自訂名稱,以提高 Dockerfile 的可讀性和靈活性。在上述範例中,我們將第一階段命名為 builder,第二階段命名為 srv。這樣,在 COPY 指令中就可以使用 --from=builder 來指定來源階段。

Dockerfile/Containerfile 的限制與 Buildah 的優勢

雖然 Dockerfile/Containerfile 建置是最常見的方法,但在實作自訂建置工作流程時仍然缺乏一些靈活性。對於這些特殊的使用案例,Buildah 的原生命令可以提供幫助。

多階段容器建置:使用 Buildah 原生命令提升靈活性

在容器化的世界中,多階段建置(Multistage Builds)是一種強大的技術,能夠幫助我們建立出更小、更安全的容器映像檔。透過 Buildah 的原生命令,我們可以進一步提升建置流程的靈活性與控制力。本文將探討如何使用 Buildah 實作多階段容器建置,並介紹相關的最佳實踐。

為何選擇多階段建置?

多階段建置允許我們在不同的階段使用不同的基礎映像檔,從而最佳化最終映像檔的大小和安全性。例如,我們可以在第一階段使用包含完整建置工具的映像檔來編譯應用程式,然後在第二階段使用精簡的執行環境映像檔來執行應用程式。

使用 Buildah 原生命令進行多階段建置

相較於 Dockerfile,Buildah 的原生命令提供了更大的靈活性。我們可以透過指令碼語言(如 Bash)來控制建置流程,加入條件判斷、迴圈等邏輯,使建置過程更加動態和可控。

以下是一個使用 Buildah 建置 hello-world Go 應用程式的例子:

#!/bin/bash

# 定義建置和執行階段的映像檔
BUILDER=docker.io/library/golang
RUNTIME=registry.access.redhat.com/ubi8/ubi-micro:latest

# 建立建置容器
container1=$(buildah from $BUILDER)

# 從主機複製檔案到建置容器
if [ -f go.mod ]; then
    buildah copy $container1 'go.mod' '/go/src/hello-world/'
else
    exit 1
fi

if [ -f main.go ]; then
    buildah copy $container1 'main.go' '/go/src/hello-world/'
else
    exit 1
fi

# 組態並開始建置
buildah config --workingdir /go/src/hello-world $container1
buildah run $container1 go get -d -v ./...
buildah run $container1 go build -v ./...

# 建立執行容器
container2=$(buildah from $RUNTIME)

# 從建置容器複製檔案到執行容器
buildah copy --chown=1001:1001 --from=$container1 $container2 '/go/src/hello-world/hello-world' '/'

# 組態暴露的埠號
buildah config --port 8080 $container2

# 組態預設的 CMD
buildah config --cmd /hello-world $container2

# 組態預設的使用者
buildah config --user=1001 $container2

# 提交最終的映像檔
buildah commit $container2 hello-world

# 移除建置容器
buildah rm $container1 $container2

內容解密:

  1. buildah from 命令:用於從指定的映像檔建立一個新的容器。在這個例子中,我們分別為建置和執行階段建立了容器。
  2. buildah copy 命令:用於在容器之間或從主機到容器之間複製檔案。我們使用 --from 選項指定來源容器,並使用 --chown 選項設定複製檔案的所有者。
  3. buildah config 命令:用於組態容器的屬性,如工作目錄、暴露的埠號、預設的 CMD 和使用者等。
  4. buildah run 命令:用於在容器中執行命令。在這個例子中,我們使用它來執行 Go 的建置命令。
  5. buildah commit 命令:用於將容器的狀態提交為一個新的映像檔。

加入語意化版本控制

在軟體開發中,語意化版本控制(Semantic Versioning)是一種管理軟體版本的標準方法。透過在建置過程中加入語意化版本控制,我們可以更好地管理軟體版本和依賴關係。

以下是一個加入了語意化版本控制的例子:

#!/bin/bash

# 定義建置和執行階段的映像檔
BUILDER=docker.io/library/golang
RUNTIME=registry.access.redhat.com/ubi8/ubi-micro:latest
RELEASE=1.0.0

# ... (省略部分與前例相同的程式碼)

# 提取建置產物並建立版本壓縮檔
buildah unshare --mount mnt=$container1 sh -c 'cp $mnt/go/src/hello-world/hello-world .'
cat > README << EOF
Version $RELEASE release notes:
- Implement basic features
EOF
tar zcf hello-world-${RELEASE}.tar.gz hello-world README
rm -f hello-world README

# ... (省略部分與前例相同的程式碼)

# 提交最終的映像檔,並加上版本標籤
buildah commit $container2 hello-world:$RELEASE

# 移除建置容器
buildah rm $container1 $container2

內容解密:

  1. buildah unshare 命令:用於在一個新的使用者名稱空間中執行命令,這裡用於掛載建置容器的檔案系統並提取建置產物。
  2. 建立版本壓縮檔:我們建立了一個包含版本資訊的 README 檔案,並將其與建置產物一起封裝成一個壓縮檔。
  3. 加上版本標籤:在提交最終映像檔時,我們加上了語意化版本標籤,以便於管理和追蹤不同版本的映像檔。

在容器中執行 Buildah

Podman 和 Buildah 採用 fork/exec 方式,使得它們非常容易在容器內執行,包括無根容器場景。

為何需要在容器中執行 Buildah

許多使用案例意味著需要容器化的建置。目前最常見的採用場景之一是應用程式建置工作流程,執行在 Kubernetes 叢集之上。

Kubernetes 基本上是一個容器協調器,它從控制平面管理容器的排程,控制平面執行在一組工作節點上,這些節點執行與容器執行時介面(CRI)相容的容器引擎。其設計允許在自定義網路、儲存和執行時方面具有很大的靈活性,並導致了現在在雲原生計算基金會(CNCF)中孵化或成熟的許多輔助專案。

原生 Kubernetes(即基本社群版本,沒有任何自定義或附加元件)沒有任何原生的建置功能,但提供了實作它的適當框架。隨著時間的推移,出現了許多解決方案試圖解決這一需求。

例如,Red Hat OpenShift 在 Kubernetes 1.0 發布時引入了自己的建置 API 和 Source-to-Image 工具包,用於直接從原始碼建立容器映像。

另一個有趣的解決方案是 Google 的 kaniko,它是一個建置工具,用於在 Kubernetes 叢集內建立容器映像,每個建置步驟都在使用者空間中執行。

使用 Buildah 實作容器化建置

除了使用已經實作的解決方案外,我們還可以設計自己的執行 Buildah 的容器,這些容器由 Kubernetes 協調。我們也可以利用無根設計來實作安全的建置工作流程。

可以在 Kubernetes 叢集上執行 CI/CD 管道,並在管道中嵌入容器化建置。CNCF 中最有趣的專案之一,Tekton Pipelines,提供了一種雲原生的方式來實作這一目標。Tekton 允許執行由 Kubernetes 自定義資源驅動的管道,這些資源是擴充套件基本 API 集的特殊 API。

Tekton Pipelines 由許多不同的任務組成,使用者可以建立自己的任務,也可以從 Tekton Hub(https://hub.tekton.dev/)取得,Tekton Hub 是一個免費的倉函式庫,提供了許多現成的任務,包括來自 Buildah 的範例(https://hub.tekton.dev/tekton/task/buildah)。

使用 volume store 執行無根 Buildah 容器

在本小節的範例中,將使用穩定的上游 quay.io/buildah/stable Buildah 映像。該映像已經嵌入了最新的穩定 Buildah 二進位檔案。

首先,讓我們執行一個無根容器,該容器建置主機上 ~/build 目錄中的內容,並將輸出儲存在名為 storevol 的本機卷中:

$ 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 選項,它將 /root/build 目錄繫結掛載到容器內,並使用 :z 字尾分配適當的 SELinux 標籤。
  • -v storevol:/var/lib/containers 選項,它建立一個新的卷,並將其掛載到預設的容器儲存區,所有層都在此建立。

內容解密:

  1. --device /dev/fuse:載入 fuse 核心模組,以便在容器內執行 fuse-overlay 命令。
  2. -v ~/build:/build:z:將主機上的 ~/build 目錄掛載到容器內的 /build 目錄,並設定 SELinux 標籤。
  3. -v storevol:/var/lib/containers:將名為 storevol 的卷掛載到容器內的 /var/lib/containers 目錄,用於儲存建置好的映像層。

建置完成後,我們可以執行一個新的容器,使用相同的卷來檢查或操作已建置的映像:

$ podman run --rm -v storevol:/var/lib/containers quay.io/buildah/stable buildah images

輸出結果如下:

REPOSITORY           TAG     IMAGE ID      CREATED        SIZE
localhost/build_test1 latest  cd36bf58daff 12 minutes ago 283 MB
docker.io/library/fedora latest  b080de8a4da3 4 days ago    159 MB

內容解密:

  1. podman run --rm -v storevol:/var/lib/containers quay.io/buildah/stable buildah images:執行一個新的容器,使用 storevol 卷,並執行 buildah images 命令列出已建置的映像。

我們已經成功地建置了一個映像,其層儲存在 storevol 卷中。要遞迴列出儲存區的內容,我們可以使用 podman volume inspect 命令提取卷掛載點:

$ ls -alR $(podman volume inspect storevol --format '{{.Mountpoint}}')

現在,我們可以啟動一個新的 Buildah 容器,對遠端登入進行身份驗證,並標記和推播映像。在下一個範例中,Buildah 標記結果映像,對遠端登入進行身份驗證,最後推播映像:

$ podman run --rm -v storevol:/var/lib/containers \
quay.io/buildah/stable \
sh -c 'buildah tag build_test1 \
registry.example.com/build_test1 \
&& buildah login -u=<USERNAME> -p=<PASSWORD> \
registry.example.com && \
buildah push registry.example.com/build_test1'

內容解密:

  1. buildah tag build_test1 registry.example.com/build_test1:將 build_test1 映像標記為 registry.example.com/build_test1
  2. buildah login -u=<USERNAME> -p=<PASSWORD> registry.example.com:對遠端登入進行身份驗證。
  3. buildah push registry.example.com/build_test1:將標記好的映像推播到遠端登入。

當映像成功推播後,最後可以安全地刪除卷:

# podman volume rm storevol

儘管這種方法工作得很好,但它有一些值得討論的限制。首先,我們可以注意到儲存卷沒有被隔離,因此其他容器可以存取其內容。為了克服這個問題,我們可以使用 SELinux 的多類別安全(MCS),並在捲上使用 :Z 字尾,以便為卷套用類別,使其只能被執行的容器存取。

在容器中執行 Buildah 的整合與安全性考量

在現代化的 DevOps 流程中,將 Buildah 整合到現有的應用程式構建過程中是一項重要的任務。Buildah 是一個用於構建 OCI 相容容器映象的工具,它能夠在不依賴 Docker 守護程式的情況下構建映象。本文將探討如何在容器中執行 Buildah,以及相關的安全性考量。

在容器中執行 Buildah 的基本方法

要在容器中執行 Buildah,首先需要確保容器具有適當的組態和許可權。預設情況下,容器會以不同的類別標籤執行,因此需要透過 --security-opt 選項來設定正確的標籤,以確保容器能夠存取所需的資源。

另一種方法是將構建、標記和推播命令放在一個單一的容器中執行,如下例所示:

$ 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'

內容解密:

  1. podman run:啟動一個新的容器。
  2. --device /dev/fuse:允許容器使用 FUSE(使用者空間檔案系統)。
  3. -v ~/build:/build:將主機上的 ~/build 目錄掛載到容器的 /build 目錄。
  4. -v secure_storevol:/var/lib/containers:Z:將一個名為 secure_storevol 的卷掛載到容器的 /var/lib/containers 目錄,並設定 SELinux 標籤。
  5. quay.io/buildah/stable:使用的 Buildah 映象。
  6. sh -c '...': 在容器中執行指定的命令序列,包括構建、標記、登入和推播映象。

安全性考量

直接在命令列中傳遞使用者名稱和密碼進行登入是一種不安全的做法。更好的方法是將包含有效會話令牌的身份驗證檔案掛載到容器內部,並使用 --authfile 選項指定該檔案。

$ 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'

內容解密:

  1. -v /run/user/<UID>/containers/auth.json:/auth.json:z:將主機上的身份驗證檔案掛載到容器的 /auth.json
  2. --authfile /auth.json:指定 Buildah 使用掛載的身份驗證檔案進行推播操作。

使用繫結掛載儲存的 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'

內容解密:

  1. BUILD_STORE:建立一個臨時目錄來儲存構建層。
  2. -v $BUILD_STORE:/var/lib/containers:Z:將臨時目錄掛載到容器的 /var/lib/containers,並設定 SELinux 標籤以實作隔離。

多種儲存策略的比較

  • 高度隔離:每個構建容器都有自己的儲存,提供了最好的安全性,但由於每次都需要重新提取基礎映象層,因此效能較差。
  • 較低隔離:所有構建容器分享主機的預設儲存,提供了較好的效能,但安全性較低,需要透過 --security-opt label=disable 來放寬 SELinux 的限制。
  • 折衷方案:使用次要的只讀映象儲存來提供對快取層的存取,從而在安全性和效能之間取得平衡。