「環境變數組態」是設定應用最簡單的方式。對於數量不多的組態值,把它們放進普遍受支援的環境變數,是外部化組態最容易的做法。 本章介紹在 Kubernetes 中宣告環境變數的各種方式,也說明用環境變數處理複雜組態時的侷限。

問題#

任何有點份量的應用,都需要組態來存取資料來源、外部服務,或做正式環境層級的調校。早在《The Twelve-Factor App》宣言之前我們就已知道:把組態寫死在應用裡是壞事。組態應該被外部化,讓我們在應用建置完成後仍能改動它。對容器化應用而言這更有價值,因為容器本就鼓勵共享不可變的應用成品。

解法#

《The Twelve-Factor App》宣言建議用環境變數儲存應用組態。這個做法簡單,且在任何環境與平台都行得通:每個作業系統都知道如何定義環境變數並傳遞給應用,每種程式語言也都能輕易存取它們。

使用環境變數時的典型模式是:在建置期定義寫死的預設值,執行期再覆寫它們。

在 Docker 中定義#

Docker 映像檔可用 ENV 指令直接在 Dockerfile 中定義環境變數,逐行或單行皆可:

FROM openjdk:11
ENV PATTERN "EnvVar Configuration"
ENV LOG_FILE "/tmp/random.log"
ENV SEED "1349093094"

# 或者寫成一行:
ENV PATTERN="EnvVar Configuration" LOG_FILE=/tmp/random.log SEED=1349093094

容器內的 Java 應用只要呼叫標準函式庫就能取得這些變數:

public Random initRandom() {
  // 用來自環境變數的種子初始化亂數產生器
  long seed = Long.parseLong(System.getenv("SEED"));
  return new Random(seed);
}

直接執行這個映像檔會使用寫死的預設值;但多數情況下你會想從映像檔外部覆寫這些參數:

docker run -e PATTERN="EnvVarConfiguration" \
           -e LOG_FILE="/tmp/random.log" \
           -e SEED="147110834325" \
           k8spatterns/random-generator:1.0

在 Kubernetes 中定義#

這類環境變數可以直接寫在 Deployment、ReplicaSet 等控制器的 Pod 規格中:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
      env:
        - name: LOG_FILE # 直接給字面值的環境變數
          value: /tmp/random.log
        - name: PATTERN # 來自 ConfigMap 的環境變數
          valueFrom:
            configMapKeyRef:
              name: random-generator-config # ConfigMap 的名稱
              key: pattern # 在該 ConfigMap 中查找值的鍵
        - name: SEED # 來自 Secret 的環境變數(查找語意與 ConfigMap 相同)
          valueFrom:
            secretKeyRef:
              name: random-generator-secret
              key: seed

在 Pod 範本中,你不只能把值直接綁到環境變數(如 LOG_FILE),也能委派給 Kubernetes Secret(敏感資料)與 ConfigMap(非敏感組態)。

這層間接的好處是:環境變數可以獨立於 Pod 定義之外被管理。

上例中 SEED 來自 Secret 資源,這雖然是 Secret 完全合理的用法,但仍必須指出:環境變數並不安全。把敏感、可讀的資訊放進環境變數,會讓這些資訊很容易被讀取,甚至可能外洩到日誌中

關於預設值:什麼時候不該給

預設值讓人輕鬆,因為它免去了「為一個你甚至不知道存在的組態參數挑值」的負擔,在**約定優於配置(convention over configuration)**的典範中也扮演重要角色。

但預設值不總是好主意,對持續演進的應用甚至可能是反模式,因為事後修改預設值是件難事

  • 改預設值意味著在程式碼中替換它們,這需要重新建置。
  • 依賴預設值的人(無論是照慣例還是刻意為之)在預設值改變時總會感到意外。我們必須溝通這項變更,而使用者的呼叫端程式碼很可能也得跟著改。

然而預設值的變更往往是必要的,因為一開始就把預設值訂對本來就很難。關鍵是:把預設值的變更視為重大變更(major change);若採用語意化版本,這種修改足以讓主版號進位。

若對某個預設值不滿意,通常更好的做法是乾脆把預設值移除,並在使用者未提供組態值時拋出錯誤。這至少會讓應用早早、顯眼地失敗,而不是默默做出不同且非預期的行為。

綜合以上:除非你有九成把握某個合理的預設值能撐很久,否則從一開始就避免預設值往往是最佳解。密碼或資料庫連線參數就是「不該提供預設值」的好例子,因為它們高度依賴環境、往往無法可靠地預測。此外,不用預設值時,組態資訊必須被明確提供,這本身也起到文件的作用

討論#

環境變數容易使用、人人都懂,這個概念能平順地對應到容器,而且每個執行期平台都支援它。

但環境變數不安全,而且只適合數量適中的組態值。當有大量不同參數要設定時,管理這些環境變數會變得笨重。

這種情況下,許多人會多加一層間接:把組態放進多份組態檔(每個環境一份),再用單一環境變數來選擇其中一份——Spring Boot 的 profile 就是這種做法。

但這些 profile 組態檔通常存放在應用內部、也就是容器內,這讓組態與應用緊密耦合。結果往往是開發與正式環境的組態並排躺在同一個 Docker 映像檔裡,任何一邊的變更都要重建映像檔。這再次說明:環境變數只適合小規模的組態。

環境變數的另外兩個缺點:

  • 定義位置分散。 正因為環境變數放諸四海皆準,我們可以在各個層級設定它們,這導致組態定義碎片化——對某個環境變數而言,很難追查它到底從哪來。沒有一個集中定義所有環境變數的地方,除錯組態問題就很困難。
  • 只能在應用啟動前設定,之後無法變更。

「無法熱改」這點有一體兩面:一方面你無法在執行期即時調校應用;但另一方面,許多人視之為優點,因為它把不可變性也推廣到了組態。這裡的不可變性意味著:你丟棄執行中的應用容器,用修改後的組態啟動新副本,而且很可能透過滾動更新這類平順的部署策略進行。這樣一來,你永遠處於一個明確且已知的組態狀態。

環境變數簡單好用,但主要適用於簡單的使用案例,面對複雜組態需求則有侷限。接下來的「組態資源」、「不可變組態」與「組態範本」模式,正是為了克服這些侷限。

更多資訊#

  • EnvVar Configuration Example
  • The Twelve-Factor App
  • Immutable Server
  • Spring Boot Profiles for Using Sets of Configuration Values