返回文章列表

Buildah 精通:開發高效容器映像檔的實戰

本文探討 Buildah 的使用方法,從 Dockerfile 建構到多階段建構,並結合原生指令與語意化版本控制,展現 Buildah 在開發精簡、高效容器映像檔的實戰技巧。搭配 Podman 協作,更能提升容器化工作流程的效率和彈性。

容器技術 DevOps

在容器化應用程式佈署的過程中,建構最佳化的映像檔至關重要。Buildah 作為一個強大的容器映像建構工具,不僅能與 Podman 無縫協作,更提供了原生指令和分層機制等特性,讓開發者能更精細地控制映像檔的建構流程。本文將探討如何使用 BuildahDockerfile 建構映像,以及如何運用多階段建構策略搭配原生指令,開發精簡高效的容器映像,並進一步整合語意化版本控制,提升整體工作流程的效率。Buildah 允許開發者以更彈性的方式管理映像層,透過 --layers 選項,可以根據需求啟用或停用層快取機制,在重建映像時有效節省計算資源。然而,啟用層快取的同時也需考量儲存空間的佔用,因此需根據實際情況進行調整。

在企業環境中,小型映像檔因其效能和安全性優勢而備受青睞。透過多階段建構,我們可以在不同階段使用不同的基礎映像檔,例如在建構階段使用包含完整開發工具的映像檔,而在執行階段使用精簡的執行環境映像檔,從而有效縮減最終映像檔的大小。Buildah 的原生指令則提供了更精細的控制,例如可以掛載建構容器的 overlay 檔案系統,並提取已建置的成品,以便在建構最終執行階段映像檔之前,發布替代套件。這對於需要在建構過程中執行一些複雜操作的場景非常有用。

利用 Buildah 從零開始開發容器映像

在容器化的世界中,客製化、更新和管理容器基礎架構至關重要。Buildah 是一個強大的工具,能與 Podman 完美協作,簡化容器映像的建構流程。本文將探討如何使用 Buildah,包括從 Dockerfile 建構映像,以及其他進階技巧。

從 Dockerfile 建構映像:簡潔高效

Buildah 能夠直接使用 Dockerfile 建構映像,這對於熟悉 Docker 生態系統的開發者來說非常方便。以下是一個範例:

buildah bud -t localhost/myhttpdservice .

這個指令會讀取目前目錄下的 Dockerfile,並根據其中的指令建構一個名為 localhost/myhttpdservice 的映像。

觀察輸出結果,Dockerfile 中定義的所有步驟都會按照順序執行,並顯示進度。例如:

STEP 1/6: FROM fedora:latest
STEP 2/6: RUN dnf -y update
STEP 3/6: RUN dnf -y install httpd
STEP 4/6: COPY index.html /var/www/html/
STEP 5/6: CMD ["/usr/sbin/httpd", "-D", "FOREGROUND"]
STEP 6/6: COMMIT

建構完成後,可以使用 buildah images 指令來確認映像是否成功建立:

buildah images

輸出結果應類別似如下:

REPOSITORY                 TAG      IMAGE ID       CREATED         SIZE
localhost/myhttpdservice   latest   14a2226710e7   2 minutes ago   497 MB

接著,可以使用 Podman 執行此映像:

podman run -d localhost/myhttpdservice:latest

透過以下指令檢視容器日誌:

podman logs 133584ab526faaf7af958da590e14dd533256b60c10f08acba6c1209ca05a885

如果想測試網頁伺服器,可以使用 curl 指令:

curl 10.88.0.4

此時,應該會看到網頁伺服器的預設頁面。

深入理解 Buildah 的分層機制

Buildah 的一個重要特性是其分層機制。預設情況下,Buildah 不會保留中間層,而是將所有變更壓縮到單一層中。但從 1.2 版本開始,Buildah 引入了 --layers 選項,允許啟用或停用層的快取機制。

啟用層快取可以使用環境變數:

export BUILDAH_LAYERS=true

啟用層快取的優點是可以節省重建映像時的計算資源,只需變更最新的層即可。缺點是保留的層會佔用系統儲存空間。

整合至現有應用程式建置流程

在企業環境中,小型映像由於其效能和安全性優勢而備受青睞。透過將建置流程分解為不同階段,可以實作這一目標。

