「不可變組態」模式把組態資料封裝進不可變的容器映像檔,並在執行期把這個組態容器連結到應用。這讓我們不只能使用不可變且有版本的組態資料,還能突破環境變數或 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.0

Kubernetes 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 使用,指示它把自身內容複製到指定目錄;該目錄由 volume config-directory 掛載而來。
  • 應用容器掛載同一個 config-directory volume,以存取 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-from in Kubernetes
  • Feature Request: Image Volumes in Kubernetes
  • docker-flexvol: A Kubernetes Driver that Supports Docker Volumes
  • OpenShift Templates