錨點、別名、覆寫、merge key 深入
寫設定檔時,常常有「好幾個地方用同一組值」——例如多個服務共用相同的 restart 策略、logging 設定或環境變數。YAML 用錨點(anchor)與別名(alias)讓你定義一次、重複引用,改一處全部生效。
&名稱 定義錨點,*名稱 引用它——引用時完整複製一份該節點。
defaults: &def timeout: 30 retries: 3 api: *def batch: *def
defaults: {timeout: 30, retries: 3}
api: {timeout: 30, retries: 3}
batch: {timeout: 30, retries: 3}
&def 把「這個節點」記名為 def;*def 在別處「完整複製一份」。改 defaults 一處,兩處都更新。別名 *def 複製整棵節點,無法在複製的同時加欄位——想加欄位就得用合併鍵。先看「不能覆寫」的版本:
api: *def # 就是 defaults 的副本,不能再加別的鍵 web: *def port: 8080 # ❌ 語法錯誤——別名節點之後不能再有內容
<< 把別名指向的對映「展開合併」進來,並可覆寫欄位——這是最實用的用法。
base: &base image: node:20 ports: [8080] web: <<: *base ports: [3000] # 覆寫
base: {image: "node:20", ports: [8080]}
web: {image: "node:20", # 繼承
ports: [3000]} # 被覆寫
<< 合併鍵是YAML 1.1 的功能,不是 1.2 核心規格的一部分——但 Docker Compose、GitHub Actions 等主流工具都支援。若你的工具不支援,別名(*def)仍可完整複製整個節點。錨點不只能存對映,任何節點都能錨定——純量、序列都行。
common_args: &args [--no-cache, --progress=plain] build_a: *args build_b: *args
經典場景——多個服務共用同一組設定。用 x- 前綴定義「延伸節點」(不是服務)當錨點來源:
x-common: &common restart: unless-stopped logging: driver: json-file services: app: <<: *common image: myapp worker: <<: *common image: myworker
錨點不是唯一選項——多數情況下「重複寫幾次」反而更直觀、更好 debug:
${{ var }} / $VAR,執行期注入。defaults: &def retries: 5 timeout: 60 alpha: <<: *def retries: 10 # 覆寫 beta: <<: *def
✅ alpha = {retries: 10, timeout: 60}
✅ beta = {retries: 5, timeout: 60}
👉 再試:把 << 改成 *def,會發生什麼?(不能加覆寫欄位)
企業級 K8s 設定檔常用錨點與合併鍵來定義「基底設定 + 環境覆寫」模式:
# 基底設定 x-base: &base image: myapp env: - name: LOG_FORMAT value: json resources: requests: {cpu: "100m", memory: "128Mi"} services: staging: <<: *base replicas: 1 env: # 完整覆寫 env - name: LOG_FORMAT value: text - name: DEBUG value: "true" production: <<: *base replicas: 5 resources: requests: {cpu: "500m", memory: "512Mi"}
staging:
image: "myapp"
env: [{name:"LOG_FORMAT",
value:"text"},
{name:"DEBUG", value:"true"}]
replicas: 1
resources:
requests: {cpu:"100m",
memory:"128Mi"}
production:
image: "myapp"
env: [{name:"LOG_FORMAT",
value:"json"}]
replicas: 5
resources:
requests: {cpu:"500m",
memory:"512Mi"}
為什麼這樣設計而非替代方案:覆寫整個 env 序列(而非逐條 append)是因為 staging 與 production 的環境變數完全不同——合併鍵是「整欄位覆寫」而非「深度合併」,這避免了「不知道 base 有哪些 env 然後意外多出一條」的隱性 bug。如果需要深度合併,通常改用程式(Helm template / Kustomize)而非 YAML 錨點。
當 << 合併鍵與其他鍵並列時,YAML 的解析順序是「先展開合併,再處理同層覆寫」。這意味著:
<< 指向的錨點,將其所有鍵展開到當前對映<< 指向的是序列(多個錨點),左邊的先展開,右邊的後展開(後展開的覆蓋先展開的)<< 是深度合併(deep merge)」——其實它只是淺層合併(shallow merge):只在最外層鍵級別覆寫。如果 base 有 resources: {limits: {cpu: "100m"}},你在覆寫時只寫 resources: {limits: {memory: "256Mi"}},cpu 會消失,因為整個 resources 被替換。想要深度合併,YAML 原生做不到,需要用 Helm/Kustomize 等工具。| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
<< 合併後某個鍵「消失」 | 淺層合併,覆寫時替换了整個父鍵 | 列出覆寫時要保留的所有子鍵 |
*alias 之後不能再加鍵 | 別名是完整節點替換,不支援後續追加 | 改用 <<: *alias 合併鍵 |
unknown anchor 錯誤 | 引用的錨點名稱不存在或在引用之後才定義 | 錨點必須在引用之前定義(文件中位置在前) |
| 合併鍵在工具中不支援 | << 是 YAML 1.1 功能,部分純 1.2 解析器不支援 | 確認工具支援或改用重複定義 |
多個 << 合併順序混淆 | 同層多個 <<,右邊覆蓋左邊 | 只用一個 <<,或明確測試覆寫順序 |
server: {http: {port: 80}, https: {port: 443}}),然後用 << 覆寫只改 http.port——結果 https 鍵還在嗎?用實際解析驗證,再討論如何避免。<<、(b) Docker Compose x- 延伸節點、(c) 環境變數 ${VAR}。列舉各自優缺點。safe_load 的限制機制防禦它。Helm 是「錨點 + 模板」的完美教材:values.yaml 用錨點定義共用基底,模板用 Go template 展開,環境覆寫用 values-*.yaml 分層。錨點讓「改一處,全部生效」變成可維護的工程實務:
myapp-chart/ ├── Chart.yaml # chart 中繼資料 ├── values.yaml # 預設值(錨點來源) ├── values-dev.yaml # 環境覆寫 ├── values-prod.yaml # 環境覆寫(型別安全:全字串) ├── templates/ │ ├── deployment.yaml # Go template + 值引用 │ └── _helpers.tpl # 共用 helper └── .github/workflows/ └── chart-ci.yml # 驗證管線
x-logging: &logging driver: json-file options: max-size: "10m" x-resources: &res requests: {cpu: "100m", memory: "128Mi"} app: replicas: 1 logging: <<: *logging resources: <<: *res
app: replicas: 5 resources: requests: {cpu: "500m", memory: "512Mi"} logging: options: max-size: "100m" # 只覆寫一層
$ helm lint myapp-chart -f myapp-chart/values-prod.yaml ==> Linting myapp-chart [INFO] Chart.yaml: icon is recommended 1 chart(s) linted, 0 chart(s) failed $ helm template myapp myapp-chart -f myapp-chart/values-prod.yaml | yq '.spec.replicas' 5 $ helm-docs # 從 values.yaml 產生 README 表格 README.md generated
① x- 節點:Helm 慣例,錨點來源但不直接輸出
② <<: *logging:合併基底,可再覆寫
③ 環境覆寫分層:prod 只改 replicas 與資源
④ helm lint:語法 + 模板檢查
⑤ helm template:展開後用 yq 驗證實際值
⑥ helm-docs:values.yaml 的型別/預設值自動化文件
關鍵洞察:錨點在此負責「共用基底」,環境差異交給 Helm 的 values 覆寫——兩者分工,values.yaml 保持單一事實來源,values-prod.yaml 只寫差異。
錨點最著名的安全問題是Billion Laughs:巢狀別名會讓記憶體指數爆炸。
a: &x [x,x,x,x] b: &y [*x, *x, *x, *x] c: [*y, *y, *y, *y] # 4×4×4 = 64 元素;10 層就是 4^10
safe_load(不展開任意物件);② 限制輸入大小與別名深度;③ 部分解析器(ruamel)有 alias 上限;④ 對不可信 YAML 先做檔案大小/內容檢查再解析。錨點讓「值從哪來」變難追蹤——*base 背後是哪一段?品質策略:錨點只用於真正的共用基底(team 都認識的片段),不要為單一用途建立錨點;命名要有語意(&logging 優於 &x);並用 helm lint / yamllint 在 CI 把關。
相反地,錨點也能省記憶體:同一棵節樹被引用多次時,解析器只存一份(某些實作會做引用計數)。但對「設定檔」這種小檔案,效果可忽略——錨點的價值在可維護性,不在效能。
| 面向 | yaml | markdown | json | github | gitlab |
|---|---|---|---|---|---|
| 重用機制 | 錨點 & / 別名 * / 合併鍵 << | 嵌入/引用圖片與文件 | 無(需程式層合併) | Dependabot / template repo | CI template / include 語法 |
| 繼承概念 | 合併鍵淺層繼承+覆寫 | CSS-like 樣式繼承(主題) | 無繼承 | workflow template / reusable workflow | include + extends 關鍵字 |
| 變數注入 | ${VAR}(執行期) | frontmatter 變數 | 無(純資料) | env / secrets / ${{ }} | variables / secrets / CI/CD vars |
| DoS 風險 | Billion Laughs(錨點展開) | 極低 | 極低 | 低(平台管控) | 低(平台管控) |
| 追蹤難易 | 錨點來源難追蹤 | 引用明列 | 無引用 | reusable workflow 需查來源 | include 路徑需查來源 |
&anchor / *alias / <<: *alias 三種用法,並預測解析結果。helm template 驗證。helm lint / yamllint 接進 CI,防住「錨點誤用」進 main。