Buildah 提供了更精細的容器映像檔控制,允許開發者從頭建構,或根據既有映像檔逐步新增元件與設定。本文將示範如何從一個空的容器開始,逐步安裝所需的套件、設定執行命令、提交變更,最終形成一個可執行的容器映像檔。同時,我們也會探討如何使用 Dockerfile 簡化這個流程,並介紹多階段建置的技巧,以最佳化映像檔大小,提升佈署效率。對於追求更小、更快、更安全容器映像檔的開發者而言,Buildah 的彈性與效能將是重要的工具。
從零開始建立容器:深入探索Buildah
在探討容器建立策略時,我們經常會遇到一個問題:是否有一種萬能的方法可以適用於所有情況?顯然,答案是否定的。首先,我們應該總是嘗試在容器社群中尋找預先建置的映像檔,以簡化我們的建置過程;另一方面,我們也可以從零開始建置。最後,我們可以考慮使用Dockerfile,以便於與開發團隊或更廣泛的容器社群分享我們的建置步驟。
從零開始建立映像檔
在探討本文內容之前,讓我們先進行一些測試,以驗證已安裝的Buildah是否正常運作。首先,檢查Buildah的映像檔快取是否為空:
# buildah images
REPOSITORY TAG IMAGE ID CREATED SIZE
內容解密:
此命令用於列出所有可用的映像檔。輸出結果顯示目前沒有任何映像檔。
接著,建立一個全新的容器:
# buildah from scratch
# buildah containers
CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME
af69b9547db9 * scratch working-container
內容解密:
buildah from scratch命令用於從零開始建立一個新的容器。buildah containers命令則列出所有現有的容器。在這個例子中,我們可以看到剛剛建立的容器working-container。
由於新建立的容器是空的,讓我們來驗證一下:
# buildah run working-container bash
2021-10-26T20:15:49.000397390Z: executable file 'bash' not found in $PATH: No such file or directory
內容解密:
此命令嘗試在新容器中執行bash,但由於容器內沒有安裝bash,因此失敗了。這證明瞭容器目前是空的。
現在,讓我們來填充這個空的容器。首先,掛載容器的儲存空間:
# buildah mount working-container
/var/lib/containers/storage/overlay/b5034cc80252b6f4af2155f9e0a2a7e65b77dadec7217bd2442084b1f4449c1a/merged
內容解密:
此命令掛載容器的檔案系統,並傳回掛載點的路徑。
接下來,利用主機的套件管理器安裝必要的套件:
# scratchmount=$(buildah mount working-container)
# dnf install --installroot $scratchmount --releasever 34 bash coreutils --setopt install_weak_deps=false -y
內容解密:
- 首先,將掛載點的路徑儲存在變數
scratchmount中。 - 然後,使用
dnf安裝bash和coreutils到容器的根目錄中。
安裝完成後,再次嘗試執行bash:
# buildah run working-container bash
bash-5.1# cat /etc/fedora-release
Fedora release 34 (Thirty Four)
內容解密:
現在,bash可以正常執行了,並且我們可以檢視Fedora的版本資訊。
接下來,建立一個簡單的Bash指令碼,並將其複製到容器中:
# cat command.sh
#!/bin/bash
cat /etc/fedora-release
/usr/bin/date
# chmod +x command.sh
# buildah copy working-container ./command.sh /usr/bin
內容解密:
- 建立一個名為
command.sh的指令碼,用於輸出Fedora版本和系統日期。 - 將指令碼複製到容器的
/usr/bin目錄下。
然後,設定容器的組態並提交變更:
# buildah config --cmd /usr/bin/command.sh working-container
# buildah config --created-by "podman book example" working-container
# buildah config --label name=fedora-date working-container
# buildah commit working-container fedora-date
內容解密:
- 使用
buildah config設定容器的預設命令和其他元資料。 - 使用
buildah commit將容器的變更提交為一個新的映像檔。
最後,清理環境並檢查可用的映像檔:
# buildah rm working-container
# buildah images
內容解密:
- 刪除名為
working-container的容器。 - 列出所有可用的映像檔,確認新的映像檔
fedora-date已經建立成功。
使用 Buildah 從零開始建立容器映像檔
檢視剛建立的容器映像檔詳細資訊
在建立容器映像檔後,我們可以使用 podman images 指令來檢視映像檔的詳細資訊:
# podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/fedora-date latest e24a2fafdeb5 About a minute ago 366 MB
接下來,我們可以使用 podman inspect 指令來檢視容器映像檔的詳細設定:
# podman inspect localhost/fedora-date:latest
[...省略輸出]
"Labels": {
"io.buildah.version": "1.23.1",
"name": "fedora-date"
},
"Annotations": {
"org.opencontainers.image.base.digest": "",
"org.opencontainers.image.base.name": ""
},
"ManifestType": "application/vnd.oci.image.manifest.v1+json",
"User": "",
"History": [
{
"created": "2021-10-26T21:16:48.777712056Z",
"created_by": "podman book example"
}
],
"NamesHistory": [
"localhost/fedora-date:latest"
]
內容解密:
此輸出結果顯示了容器映像檔的後設資料,包括標籤、註解、歷史記錄等。這些資訊可以幫助我們瞭解容器映像檔的建立過程和設定。
執行新建立的容器映像檔
最後,我們可以使用 podman run 指令來執行新建立的容器映像檔:
# podman run -ti localhost/fedora-date:latest
Fedora release 34 (Thirty Four)
Tue Oct 26 21:18:29 UTC 2021
這證明瞭我們的容器映像檔已經成功建立並執行。
從 Dockerfile 建立映像檔
接下來,我們將介紹如何使用 Buildah 從 Dockerfile 建立映像檔。首先,我們需要建立一個新的目錄,並在其中建立一個名為 Dockerfile 的檔案:
# mkdir webserver
# cd webserver/
[webserver]# vi Dockerfile
[webserver]# cat Dockerfile
# 從最新的 Fedora 容器基礎映像檔開始
FROM fedora:latest
MAINTAINER podman-book # 這應該是一個電子郵件地址
# 更新容器基礎映像檔
RUN echo "Updating all fedora packages"; dnf -y update; dnf -y clean all
# 安裝 httpd 套件
RUN echo "Installing httpd"; dnf -y install httpd
# 暴露 HTTP 連線埠 80
EXPOSE 80
# 設定預設的命令以在容器啟動時執行
CMD ["/usr/sbin/httpd", "-DFOREGROUND"]
內容解密:
此 Dockerfile 定義了建立一個容器化的 Web 伺服器的步驟,包括更新套件、安裝 httpd 套件、暴露連線埠 80 和設定預設命令。
使用 Buildah 建立映像檔
現在,我們可以使用 buildah build 指令來建立映像檔:
[webserver]# buildah build -f Dockerfile -t myhttpdservice .
STEP 1/6: FROM fedora:latest
Resolved "fedora" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull registry.fedoraproject.org/fedora:latest...
Getting image source signatures
Copying blob 944c4b241113 done
Copying config 191682d672 done
Writing manifest to image destination
Storing signatures
STEP 2/6: MAINTAINER podman-book # 這應該是一個電子郵件地址
STEP 3/6: RUN echo "Updating all fedora packages"; dnf -y update; dnf -y clean all
Updating all fedora packages
Fedora 34 - x86_64 16 MB/s | 74 MB 00:04
...
STEP 4/6: RUN echo "Installing httpd"; dnf -y install httpd
Installing httpd
Fedora 34 - x86_64 20 MB/s | 74 MB 00:03
...
STEP 5/6: EXPOSE 80
STEP 6/6: CMD ["/usr/sbin/httpd", "-DFOREGROUND"]
COMMIT myhttpdservice
Getting image source signatures
Copying blob 7500ce202ad6 skipped: already exists
Copying blob 51b52d291273 done
Copying config 14a2226710 done
Writing manifest to image destination
Storing signatures
--> 14a2226710e
Successfully tagged localhost/myhttpdservice:latest
14a2226710e7e18d2e4b6478e09a9f55e60e0666dd8243322402ecf6fd1eaa0d
內容解密:
此輸出結果顯示了 Buildah 建立映像檔的過程,包括下載基礎映像檔、執行 Dockerfile 中的指令和建立新的映像檔。
使用 Buildah 從零開始建立容器
在我們剛剛執行的命令中,可以看到輸出結果顯示所有在 Dockerfile 中定義的步驟都按照確切的順序被執行,並且印出了一個分數來表示相對於總步驟的中間步驟。總共執行了六個步驟。
我們可以透過執行 buildah images 命令來檢查命令的結果:
[webserver]# buildah images
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/myhttpdservice latest 14a2226710e7 2 minutes ago 497 MB
如我們所見,我們的容器映像已經被建立,並且標籤為 latest。讓我們試著執行它:
# podman run -d localhost/myhttpdservice:latest
133584ab526faaf7af958da590e14dd533256b60c10f08acba6c1209ca05a885
# podman logs 133584ab526faaf7af958da590e14dd533256b60c10f08acba6c1209ca05a885
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.88.0.4. Set the 'ServerName' directive globally to suppress this message
# curl 10.88.0.4
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Test Page for the HTTP Server on Fedora</title>
<style type="text/css">
...
內容解密:
buildah images命令用於列出本機上所有可用的映像。podman run命令用於啟動一個新的容器,並在背景執行。podman logs命令用於檢視容器的日誌輸出,幫助我們瞭解容器的 IP 地址。curl命令用於測試 HTTP 伺服器是否正常運作。
從輸出結果來看,我們剛剛在分離模式下執行了容器;之後,我們檢查了日誌以找出需要傳遞給 curl 測試命令的 IP 地址。
探索 buildah build 命令的其他選項
正如我們所預料的,buildah build 命令有很多其他有用的選項,尤其是在開發和建立全新的容器映像時。其中一個值得一提的選項是 --layers。
從 Buildah 的 1.2 版本開始,開發團隊新增了這個選項,讓我們能夠啟用或停用層的快取機制。預設組態將 --layers 選項設定為 false,這意味著 Buildah 不會保留中間層,從而導致構建結果將所有變更壓縮到單一層中。
也可以透過環境變數來設定層的管理,例如,執行 export BUILDAH_LAYERS=true 以啟用層快取。
將 Buildah 與現有的應用程式構建流程整合
在學習瞭如何使用 Podman 和 Buildah 建立自定義容器映像之後,我們現在可以專注於一些特殊的使用案例,這些使用案例可以使我們的構建工作流程更加高效和便攜。例如,在企業環境中,由於效能和安全性的原因,小映像是非常常見的需求。我們將探討如何透過將構建過程分解為不同的階段來實作這一目標。
多階段容器構建
到目前為止,我們已經學習瞭如何使用 Podman 和 Buildah 從 Dockerfile 或原生 Buildah 命令建立構建。然而,還有一個重要的點我們尚未討論——映像的大小。當建立新的映像時,我們應該始終關注其最終大小,這是所有層的總數和其中變更檔案數量的結果。
最小化映像大小的最佳實踐
最小化映像大小具有很大的優勢,例如可以更快地從登入檔中提取映像。然而,大型映像會佔用主機本地儲存中的大量寶貴磁碟空間。
我們已經展示了一些保持映像緊湊的最佳實踐,例如從零開始構建、清理套件管理器快取以及將 RUN、COPY 和 ADD 指令的數量減少到必要的最小值。然而,當我們需要從原始碼構建應用程式並建立包含最終產物的最終映像時,會發生什麼?
例如,如果我們需要構建一個容器化的 Go 應用程式,我們應該從包含 Go 執行時的基礎映像開始,複製原始碼,並編譯以產生最終的二進位制檔案,同時伴隨著一系列中間步驟,最顯著的是在映像快取中下載所有必要的 Go 套件。在構建結束時,我們應該清理所有原始碼和下載的依賴項,並將最終的二進位制檔案(在 Go 中是靜態連結的)放在工作目錄中。一切都會正常運作,但最終映像仍將包含在編譯過程結束後不再需要的 Go 執行時。
多階段構建流程
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Buildah容器映像檔建立
package "Docker 架構" {
actor "開發者" as dev
package "Docker Engine" {
component [Docker Daemon] as daemon
component [Docker CLI] as cli
component [REST API] as api
}
package "容器運行時" {
component [containerd] as containerd
component [runc] as runc
}
package "儲存" {
database [Images] as images
database [Volumes] as volumes
database [Networks] as networks
}
cloud "Registry" as registry
}
dev --> cli : 命令操作
cli --> api : API 呼叫
api --> daemon : 處理請求
daemon --> containerd : 容器管理
containerd --> runc : 執行容器
daemon --> images : 映像檔管理
daemon --> registry : 拉取/推送
daemon --> volumes : 資料持久化
daemon --> networks : 網路配置
@enduml
此圖示展示了一個多階段構建流程,從選擇基礎映像到產生最終的容器映像。
內容解密:
- 多階段構建允許我們在不同的階段中使用不同的基礎映像和指令,最終產生一個精簡的映像。
- 在第一階段,我們可以使用包含編譯器的映像來編譯原始碼。
- 在第二階段,我們可以使用一個精簡的基礎映像,並將編譯好的二進位制檔案複製到其中。
- 這種方法可以顯著減小最終映像的大小。
多階段容器建置(Multistage Container Builds)203
當 Docker 被引入且 Dockerfile 開始流行時,DevOps 團隊為了保持映像檔的最小化,採用了不同的方法來規避這個問題。例如,二進位建置是一種將外部編譯的最終產物注入建置映像檔的方法。這種方法解決了映像檔大小的問題,但失去了執行時/編譯器映像檔提供的標準化建置環境的優勢。
更好的方法是讓容器之間分享磁碟區,並讓最終的容器映像檔從第一個建置映像檔中取得編譯的產物。為了提供標準化的方法,Docker 和 OCI 規範引入了多階段建置的概念。多階段建置允許多個階段使用不同的 FROM 指令,並且後續的映像檔可以從前面的映像檔中取得內容。
在接下來的子章節中,我們將探討如何使用 Dockerfile/Containerfile 和 Buildah 的原生命令來實作這一結果。
使用 Dockerfile 進行多階段建置
第一種實作多階段建置的方法是在單一的 Dockerfile/Containerfile 中建立多個階段,每個區塊以 FROM 指令開始。建置階段可以使用 --from 選項從前面的階段複製檔案和資料夾,以指定來源階段。
程式碼範例:
# 建置映像檔
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 /
EXPOSE 8080
CMD ["/hello-world"]
內容解密:
- 多階段建置的第一階段:使用官方的
golang映像檔作為建置環境,將go.mod和main.go複製到容器中,下載依賴並編譯應用程式。 - 第二階段:使用
ubi-micro映像檔,這是一個精簡的 Red Hat 映像檔,將第一階段編譯好的應用程式複製到新的映像檔中。 COPY --from=builder指令:從名為builder的第一階段複製編譯好的應用程式到第二階段的映像檔中。EXPOSE和CMD指令:暴露 8080 連線埠並設定預設命令以執行應用程式。
建置與執行
我們可以使用 Buildah 建置這個應用程式:
$ cd http_hello_world
$ buildah build -t hello-world .
檢查產生的映像檔大小:
$ buildah images --format '{{.Name}} {{.Size}}' \
localhost/hello-world
localhost/hello-world 45 MB
最終的映像檔大小僅為 45 MB!