「不可變組態」模式把組態資料封裝進不可變的容器映像檔,並在執行期把這個組態容器連結到應用。這讓我們不只能使用不可變且有版本的組態資料,還能突破環境變數或 ConfigMap 的大小限制。
問題#
環境變數提供了設定容器化應用的簡單方式,好用且普遍受支援;但一旦數量超過某個門檻,管理起來就變得困難。這種複雜度雖可用組態資源緩解一部分,但這些模式都沒有強制組態資料本身的不可變性。
這裡的不可變性是指:應用啟動後就無法改變組態,以確保組態資料永遠處於定義明確的狀態。此外,不可變組態還能納入版本控管,並遵循變更控制流程。
解法#
把所有環境特定的組態資料放進一個被動的資料映像檔,像一般容器映像檔那樣散布。執行期把應用與資料映像檔連結起來,讓應用從資料映像檔中取出組態。
用這種做法,為不同環境打造不同的組態資料映像檔非常容易。這些映像檔集合了特定環境的所有組態資訊,並能像其他容器映像檔一樣做版本控管。
建立這種資料映像檔很簡單——它就是一個只裝資料的容器映像檔。挑戰在於啟動時的「連結」步驟,做法依平台而異。
Docker Volumes#
先退一步看純 Docker 的情況。在 Docker 中,容器可以曝露一個帶有容器內資料的 volume:在 Dockerfile 中用 VOLUME 指令指定一個稍後可共享的目錄,啟動時該目錄的內容會被複製到這個共享目錄。

圖 20-1:使用 Docker volume 的不可變組態——這是把組態資訊從專屬組態容器分享給另一個應用容器的絕佳方式
以開發環境為例,我們建立一個裝著開發者組態、並建立 /config volume 的映像檔:
FROM scratch
ADD app-dev.properties /config/app.properties # 加入指定的屬性檔
VOLUME /config # 建立 volume 並把屬性檔複製進去建置映像檔與容器:
docker build -t k8spatterns/config-dev-image:1.0.1 -f Dockerfile-config
docker create --name config-dev k8spatterns/config-dev-image:1.0.1 .最後啟動應用容器並連結到這個組態容器:
docker run --volumes-from config-dev k8spatterns/welcome-servlet:1.0應用映像檔預期在 /config 目錄(也就是組態容器曝露的 volume)中找到組態檔。
從開發環境移到正式環境時,你要改的只有啟動指令,完全不必更動應用映像檔本身——只需把應用容器改為連結到正式環境的組態容器:
docker build -t k8spatterns/config-prod-image:1.0.1 -f Dockerfile-config
docker create --name config-prod k8spatterns/config-prod-image:1.0.1 .
docker run --volumes-from config-prod k8spatterns/welcome-servlet:1.0Kubernetes Init Containers#
在 Kubernetes 中,Pod 內的 volume 共享非常適合這種「組態容器與應用容器連結」的需求。
但若想把 Docker volume 連結的技巧搬到 Kubernetes,會發現 Kubernetes 目前不支援容器 volume。考慮到這個議題討論已久、實作複雜度高而效益有限,容器 volume 短期內大概不會出現。
也就是說:容器能共享外部 volume,卻還不能直接共享位於容器內部的目錄。因此要在 Kubernetes 中使用不可變組態容器,就得借助 Init Container 模式,在啟動期間初始化一個空的共享 volume。
在 Docker 範例中,我們的組態映像檔基於 scratch(不含任何作業系統檔案的空映像檔),因為我們只需要透過 Docker volume 共享組態資料。但對 Kubernetes init container 而言,我們需要基礎映像檔幫忙把組態資料複製到共享的 Pod volume——busybox 是好選擇,它仍然很小,卻能讓我們使用一般的 Unix cp 指令。
FROM busybox
ADD dev.properties /config-src/demo.properties
# 這裡用 shell 是為了解析萬用字元
ENTRYPOINT [ "sh", "-c", "cp /config-src/* $1", "--" ]與純 Docker 版本的唯一差別是:換了基礎映像檔,並加上一個 ENTRYPOINT,在映像檔啟動時把屬性檔複製到「以參數指定的目錄」。接著就能在 Deployment 的 .template.spec 中引用它:
initContainers:
- image: k8spatterns/config-dev:1
name: init
args:
- "/config"
volumeMounts:
- mountPath: "/config"
name: config-directory
containers:
- image: k8spatterns/demo:1
name: demo
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- mountPath: "/config"
name: config-directory
volumes:
- name: config-directory
emptyDir: {}這份 Pod 範本規格包含一個 volume 與兩個容器:
- volume
config-directory型別為emptyDir,因此會在承載該 Pod 的節點上建立為一個空目錄。 - init container 由我們剛建立的映像檔而來,並帶入單一參數
/config供其ENTRYPOINT使用,指示它把自身內容複製到指定目錄;該目錄由 volumeconfig-directory掛載而來。 - 應用容器掛載同一個
config-directoryvolume,以存取 init container 複製過來的組態。

