Kubernetes 是通用的編排引擎,不只適合執行應用,也適合建置容器映像檔。「映像檔建置器」模式說明為什麼在叢集內建置映像檔是合理的,以及目前有哪些技術能在 Kubernetes 內建立映像檔。
問題#
本書至今的所有模式都在談「如何在 Kubernetes 上操作應用」。但建置應用本身呢?經典做法是在叢集外建置容器映像檔、推送到 registry,再於 Kubernetes 部署描述檔中引用它們。然而在叢集內建置有數項優勢:
- 只用一個叢集處理所有事(若公司政策允許)。在同一處建置與執行應用能大幅降低維護成本,也簡化容量規劃、減少平台資源開銷。
- 排程效率。 通常我們用 Jenkins 這類 CI 系統建置映像檔,而用 CI 系統建置本質上是一個排程問題:如何有效率地為建置工作找到閒置的運算資源。Kubernetes 的核心正是一個高度精巧的排程器,完美契合這類挑戰。
- 打通 CI 到 CD。 當我們邁向持續交付、從建置映像檔過渡到執行容器時,若建置發生在同一叢集內,兩個階段共享同一套基礎設施,過渡更順暢。
舉個具體例子:假設在所有應用共用的基礎映像檔中發現了新的安全漏洞。團隊修好之後,你必須重建所有依賴該基礎映像檔的應用映像檔,並用新映像檔更新執行中的應用。實作映像檔建置器模式後,叢集同時知道「映像檔的建置」與「它的部署」,因而能在基礎映像檔改變時自動重新部署。
無守護程式建置(Daemonless Builds)
在 Kubernetes 內建置時,叢集完全掌控建置流程,但也因此需要更高的安全標準——建置不再是隔離執行的。要在叢集內建置,關鍵是不以 root 權限執行建置。
Docker 憑藉無與倫比的使用者體驗,成功把容器技術帶給大眾。它基於客戶端-伺服器架構,背景執行的 Docker daemon 透過 REST API 接收客戶端指令。這個 daemon 主要因網路與 volume 管理需求而需要 root 權限,但這帶來安全風險:不受信任的行程可能逃出容器,入侵者便能取得整台主機的控制權。
這個疑慮不只適用於執行容器,也適用於建置容器——因為 Docker daemon 執行任意指令時,建置同樣發生在容器內。
許多專案因此誕生,讓 Docker 建置不需要 root 權限,以縮小攻擊面。有些不允許建置期間執行指令(例如 Jib),其他工具則採用不同技術。撰寫本書時,最知名的無守護程式映像檔建置工具是 img、buildah 與 Kaniko;此外 S2I 系統也在不需要 root 權限的情況下完成映像檔建置。
解法#
在 Kubernetes 叢集中建置映像檔,最古老也最成熟的方式是 OpenShift build 子系統,它支援多種建置方式,其中一種是 Source-to-Image(S2I)——一種以「builder image」進行建置、帶有明確主張(opinionated)的做法。
另一個叢集內建置的機制是 Knative Build,它運行在 Kubernetes 與 service mesh Istio 之上,是 Knative(一個建置、部署與管理無伺服器工作負載的平台)的主要組成之一。
OpenShift Build#
Red Hat OpenShift 是 Kubernetes 的企業版發行版。除了支援 Kubernetes 的一切之外,它加入了整合式容器映像檔 registry、單一登入支援、新的使用者介面,也為 Kubernetes 加上原生的映像檔建置能力。OpenShift build 是第一個與叢集整合、直接建置由 Kubernetes 管理之映像檔的方式,支援多種建置策略:
- Source-to-Image(S2I):取用應用原始碼,藉助語言特定的 S2I builder image 建立可執行的成品,再把映像檔推送到整合式 registry。
- Docker Builds:使用 Dockerfile 加上一個 context 目錄,像 Docker daemon 那樣建立映像檔。
- Pipeline Builds:把建置對應到內部託管之 Jenkins 伺服器的建置工作,讓使用者設定 Jenkins pipeline。
- Custom Builds:讓你完全掌控映像檔的建立方式。在自訂建置中,你必須自己在建置容器內建立映像檔並推送到 registry。
建置的輸入可來自不同來源:
- Git:以遠端 URL 指定的儲存庫,從中取得原始碼。
- Dockerfile:直接存放為建置組態資源一部分的 Dockerfile。
- Image:另一個容器映像檔,從中抽取檔案供當前建置使用。這個來源型別讓「串接建置(chained builds)」成為可能。
- Secret:為建置提供機密資訊的資源。
- Binary:從外部提供所有輸入,必須在啟動建置時提供。
可用哪些輸入來源、以何種方式使用,取決於建置策略。Binary 與 Git 是互斥的來源型別,其他來源可以組合或單獨使用。
所有建置資訊定義在名為 BuildConfig 的中心資源物件中。我們可以直接把它套用到叢集,或使用 oc(OpenShift 版的 kubectl)——它支援定義與觸發建置的專屬指令。
在看 BuildConfig 之前,需要先理解兩個 OpenShift 特有的概念:
ImageStream
一個引用一或多個容器映像檔的 OpenShift 資源,有點像包含多個不同標籤映像檔的 Docker repository。OpenShift 把實際帶標籤的映像檔對應到 ImageStreamTag 資源,於是一個 ImageStream(repository)就擁有一份 ImageStreamTag(帶標籤映像檔)的引用清單。
為什麼需要這層額外抽象?因為它讓 OpenShift 能在 registry 中某個 ImageStreamTag 的映像檔更新時發出事件。 映像檔在建置期間、或被推送到 OpenShift 內部 registry 時產生;建置或部署就能監聽這些事件,觸發新的建置或啟動部署。
要把 ImageStream 連到部署,OpenShift 使用
DeploymentConfig資源,而非只能直接使用容器映像檔引用的 KubernetesDeployment資源。不過若你不打算使用 ImageStream,在 OpenShift 上仍可使用原生的Deployment。
Trigger(觸發器)
可以視為一種事件監聽器。其中一種是 imageChange,它對「ImageStreamTag 變更所發布的事件」做出反應——例如導致另一個映像檔重建,或讓使用該映像檔的 Pod 重新部署。
Source-to-Image(S2I)#
S2I builder image 就是一個標準容器映像檔,內含一組 S2I 腳本,其中兩個指令是必要的:
assemble:建置開始時被呼叫。它的任務是取用某個已設定輸入所提供的原始碼,必要時編譯,並把最終成品複製到適當位置。run:作為此映像檔的進入點。OpenShift 部署映像檔時會呼叫這個腳本,它使用產生的成品來提供應用服務。
你也可以選擇性地提供腳本來顯示用法訊息、為「增量建置」保存產生的成品(供後續建置中的
assemble腳本取用),或加入一些健全性檢查。
S2I 建置有兩項材料:builder image 與 source input。建置啟動時(因收到觸發事件或手動啟動),S2I 建置系統把兩者結合;當建置映像檔完成工作(例如編譯完原始碼),該容器就被提交為一個映像檔並推送到設定的 ImageStreamTag。這個映像檔包含編譯與備妥的成品,且其 run 腳本被設為進入點。