本章將探討以下主題:

  • 多階段容器建置
  • 在容器內執行 Buildah
  • 將 Buildah 與自訂建置器整合

多階段容器建置:開發精簡映像

在建構新映像時,應始終關注其最終大小。最小的映像能夠更快地從 registry 中提取,並減少主機上的磁碟空間佔用。

雖然可以透過從 scratch 建構、清理套件管理器快取以及減少 RUNCOPYADD 指令的數量來縮減映像大小,但在需要從原始碼建置應用程式時,情況會變得更複雜。

假設需要建置一個容器化的 Go 應用程式。首先,需要一個包含 Go 執行階段的基礎映像,然後複製原始碼並編譯以產生最終二進位檔案。在這個過程中,會下載所有必要的 Go 套件到映像快取中。建置完成後,需要清理所有原始碼和下載的依賴項,並將最終二進位檔案放置在工作目錄中。

雖然這樣做可以正常工作,但最終映像仍然包含基礎映像中的 Go 執行階段,而這些執行階段在編譯過程結束後已不再需要。

為何我擁抱多階段建構:容器瘦身與效率提升之旅

在容器技術的世界裡,Dockerfile就像一份藍圖,指引著我們如何開發應用程式的家。但隨著應用程式日漸複雜,傳統的Dockerfile往往會產生臃腫的容器映像檔,這不僅佔用大量的儲存空間,也拖慢了佈署速度。為了擺脫這種困境,我開始探索多階段建構的奧秘。

多階段建構,顧名思義,就是將建構過程拆解為多個階段,每個階段都使用不同的基礎映像檔。這樣一來,我們就可以在一個階段中使用包含編譯器、函式庫等完整開發工具的映像檔來建構應用程式,而在另一個階段中使用精簡的執行時映像檔來執行應用程式。最終,我們只需將建構好的執行檔複製到執行時映像檔中,就能得到一個體積小巧、功能完整的容器映像檔。

Dockerfile多階段建構實戰:Go應用程式瘦身記

讓我們先來看一個使用Dockerfile實作多階段建構的例子。假設我們有一個使用 Go 語言開發的簡單 Web 應用程式,它的作用是監聽 8080 埠,並在收到 GET 請求時傳回 “Hello World!"。

package main

import (
	"log"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
	w.Header().Set("Content-Type", "text/html")
	w.Write([]byte("<html>\n<body>\n"))
	w.Write([]byte("<p>Hello World!</p>\n"))
	w.Write([]byte("</body>\n</html>\n"))
}

