DevOps - 如何使用 Python 合併 YAML
在 Kubernetes 的世界中,Helm 是最主流的套件管理工具。而在 Helm Chart 中,values.yaml 扮演著靈魂角色,定義了所有部署參數的預設值(Default Values)。
然而,隨著環境變多(dev, stag, prod),我們經常面臨一個挑戰:如何有效管理不同環境的差異?
通常,我們會有一個巨大的 values.yaml 作為預設值,然後針對不同環境建立 values-prod.yaml 或 values-dev.yaml。
雖然 Helm 原生指令 helm install -f values.yaml -f values-prod.yaml 支援執行時合併,但在 CI/CD 流程中,我們常需要用腳本預先生成一份「最終版」的設定檔,用於:
- Config Linting:在部署前對最終設定進行驗證。
- Audit Log:將實際部署的完整設定檔存檔備查。
- GitOps:生成靜態的
Manifest提交到 Git Repo。
本文將示範如何使用 Python 實作這種 YAML Deep Merge(深層合併) 機制,模擬 Helm 的繼承行為。
為什麼需要合併?
想像一下,一個標準的 Helm Chart values.yaml 可能有上千行設定。
- values.yaml:預設值。定義了
Image、Service.Port、Liveness Probe等所有環境通用的設定。 - values-prod.yaml:各環境差異。設定只包含生產環境的差異,例如
replicaCount、resources或ingress。
我們的目標是:保留預設值的所有設定,再精準地將你需要的環境差異「更新」上去。
核心實作邏輯:Python 與 Deepmerge
Python 內建的 update() 方法無法處理巢狀結構(它會直接覆蓋掉整個字典),因此我們需要使用 deepmerge 套件來實現遞迴合併。
1. 環境準備
1 | pip install PyYAML deepmerge |
2. 核心合併策略
我們的程式碼核心在於利用 deepmerge.always_merger 來處理兩個字典的合併。它能確保子層級的 Key 不會因為父層級被覆蓋而遺失。1
2
3
4
5
6
7
8from deepmerge import always_merger
def merge(base_path, overlay_path):
# ... (讀取檔案邏輯省略) ...
# 使用 deepmerge 進行合併
# 這會保留 base_dict 中未被覆蓋的巢狀結構
return always_merger.merge(base_dict, overlay_dict)
範例
讓我們用一個經典的 Kubernetes Nginx 設定來測試這個工具的效果。
values.yaml (預設值)
這是 Chart 的預設值,適用於大多數情況,資源給得很小,副本數只有 1。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22replicaCount: 1
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.19.0"
service:
type: ClusterIP
port: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
ingress:
enabled: false
annotations: {}
values-prod.yaml (環境設定)
這是生產環境的設定。注意看,我們不需要重寫 service 或 image.repository,我們只寫變更的部分:副本數改為 3,資源加大,並開啟 Ingress。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18replicaCount: 3 # 生產環境需要高可用
image:
tag: "1.21.0-alpine" # 生產環境使用特定版本
resources: # 覆蓋資源限制
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
ingress:
enabled: true # 開啟 Ingress
hosts:
- host: api.example.com
paths: ["/"]
合併
將附錄的程式碼存為 merge_values.py 並執行:1
python merge_values.py values.yaml values-prod.yaml final-values.yaml
結果
生成的檔案完美地融合了兩者。請注意:
service區塊原封不動地繼承了下來。image區塊中,repository繼承自values.yaml,但tag被 Prod 環境覆蓋了。resources區塊被 Prod 的設定完整更新。
1 | replicaCount: 3 |
總結
透過這個簡單的 Python 工具,我們實現了類似 Helm 內部的 Values 合併邏輯。這在 DevOps 流程中帶來了極大的靈活性:
- 視覺化 Debug:當 Helm 部署不如預期時,可以先跑這個工具看看到底合併出了什麼。
- 檔案快照:在執行 helm install 之前,可以將這個
final-values.yaml存到 Artifact Registry,未來若要 Rollback 或稽核,除了看 Helm History,還有實體檔案可查。 - 邏輯擴充:如果您需要與 Helm 不一樣的合併邏輯,你可以參考 deepmerge 的
Merger來達成。
https://deepmerge.readthedocs.io/en/latest/
完整程式碼
您可以直接複製以下程式碼使用,請先記得安裝相依套件 PyYAML 和 deepmerge。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68import os.path
import sys
import yaml
from deepmerge import always_merger
# 簡單的 Logger 設置
import logging
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)
def load_yaml_file(file_path):
"""
安全地讀取 YAML 檔案
"""
if os.path.isfile(file_path):
try:
with open(file_path, "r") as f:
return yaml.safe_load(f)
except Exception as e:
logger.error(f"Error loading YAML file {file_path}: {e}")
else:
logger.error(f"YAML file {file_path} does not exist")
return None
def merge(base_path, overlay_path):
"""
執行 Deep Merge
:param base_path: 通常是 Chart 的預設 values.yaml
:param overlay_path: 環境特定的 values-prod.yaml
"""
# 1. 讀取預設值設定
base_dict = load_yaml_file(base_path)
if base_dict is None:
return None
# 2. 讀取覆蓋設定 (若檔案不存在或為空,則視為錯誤)
if (overlay_dict := load_yaml_file(overlay_path)) is None:
return None
# 3. 使用 deepmerge 進行合併
# 這會保留 base_dict 中未被覆蓋的巢狀結構
return always_merger.merge(base_dict, overlay_dict)
if __name__ == '__main__':
if len(sys.argv) != 4:
print(f"Usage: python {sys.argv[0]} <values.yaml> <values-env.yaml> <output.yaml>")
exit(1)
base_yaml = sys.argv[1]
overlay_yaml = sys.argv[2]
output_yaml = sys.argv[3]
logger.info(f"Merging {overlay_yaml} into {base_yaml}...")
merged_data = merge(base_yaml, overlay_yaml)
if merged_data:
try:
with open(output_yaml, "w") as f:
# sort_keys=False 保持 YAML 的閱讀順序
yaml.dump(merged_data, f, sort_keys=False)
logger.info(f"Success! Merged file saved to: {output_yaml}")
except Exception as e:
logger.error(f"Failed to write output: {e}")
exit(1)
else:
logger.error("Merge failed due to input errors.")
exit(1)