本章延續第 14 章的組態設計哲學,聚焦於組態的具體實務層面。管理生產系統是 SRE 為組織提供價值的重要方式之一,而組態和運行應用程式需要深入了解系統的組成方式和運作原理。本章以 Jsonnet 作為代表性的組態語言,提供實際範例來說明如何減少組態相關的苦差事(Toil)。

組態引發的苦差事#

在專案生命週期初期,組態通常輕量且簡單,可能只有幾個 INI、JSON、YAML 或 XML 檔案。但隨著應用程式、伺服器和變異數量增加,組態會變得複雜且冗長。

複製苦差事(Replication Toil)#

原本只需編輯一個組態檔來「更改設定」,現在可能需要在多個位置更新組態檔。閱讀這些組態也很困難,因為重要的差異隱藏在大量無關的重複細節中。這種複製苦差事在微服務架構中尤其常見。

工程師通常透過建立自動化或組態框架來回應複製苦差事,使用「組態語言」來消除重複並讓組態更易於理解和維護。

複雜性苦差事(Complexity Toil)#

不幸的是,消除複製苦差事後,專案往往會重新快速成長,最終遭遇複雜性苦差事:處理複雜自動化所產生的非預期行為。這種苦差事通常在較大的組織(10+ 工程師)中出現,並隨著成長而加劇。越早處理越好。

減少組態引發的苦差事#

基本策略包括:

  • 移除組態:如果應用程式是自建的,某些組態面向可由應用程式自行處理(例如根據機器資訊指派預設值,或根據負載動態調整)
  • 引入組態語言:如果無法移除組態,且複製苦差事日益嚴重,考慮整合新的組態語言或改進現有設定
  • 採用最佳實務:無論使用何種語言,都可透過流程和工具來最小化複雜性苦差事

組態系統的關鍵特性與陷阱#

除了輕量、易學、簡潔和表達力等通用需求外,高效的組態系統還必須:

  • 透過工具(linter、debugger、formatter、IDE 整合等)支援組態健康、工程師信心和生產力
  • 提供封閉式組態評估(Hermetic Evaluation) 以支援回滾和可重現性
  • 分離組態與資料,以便於分析和支援多種組態介面

Google 在其發展歷程中創造了多個缺乏這些關鍵特性的組態系統,業界也很難找到不犯以下至少一個陷阱的系統。

陷阱一:未將組態視為程式語言問題#

如果你不是刻意在設計一個語言,那你最終得到的「語言」不太可能是好的。雖然組態語言描述的是資料而非行為,但它們仍具有程式語言的其他特徵。如果組態策略始於使用純資料格式的目標,程式語言特性往往會從後門悄悄滲入,最終變成一個難以理解的複雜語言。

典型例子包括:在 VM 配置中加入 count 屬性、字串插值規則不斷擴增、以及 YAML + Jinja 的組合。

陷阱二:設計隨意或臨時的語言特性#

隨時間向簡單的組態格式添加臨時性的程式語言特性,可能建立出功能完整但比正式設計的等效方案更複雜、表達力更弱的解決方案。臨時語言還容易產生陷阱和怪異行為,因為作者無法事先考慮特性之間的交互作用。

陷阱三:過度建構領域特定最佳化#

使用者基礎越小的領域特定方案,累積足夠使用者來證明建構工具的合理性所需的時間越長。工程師不願花時間理解適用範圍有限的語言,學習資源(如 Stack Overflow)也不太可能存在。

陷阱四:交織「組態評估」與「副作用」#

副作用包括在組態執行期間對外部系統做變更或查詢帶外資料來源(DNS、VM ID、最新建構版本等)。允許這些副作用的系統違反了封閉性,也阻止了組態與資料的分離。極端情況下,你甚至無法在不花錢預留雲端資源的情況下除錯組態。

正確做法是:先評估組態,再將結果資料提供給使用者分析,最後才允許副作用。

陷阱五:使用通用腳本語言#

使用 Python、Ruby 或 Lua 等通用腳本語言看似能輕易避免前四個陷阱,但這些實作通常很重量級,且需要侵入性的沙箱機制來確保封閉性和安全性。此外,不能假設維護組態的人都熟悉這些語言。

避免這些陷阱的建議是使用現有的可重用領域特定語言(DSL),如 HOCON、Flabbergast、Dhall 和 Jsonnet。即使 DSL 看起來功能過於強大,你總可以透過內部風格指南來限制其功能。

整合組態語言#

以特定格式產生組態#

組態語言可能原生輸出正確格式(如 Jsonnet 輸出 JSON)。對於不原生支援的格式,需要:

  1. 找到在組態語言中表示組態資料的方式
  2. 利用該語言的建構來減少重複
  3. 撰寫(或重用)必要輸出格式的序列化函式

驅動多個應用程式#

一旦能從組態語言驅動任意應用程式,就可能從同一組態中針對多個應用程式產生輸出:

  • 從單一 Jsonnet 評估中輸出 Nginx 網頁伺服器組態和 Terraform 防火牆組態
  • 從相同檔案配置監控儀表板、保留政策和告警通知管線
  • 在 VM 啟動腳本和磁碟映像建構腳本之間移動初始化命令

整合既有應用程式:Kubernetes 案例#

Kubernetes 是一個有趣的案例研究,因為:

  • 在 Kubernetes 上運行的作業需要配置,組態可能變得複雜
  • Kubernetes 不附帶內建的組態語言