圖 20-2:使用 init container 的不可變組態——應用容器透過共享 volume 存取 init container 建立的組態資料
現在要把組態從開發換成正式環境,只需抽換 init container 的映像檔:改 YAML 定義或用
kubectl更新皆可。
延伸:用 OpenShift Templates 避免逐環境編輯描述檔
為每個環境都得編輯一次資源描述檔並不理想。若你使用 Red Hat OpenShift(Kubernetes 的企業版發行版),OpenShift Templates 可以從單一範本為不同環境產生不同的資源描述檔。
Template 就是被參數化的常規資源描述檔,我們可以輕易把組態映像檔設為參數:
apiVersion: v1
kind: Template
metadata:
name: demo
parameters:
- name: CONFIG_IMAGE # 宣告範本參數 CONFIG_IMAGE
description: Name of configuration image
value: k8spatterns/config-dev:1
objects:
- apiVersion: v1
kind: DeploymentConfig
# ....
spec:
template:
metadata:
# ....
spec:
initContainers:
- name: init
image: ${CONFIG_IMAGE} # 使用該範本參數
args: [ "/config" ]
volumeMounts:
- mountPath: /config
name: config-directory
containers:
- image: k8spatterns/demo:1
# ...
volumeMounts:
- mountPath: /config
name: config-directory
volumes:
- name: config-directory
emptyDir: {}在 OpenShift 叢集上建立此範本後,就能用 oc 實例化它:
oc new-app demo -p CONFIG_IMAGE=k8spatterns/config-prod:1討論#
用資料容器來實作不可變組態,坦白說有點繁瑣,但這個模式有其獨到優勢:
- 環境特定的組態被封存在容器裡,因此能像其他容器映像檔一樣做版本控管。
- 這樣建立的組態可透過容器 registry 散布,不必存取叢集也能檢視組態。
- 組態如同承載它的容器映像檔一樣不可變:改組態就必須更新版本、產生新的容器映像檔。
- 當組態資料複雜到無法放進環境變數或 ConfigMap 時,組態資料映像檔可以承載任意大的組態資料。
不出所料,它也有缺點:
- 複雜度較高,因為需要額外建置容器映像檔並透過 registry 散布。
- 完全沒有處理敏感組態資料的安全疑慮。
- 在 Kubernetes 場景中需要額外的 init container 處理,因而必須為不同環境管理不同的 Deployment 物件。
總的來說,我們應該審慎評估是否真的需要這麼繁瑣的做法。若不可變性並非必要,一個單純的 ConfigMap 或許就完全足夠了。
若要處理的是「各環境間只有些微差異的大型組態檔」,下一章的「組態範本」模式提供了另一條路。
更多資訊#
- Immutable Configuration Example
- How to Mimic
--volumes-fromin Kubernetes - Feature Request: Image Volumes in Kubernetes
- docker-flexvol: A Kubernetes Driver that Supports Docker Volumes
- OpenShift Templates