圖 25-1:以 Git 原始碼為輸入的 S2I 建置
apiVersion: v1
kind: BuildConfig
metadata:
name: random-generator-build
spec:
source:
git: # 要取得的原始碼引用,此例從 GitHub 抓取
uri: https://github.com/k8spatterns/random-generator
strategy:
sourceStrategy: # sourceStrategy 切換為 S2I 模式
from:
kind: DockerImage # builder image 直接從 Docker Hub 取得
name: fabric8/s2i-java
output:
to:
# 要以產生之映像檔更新的 ImageStreamTag,
# 亦即 assemble 腳本執行後被提交的 builder 容器
kind: ImageStreamTag
name: random-generator-build:latest
triggers:
- type: ImageChange # builder image 更新時自動重建S2I 是建立應用映像檔的健壯機制,且比純 Docker 建置更安全,因為建置流程完全由受信任的 builder image 掌控。但它仍有一些缺點:
建議做法:架設一個叢集內部的 Maven repository 作為快取,並把 builder image 設定為存取這個共用 repository,而非從遠端下載成品。
另一種縮短建置時間的方式是 S2I 的增量建置,重用先前建置所建立或下載的成品。但這會從先前產生的映像檔複製大量資料到當前建置容器,效能好處通常不比「用叢集本地代理保存依賴」高多少。
為了擺脫 Maven 這類不必要的 builder 工具,OpenShift 提供串接建置,取用 S2I 建置的結果來建立精簡的執行期映像檔。
Docker builds#
OpenShift 也支援在叢集內直接進行 Docker 建置:把 Docker daemon 的 socket 直接掛載進建置容器,再用它執行 docker build。來源是一個 Dockerfile 與一個裝著 context 的目錄;你也可以用 Image 來源引用任意映像檔,從中把檔案複製到 Docker 建置的 context 目錄——這個技巧搭配觸發器,正是串接建置的基礎。
或者,你也可以用標準的多階段(multistage)Dockerfile 來分離建置與執行期部分,結果與下述串接建置相同。
串接建置(Chained builds)#
串接建置由一次初始的 S2I 建置組成,產生二進位執行檔這類執行期成品;接著由第二次建置(通常是 Docker 類型)從產生的映像檔中取出這個成品。