Kubernetes 使用者的開箱體驗是撰寫代表 Kubernetes 物件的 YAML 檔案,然後用 kubectl 部署。YAML 提供了註解和簡潔語法,但在抽象化方面有所不足——它只提供了錨點(Anchors),在實務中很少有用。

使用 Jsonnet 整合 Kubernetes#

假設需要複製一個 Kubernetes 物件四次,只有命名空間、標籤等細微差異。使用純 YAML 意味著完整複製四份檔案,重要差異被大量重複淹沒。

使用 Jsonnet 後,可以建立抽象模板並實例化四次,每次只指定差異部分。關鍵技巧包括:

  • local 關鍵字定義變數,self 參照最近的外圍物件
  • 雙冒號(::)建立隱藏欄位,不輸出到產生的 JSON 但可被覆寫和參照
  • error 建構表達「必須覆寫」的語意,類似抽象方法
  • 使用 +: 語法可覆寫特定低層次細節,提供「跳脫簡潔」的逃生艙口

抽象共通性的好處隨著差異變得更微妙或更難表達時更加顯著,適用場景包括:

  • 管理跨不同環境(prod/stage/dev/test)的部署
  • 組織的基礎架構團隊維護可重用元件模板,應用程式團隊各自實例化

整合自建應用程式#

為自建應用程式設計組態時的最佳實務:

  • 消費單一純資料檔案,讓組態語言處理檔案的分割和匯入
  • 使用物件(而非陣列)表示具名實體的集合,以欄位名稱作為 key
  • 避免在頂層按類型分組實體,而是將邏輯相關的組態分組在同一子樹
  • 保持資料表示設計簡潔:不在資料表示中嵌入語言特性,不擔心過於冗長的資料表示,避免在應用程式中解譯自訂字串插值語法

組態變更隨時間往往成為中斷事件根本原因的主要來源。驗證組態變更是維護可靠性的關鍵步驟:語法驗證(如 JSON 是否可解析)不足以發現許多錯誤,還需要進行通用的 schema 驗證和領域特定的屬性檢查。不要忽略未識別的欄位名稱,因為它們可能表示打字錯誤。

有效運作組態系統#

版本控制#

組態語言通常促使工程師撰寫模板庫和工具函式庫。當需要對庫進行重大變更時,有兩個選擇:

  • 提交所有客戶端程式碼的全域更新(可能在組織上不可行)
  • 對庫進行版本控制,讓不同消費者獨立遷移

原始碼管理#

將組態簽入原始碼管理系統,可獲得歷史記錄追蹤、變更稽核、簡易回滾和程式碼審查等能力。

工具#

考慮如何強制執行風格和 lint 組態,調查是否有編輯器外掛可整合這些工具。目標是在所有作者間維持一致的風格、提高可讀性和偵測錯誤。

測試#

建議為上游模板庫實作單元測試,確保庫在以各種方式實例化時能產生預期的具體組態。在 Jsonnet 中,可以撰寫測試檔案來匯入庫、執行庫,並使用 assertstd.assertEqual 驗證輸出。

何時評估組態#

封閉性意味著組態語言無論何時何地執行都應產生相同的組態資料。有三個時機可以評估組態,各有優缺點:

極早期:簽入 JSON#

在將 Jsonnet 程式碼簽入版本控制前先產生 JSON,一併簽入。

  • 優點:審查者可檢查具體變更;可在產生層和抽象層進行行級註解;執行時不需運行 Jsonnet
  • 缺點:產生的 JSON 不一定可讀;JSON 可能不適合簽入版控(太大或包含機密);多人同時編輯可能產生合併衝突

中間路線:建構時評估#

在建構時運行 Jsonnet 命令列工具,將產生的 JSON 嵌入發行產物。

  • 優點:能控制執行時複雜度且無需每次 PR 都重建 JSON;Jsonnet 程式碼和 JSON 之間不會脫同步
  • 缺點:建構更複雜;程式碼審查時較難評估具體變更

晚期:執行時評估#

連結 Jsonnet 庫,讓應用程式在任何時候解譯組態。

  • 優點:更簡單,不需要事先評估;可評估使用者在執行期間提供的 Jsonnet 程式碼
  • 缺點:增加佔用空間和風險暴露;組態錯誤可能在執行時才發現(為時已晚);如果 Jsonnet 程式碼不受信任,需要特別小心

防範濫用組態#

組態執行應該快速終止並產出結果,但由於錯誤或惡意攻擊,組態可能消耗任意的 CPU 時間或記憶體。即使限制語言使其不再圖靈完備,也不能完全防止資源過度消耗。

即使是簡單的組態格式(如 XML 和 YAML)也存在資源過度消耗的程式。例如 YAML 的遞迴錨點可以指數級地擴展資料。

風險程度取決於場景:

  • 低風險:命令列工具在使用者自己的機器上使用 Jsonnet 建構 Kubernetes 物件,程式碼是受信任的
  • 高風險:像 Helm 或 Spinnaker 這類服務接受任意組態程式碼並在請求處理器中評估,必須防範 DOS 攻擊

對於不受信任的 Jsonnet 程式碼,可透過使用獨立程序和 ulimit 來沙箱化執行,確保在給定資源內未完成的程式安全失敗並通知使用者。

結論#

無論使用 Jsonnet、其他組態語言還是自行開發,組態語言的最低關鍵特性是:良好的工具支援、封閉式組態,以及組態與資料的分離。

你的系統可能還不夠複雜到需要組態語言。當複雜度增加時,轉向領域特定語言(如 Jsonnet)是一個值得考慮的策略,它能讓你提供一致且結構良好的介面,並釋放 SRE 團隊的時間去處理其他重要專案。