單元 6 · 錨點與別名

錨點、別名、覆寫、merge key 深入

6.1 原理:為什麼需要重用

寫設定檔時,常常有「好幾個地方用同一組值」——例如多個服務共用相同的 restart 策略、logging 設定或環境變數。YAML 用錨點(anchor)別名(alias)讓你定義一次、重複引用,改一處全部生效。

比喻:錨點就像「變數宣告」,別名就像「使用變數」。錨定一個節點,之後到處引用它。

6.2 錨點與別名 · Anchor & Alias

&名稱 定義錨點,*名稱 引用它——引用時完整複製一份該節點。

原始碼 · YAML SOURCE
defaults: &def
  timeout: 30
  retries: 3
api:
  *def
batch:
  *def
渲染結果 · EQUIVALENT
defaults: {timeout: 30, retries: 3}
api:      {timeout: 30, retries: 3}
batch:    {timeout: 30, retries: 3}
讀法:&def 把「這個節點」記名為 def*def 在別處「完整複製一份」。改 defaults 一處,兩處都更新。

6.3 別名之後的覆寫

別名 *def 複製整棵節點,無法在複製的同時加欄位——想加欄位就得用合併鍵。先看「不能覆寫」的版本:

別名限制 · LIMITATION
api: *def          # 就是 defaults 的副本,不能再加別的鍵
web: *def
  port: 8080   # ❌ 語法錯誤——別名節點之後不能再有內容

6.4 合併鍵 · Merge Keys <<

<< 把別名指向的對映「展開合併」進來,並可覆寫欄位——這是最實用的用法。

原始碼 · YAML SOURCE
base: &base
  image: node:20
  ports: [8080]

web:
  <<: *base
  ports: [3000]   # 覆寫
渲染結果 · EQUIVALENT
base: {image: "node:20", ports: [8080]}

web:  {image: "node:20",   # 繼承
        ports: [3000]}     # 被覆寫
重要限制:<< 合併鍵是YAML 1.1 的功能,不是 1.2 核心規格的一部分——但 Docker Compose、GitHub Actions 等主流工具都支援。若你的工具不支援,別名(*def)仍可完整複製整個節點。

6.5 序列也能用別名

錨點不只能存對映,任何節點都能錨定——純量、序列都行。

YAML · 原始碼
common_args: &args [--no-cache, --progress=plain]
build_a: *args
build_b: *args

6.6 實例:Docker Compose 共用環境

經典場景——多個服務共用同一組設定。用 x- 前綴定義「延伸節點」(不是服務)當錨點來源:

docker-compose 片段 · SNIPPET
x-common: &common
  restart: unless-stopped
  logging:
    driver: json-file

services:
  app:
    <<: *common
    image: myapp
  worker:
    <<: *common
    image: myworker

6.7 重用取捨與 Worked Example

錨點不是唯一選項——多數情況下「重複寫幾次」反而更直觀、更好 debug:

練習 · PRACTICE
defaults: &def
  retries: 5
  timeout: 60

alpha:
  <<: *def
  retries: 10     # 覆寫

beta:
  <<: *def
驗收 · ACCEPT

alpha = {retries: 10, timeout: 60}
beta = {retries: 5, timeout: 60}
👉 再試:把 << 改成 *def,會發生什麼?(不能加覆寫欄位)

判斷準則:錨點讓檔案更短,卻也讓「值從哪來」更難追蹤。團隊協作時,先確認大家都懂錨點語法再大量使用。
看完這單元你應該能說出:
  • 用 &name 定義錨點、*name 引用別名。
  • 說明合併鍵 << 的繼承與覆寫行為。
  • 指出合併鍵是 YAML 1.1 功能,需確認工具支援。
  • 判斷何時該用錨點、何時該重複寫或抽環境變數。

延伸閱讀


進階真實情境 Worked Example:多環境 Kubernetes 部署共用基底

企業級 K8s 設定檔常用錨點與合併鍵來定義「基底設定 + 環境覆寫」模式:

原始碼 · deployment.yml
# 基底設定
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"}
解析結果 · MERGED
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 錨點。

深入原理擴充

Merge Key 的解析順序

<< 合併鍵與其他鍵並列時,YAML 的解析順序是「先展開合併,再處理同層覆寫」。這意味著:

  1. 解析器先讀取 << 指向的錨點,將其所有鍵展開到當前對映
  2. 然後讀取同層的其他鍵,覆蓋展開後的同名鍵
  3. 如果 << 指向的是序列(多個錨點),左邊的先展開,右邊的後展開(後展開的覆蓋先展開的)

大家以為是這樣但其實不是……

誤解:「合併鍵 << 是深度合併(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 解析器不支援確認工具支援或改用重複定義
多個 << 合併順序混淆同層多個 <<,右邊覆蓋左邊只用一個 <<,或明確測試覆寫順序

進階挑戰題

  1. 深度合併陷阱:設計一份 base config 含三層巢狀(server: {http: {port: 80}, https: {port: 443}}),然後用 << 覆寫只改 http.port——結果 https 鍵還在嗎?用實際解析驗證,再討論如何避免。
  2. 錨點 vs 環境變數:同一個共用設定,比較三種方案:(a) YAML 錨點 <<、(b) Docker Compose x- 延伸節點、(c) 環境變數 ${VAR}。列舉各自優缺點。
  3. Billion Laughs 攻擊實作:用 YAML 錨點巢狀展開設計一個「10 層展開」的 YAML,計算展開後的記憶體佔用量。再用 safe_load 的限制機制防禦它。

① 專案級端到端 Worked Example:Helm chart 多環境部署專案

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        # 驗證管線

關鍵檔案內容

values.yaml(錨點基底)
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
values-prod.yaml(覆寫)
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 攻擊(錨點展開 DoS)

錨點最著名的安全問題是Billion Laughs:巢狀別名會讓記憶體指數爆炸。

攻擊示意 · EXPANSION BOMB
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 把關。

效能:錨點是省記憶體的技巧

相反地,錨點也能省記憶體:同一棵節樹被引用多次時,解析器只存一份(某些實作會做引用計數)。但對「設定檔」這種小檔案,效果可忽略——錨點的價值在可維護性,不在效能。

③ 站際比較對照表

面向yamlmarkdownjsongithubgitlab
重用機制錨點 & / 別名 * / 合併鍵 <<嵌入/引用圖片與文件無(需程式層合併)Dependabot / template repoCI template / include 語法
繼承概念合併鍵淺層繼承+覆寫CSS-like 樣式繼承(主題)無繼承workflow template / reusable workflowinclude + extends 關鍵字
變數注入${VAR}(執行期)frontmatter 變數無(純資料)env / secrets / ${{ }}variables / secrets / CI/CD vars
DoS 風險Billion Laughs(錨點展開)極低極低低(平台管控)低(平台管控)
追蹤難易錨點來源難追蹤引用明列無引用reusable workflow 需查來源include 路徑需查來源

④ 互動式檢核清單