圖 25-2:串接建置——以 S2I 編譯、以 Docker build 產生應用映像檔
apiVersion: v1
kind: BuildConfig
metadata:
name: runtime
spec:
source:
images:
# Image 來源引用「含 S2I 建置結果」的 ImageStream,
# 並選出映像檔中含已編譯 JAR 封存檔的目錄
- from:
kind: ImageStreamTag
name: random-generator-build:latest
paths:
- sourcePath: /deployments/.
destinationDir: "."
# Docker 建置的 Dockerfile 來源:從 S2I 產生的 ImageStream 複製 JAR 封存檔
dockerfile: |-
FROM openjdk:8-alpine
COPY *.jar /
CMD java -jar /*.jar
strategy:
type: Docker # 策略選擇 Docker 建置
output:
to:
kind: ImageStreamTag
name: random-generator:latest
triggers:
# S2I 結果的 ImageStream 改變時自動重建
#(也就是在成功執行 S2I 編譯出 JAR 封存檔之後)
- imageChange:
automatic: true
from:
kind: ImageStreamTag
name: random-generator-build:latest
type: ImageChange注意這裡的觸發器監控的是 S2I 建置的結果。每當我們執行 S2I 建置,它就會導致執行期映像檔重建,讓兩個 ImageStream 始終保持同步。最終推送到
random-generatorImageStream 的映像檔,就能用於 DeploymentConfig 來執行應用。
Knative Build#
Google 於 2018 年啟動 Knative 專案,目標是為 Kubernetes 帶來進階的應用相關功能。
Knative 的基礎是 Istio 這類 service mesh,它開箱即用地提供流量管理、可觀測性與安全的基礎設施服務(service mesh 使用邊車為應用埋設基礎設施相關功能)。在 service mesh 之上,Knative 提供主要面向應用開發者的額外服務:
- Knative serving:為應用服務提供 scale-to-zero 支援,可供 FaaS 平台運用。搭配「彈性擴縮」模式與底層 service mesh 支援,Knative serving 讓服務能從零擴縮到任意多個複本。
- Knative eventing:透過 channel 把事件從來源送到接收端的機制。事件可以觸發作為 sink 的 Service 從零擴大。
- Knative build:在 Kubernetes 叢集內把應用原始碼編譯成容器映像檔。後續專案是 Tekton Pipelines,最終將取代 Knative build。
Istio 與 Knative 都以 Operator 模式實作,並用 CRD 宣告它們所管理的領域資源。
Knative 的設計是提供建構單元以整合你既有的 CI/CD 方案,它本身並不是一套建置容器映像檔的 CI/CD 解決方案;Knative build 主要面向工具開發者,讓他們為終端使用者提供介面與無縫的建置體驗。
簡單的 Build#
Build CRD 是 Knative build 的核心元素,它定義了 Knative build operator 需要執行的具體步驟:
- source 規格:指向應用原始碼的位置。可以是 Git 儲存庫、Google Cloud Storage 這類遠端位置,甚至是任意一個 build operator 能從中抽取原始碼的容器。
- steps:把原始碼變成可執行容器映像檔所需的步驟。每個步驟引用一個用來執行該步驟的 builder image。每個步驟都能存取掛載在
/workspace的 volume,它裝著原始碼,同時也用於步驟之間共享資料。
apiVersion: build.knative.dev/v1alpha1
kind: Build
metadata:
name: random-generator-build-jib # build 物件的名稱
spec:
source:
git: # 以 GitHub URL 指定的原始碼規格
url: https://github.com/k8spatterns/random-generator.git
revision: master
steps: # 一或多個建置步驟
- name: build-and-push
image: gcr.io/cloud-builders/mvn # 此步驟使用的映像檔,內含 Java 與 Maven
args: # 傳給 builder 容器的參數,
# 會觸發 Maven 透過 jib-maven-plugin
# 編譯、建立並推送容器映像檔
- compile
- com.google.cloud.tools:jib-maven-plugin:build
- -Djib.to.image=registry/k8spatterns/random-generator
workingDir: /workspace # /workspace 為每個建置步驟共享並掛載這裡用 Jib 在不需要 Docker daemon 的情況下建置映像檔並推送到 registry。
底層如何運作

圖 25-3:Knative build 使用 init container
自訂的 Build 資源會被轉換成一般的 Kubernetes 資源:一個 Build 變成一個 Pod,建置步驟被轉譯成一連串依序呼叫的 Init Container。
- 最前面幾個 init container 是隱含建立的:本例中一個負責初始化與外部儲存庫互動所需的憑證,另一個負責從 GitHub 取出原始碼。
- 其餘的 init container 就是我們宣告的步驟與其 builder image。
- 所有 init container 結束後,主容器只是個什麼都不做的 no-op,於是 Pod 在初始化步驟後就停止。
Build templates#
上例只有單一建置步驟,但典型的建置由多個步驟組成。BuildTemplate 自訂資源可以讓相似的建置重用同一組步驟。
以下範本包含三個步驟:
- 用
mvn package產生 Java JAR 檔。 - 產生一個 Dockerfile,把 JAR 複製進容器映像檔,並以
java -jar啟動它。 - 用 builder image 透過 Kaniko 建立並推送容器映像檔(Kaniko 是 Google 開發的工具,能在容器內、以使用者空間執行的本地 Docker daemon,從 Dockerfile 建置容器映像檔)。
範本與 Build 大致相同,差別在於它支援參數作為佔位符,在使用範本時才填入。本例中
IMAGE是唯一需要的參數,用來指定要建立的目標映像檔。
apiVersion: build.knative.dev/v1alpha1
kind: BuildTemplate
metadata:
name: maven-kaniko
spec:
parameters: # 使用此範本時需提供的參數清單
- name: IMAGE
description: The name of the image to create and push
steps:
- name: maven-build # 用 Maven 編譯與封裝 Java 應用的步驟
image: gcr.io/cloud-builders/mvn
args:
- package
workingDir: /workspace
- name: prepare-docker-context # 產生 Dockerfile 以複製並啟動 JAR 的步驟
image: alpine
command: [....]
- name: image-build-and-push # 呼叫 Kaniko 建置並推送 Docker 映像檔的步驟
image: gcr.io/kaniko-project/executor
args:
- --context=/workspace
- --destination=${IMAGE} # 目的地使用所提供的範本參數接著 Build 只需指定範本名稱,而不必列出步驟清單:
apiVersion: build.knative.dev/v1alpha1
kind: Build
metadata:
name: random-generator-build-chained
spec:
source: # 原始碼來源規格
git:
url: https://github.com/k8spatterns/random-generator.git
revision: master
template: # 引用前面定義的範本
name: maven-kaniko
arguments: # 作為參數填入範本的映像檔規格
- name: IMAGE
value: registry:80/k8spatterns/random-generator如你所見,你只需指定要建立的應用容器映像檔名稱,就能輕鬆重用這個多步驟建置來建立不同的應用。Knative 的 build-templates 儲存庫中有許多預先定義好的範本。
討論#
我們看了兩種在叢集內建置容器映像檔的方式。
OpenShift build 漂亮地展示了「在同一叢集內建置與執行應用」的主要好處之一:透過 ImageStream 觸發器,你不只能串連多個建置,還能在建置更新了應用容器映像檔時重新部署應用。
這在較前段的環境特別有用——那裡的建置步驟通常緊接著部署步驟。建置與部署之間更好的整合,是邁向持續交付這個聖杯的一步。
搭配 S2I 的 OpenShift build 是久經驗證的成熟技術,但 S2I 目前只能在 OpenShift 版本的 Kubernetes 上使用。
Knative build 是這個模式的另一種實作。它的主要目的是把原始碼轉換成可執行的容器映像檔並推送到 registry,好讓 Deployment 取用;這些步驟由 builder image 執行,可為不同技術各自提供。Knative build 對建置的具體步驟不置可否,它關心的是如何管理建置的生命週期、以及如何排程建置。
Knative build 仍是年輕專案(2019 年),提供的是叢集建置的建構單元。它不太是給終端使用者用的,而是給工具打造者用的。 可以預期新舊工具會陸續支援 Knative build 或其後繼專案,屆時我們將看到更多映像檔建置器模式的實作。
更多資訊#
- ImageBuilder Examples
- Jib
- Img
- Buildah
- Kaniko
- OpenShift Builds Design Document
- Multistage Dockerfile
- Chaining S2I Builds
- Build Triggers
- Source-to-Image Specification
- Incremental S2I Builds
- Knative
- Building Container Images on Your Kubernetes Cluster with Knative Build
- Knative Build
- Tekton Pipelines
- Knative: Building Your Serverless Service
- Introducing Knctl: A Simpler Way to Work with Knative
- Interactive Knative Build Tutorial
- Knative Build Templates