func main() {
	http.HandleFunc("/", handler)
	log.Println("Starting http server")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

為了建構這個應用程式的容器映像檔,我們可以編寫如下的Dockerfile

# 第一階段:建構階段
FROM docker.io/library/golang AS builder

# 複製原始碼
COPY go.mod /go/src/hello-world/
COPY main.go /go/src/hello-world/

# 設定工作目錄
WORKDIR /go/src/hello-world

# 下載依賴
RUN go get -d -v ./...

# 建構應用程式
RUN go build -v ./...

# 第二階段:執行階段
FROM registry.access.redhat.com/ubi8/ubi-micro:latest AS srv

# 從建構階段複製執行檔
COPY --from=builder /go/src/hello-world/hello-world /

# 暴露 8080 埠
EXPOSE 8080

# 啟動應用程式
CMD ["/hello-world"]

這個Dockerfile定義了兩個階段:

  1. 建構階段(builder):使用docker.io/library/golang映像檔作為基礎,複製原始碼、下載依賴並建構應用程式。
  2. 執行階段(srv):使用registry.access.redhat.com/ubi8/ubi-micro:latest映像檔作為基礎,從建構階段複製建構好的執行檔,並設定埠和啟動命令。

透過這樣的多階段建構,最終產生的容器映像檔只包含執行應用程式所需的最小依賴,體積大幅縮小。

Buildah 的優勢:靈活的容器建構方式

雖然Dockerfile是容器建構的標準方式,但在某些特殊情況下,它可能無法滿足我們的需求。例如,當我們需要更精細地控制建構流程、或者需要在建構過程中執行一些複雜的操作時,Buildah就派上了用場。

Buildah是一個用於建構容器映像檔的工具,它提供了一組底層命令,讓我們可以像拼積木一樣,一步一步地建構容器映像檔。與Dockerfile相比,Buildah更加靈活,可以讓我們更好地控制建構過程。

運用 Buildah 原生指令實作多階段容器建構

多階段建構是建立精簡映像檔和縮小攻擊面的絕佳方法。為了在建構過程中提供更大的彈性,我們可以仰賴 Buildah 的原生指令。正如我們在第六章「從頭開始建構容器」中提到的,Buildah 提供了一系列指令,可以模擬 Dockerfile 指令的行為,從而在指令碼或自動化流程中提供對建構過程的更佳控制。

這個概念同樣適用於多階段建構,我們可以在各階段之間加入額外的步驟。例如,我們可以掛載建構容器的 overlay 檔案系統,並提取已建置的成品,以便在建構最終執行階段映像檔之前,發布替代套件。

以下範例透過將先前的 Dockerfile 指令轉換為原生 Buildah 指令,在一個簡單的 Shell 指令碼中建構相同的 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. BUILDERRUNTIME 變數:定義了用於建構和執行應用程式的基礎映像檔。BUILDER 使用 golang 映像檔,RUNTIME 使用 ubi8/ubi-micro 映像檔。
  2. buildah from:使用指定的基礎映像檔建立新的容器。container1 用於建構,container2 用於執行。
  3. buildah copy:將 go.modmain.go 檔案從主機複製到建構容器的 /go/src/hello-world/ 目錄中。
  4. buildah config:組態建構容器的工作目錄為 /go/src/hello-world
  5. buildah run:在建構容器中執行 go getgo build 命令,下載依賴並編譯應用程式。
  6. buildah copy --from:將編譯後的 hello-world 執行檔從建構容器複製到執行容器的根目錄 /
  7. buildah config:組態執行容器的埠、命令和使用者。
  8. buildah commit:將執行容器提交為名為 hello-world 的映像檔。
  9. buildah rm:刪除建構和執行容器。

在上面的範例中,我們突顯了兩個工作容器的建立指令,以及儲存容器 ID 的相關 container1container2 變數。

此外,請注意 buildah copy 指令,我們在其中使用 --from 選項定義了來源容器,並使用 --chown 選項定義了複製資源的使用者和群組擁有者。這種方法比根據 Dockerfile 的工作流程更具彈性,因為我們可以利用變數、條件和迴圈來豐富我們的指令碼。

例如,我們在 Bash 指令碼中使用 if 條件來檢查 go.modmain.go 檔案是否存在,然後再將它們複製到專用於建構的工作容器中。

整合語意化版本控制到 Buildah 指令碼

讓我們先為指令碼增加一個額外的功能。在以下範例中,我們透過為建構增加語意化版本控制,並在開始建構最終執行階段映像檔之前建立版本封存,從而改進了先前的範例:

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

# 建立建構器容器
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 ./...

# 提取建構成品並建立版本封存
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

# 建立執行階段容器
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:$RELEASE

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

程式碼解密

  1. RELEASE 變數:定義了應用程式的版本號,這裡設定為 “1.0.0”。
  2. buildah unshare --mount
    • buildah unshare 建立一個新的名稱空間,允許在沒有 root 許可權的情況下執行某些操作。
    • --mount mnt=$container1 將建構容器 container1 掛載到 /mnt 目錄。
    • sh -c 'cp $mnt/go/src/hello-world/hello-world .' 在新的名稱空間中執行 shell 命令,將建構容器中的 hello-world 執行檔複製到當前目錄。
  3. cat > README << EOF ... EOF:建立一個包含版本資訊的 README 檔案。
  4. tar zcf hello-world-${RELEASE}.tar.gz hello-world README:將 hello-world 執行檔和 README 檔案封裝成一個 tar 壓縮檔,檔名包含版本號。
  5. rm -f hello-world README:刪除原始的 hello-world 執行檔和 README 檔案。
  6. buildah commit $container2 hello-world:$RELEASE:提交最終映像檔,並使用版本號作為標籤。

透過這個修改後的指令碼,我們不僅可以建構應用程式,還可以建立包含版本資訊的封存檔,並為最終映像檔增加標籤,從而更有效地管理和追蹤不同的版本。

總結來說,使用 Buildah 原生指令進行多階段建構,為我們提供了更大的彈性和控制力。透過在指令碼中加入條件判斷、迴圈和版本控制等功能,我們可以建立更複雜、更精確的建構流程,從而更好地管理容器映像檔的建構和發布。