單元 2 · 基本語法

鍵值對、縮排、註解、空值、文件分隔

2.1 鍵值對 · Key-Value Pairs

YAML 最基本的單位是 鍵: 值。冒號後要加一個空格——這條鐵則沒有例外,也是新手第一個踩的坑。

原始碼 · YAML SOURCE
name: YAML 教學
author: OpenCode
units: 8
渲染結果 · EQUIVALENT JSON
{"name": "YAML 教學",
 "author": "OpenCode",
 "units": 8}
最常見錯誤:name:YAML 教學(冒號後沒空格)會被整個當成一個字串,而不是鍵值對。若你看到解析結果「key 消失」或鍵變長,八成是這個。

2.2 縮排 · Indentation(最重要)

巢狀層級靠縮排表達,這是 YAML 之所以好讀、也之所以難 debug 的原因。規則有三:同層縮排一致只能用空格、禁用 Tab慣用兩個空格

原始碼 · YAML SOURCE
app:
  database:
    host: db.example.com
  cache:
    ttl: 60
渲染結果 · EQUIVALENT JSON
{"app": {
    "database": {"host": "db.example.com"},
    "cache":    {"ttl": 60}}}
鐵律:同一層級縮排必須一致,且只能用空格、禁用 Tab。Tab 與空格混合是 YAML 報錯的頭號來源——因為「看起來」對齊了,但解析器用的是字元。編輯器通常會自動把 Tab 轉成空格,請確認設定。

2.3 註解 · Comments

# 開頭到行尾皆為註解,可佔一整行,也可放在行尾。註解是 YAML 打敗 JSON 的殺手級功能——設定檔需要解釋「為什麼」。

原始碼 · YAML SOURCE
# 這是註解
name: YAML 教學   # 行尾註解也行
# 注意:值開頭是 # 就不是註解,是字串,要加引號(單元 3)

2.4 空值 · Null

空值可以寫 null~,或直接留空(冒號後不寫值)。三者等價。

原始碼 · YAML SOURCE
a: null
b: ~
c:
d: null   # 三者等價
渲染結果 · EQUIVALENT JSON
{"a": null, "b": null,
 "c": null, "d": null}
陷阱:很多人以為「留空」表示「沒設定」——其實它是 null。對某些程式(例如 Python 的 None)行為相同,但字典合併、預設值覆寫的邏輯可能不同,是隱性 bug 來源。

2.5 文件分隔 · Document Separator

一個檔案可含多份文件,用 --- 分隔;... 表示文件結束。多數設定檔只用一份,但 frontmatter 就是利用檔首的 --- 區塊(Jekyll / Hugo / Obsidian),單元 7 會看到實例。

原始碼 · YAML SOURCE
---
first: 文件一
---
second: 文件二
...

多份文件在 Python 用 yaml.safe_load_all、在 yq 用 yq 'eval-all' 讀取。

2.6 Worked Example:從零寫一個設定檔

原始碼 · YAML SOURCE
# 應用程式設定
title: 我的服務
replicas: 3
features:
  auth: true
  cache: true
tags: [web, api]
notes: null
拆解 · ANATOMY

① 註解解釋用途
② 字串值
③ 整數 replicas: 3
④ 巢狀對映 features
⑤ flow 序列 [web, api]
⑥ 空值 null

練習:幫你手上的專案寫一個 config.yml,包含標題、三個以上鍵值、一層巢狀與一行註解,然後用 yaml.safe_load 讀回來檢查。

看完這單元你應該能說出:
  • 寫出鍵值對並遵守「冒號後加空格」的鐵則。
  • 說明縮排代表巢狀層級,且只能用空格、禁用 Tab。
  • 寫出註解與空值(null / ~ / 留空)的三種寫法。
  • 用 --- 分隔多份文件,並解釋 frontmatter 的原理。

延伸閱讀


進階真實情境 Worked Example:Ansible Inventory

Ansible 的 inventory 檔大量使用 YAML 基本語法——鍵值對、巢狀對映、多文件分隔、空值。以下是一份真實場景:

原始碼 · inventory.yml
---
# Ansible inventory — 生產環境
all:
  vars:
    ansible_user: deploy
    ansible_port: 22
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
    databases:
      hosts:
        db1.example.com:
          ansible_port: 5432
...
拆解 · ANATOMY

--- 文件分隔符
web1.example.com:鍵名含 .,plain string 合法
db1.example.com 下方覆寫 ansible_port
④ 留空的值(web1.example.com 後)= null
... 文件結束符

為什麼這樣設計而非替代方案:Ansible inventories 故意讓 host 名稱作為「無值的鍵」(等同 null),因為 host 本身就是標識——不需要值。用 --- / ... 包圍是 YAML 慣例,讓解析器明確知道這是一份完整文件。

深入原理擴充

Implicit vs Explicit Key

YAML 對映的鍵有兩種寫法:implicit key(plain string,不需要引號)與 explicit key(加引號)。在 block context 中,implicit key 只能是「單行的純量」——一旦鍵名含有 :#{}[], 等字元,就必須加引號變成 explicit key。例如 "key: with colon": value 在 YAML 1.2 是合法的。

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

誤解:「空行會影響 YAML 結構」——事實上空行(blank line)在 YAML 中完全無意義,只影響可讀性。解析器遇到空行會直接跳過。真正影響結構的是縮排的空格數,而不是行與行之間有沒有空行。

診斷式疑難排解表

症狀可能原因解決方案
mapping values not allowed here冒號後沒空格確認 key: value 冒號後至少一個空格
tab characters are not allowed混用 Tab 與空格設定編輯器展開 Tab,用 cat -A 檢查
while parsing a block mapping同層縮排不一致對齊同層級所有鍵的起始位置
解析器讀到意外的 ---檔案中間有 --- 被當成分隔符中間的 --- 要加引號或移除
值是 null 但預期有值冒號後換行沒寫值確認冒號後有內容,或用引號包住空字串

進階挑戰題

  1. 多文件操作:在同一個 .yml 檔中用 --- 分隔三份文件,分別是 dict、list、scalar。用 Python yaml.safe_load_all() 讀取並印出每份文件的 type()。
  2. 隱性 bug 偵測:故意在 YAML 中放一個「冒號後沒空格」的值,再放一個「正確寫法」的值。用 yaml.safe_load 讀取,比較兩者的差異。如果冒號後沒空格的值「看起來」是對的,怎麼發現它是錯的?
  3. 設計挑戰:設計一份「零陷阱」的 YAML 設定檔——所有值都用最安全的寫法(明確型別、必要時加引號),然後與一份「自然寫法」的版本對比,討論可讀性與安全性的權衡。

① 專案級端到端 Worked Example:Ansible 多主機佈建專案

基本語法(鍵值對、巢狀、多文件、空值)組合起來就是真實的部署專案。以下是一個用 Ansible 佈建三台主機的完整專案:

產出檔案樹

provisioner/
├── inventory/
│   └── production.yml      # 主機清單(多文件 + 巢狀 + 空值)
├── group_vars/
│   └── all.yml             # 全域變數(鍵值對)
├── playbooks/
│   ├── deploy.yml          # 主 playbook
│   └── healthcheck.yml     # 健康檢查
└── ansible.cfg             # INI 格式,非 YAML——工具混用是常態

關鍵檔案內容

inventory/production.yml
---
all:
  vars:
    ansible_user: deploy
    ansible_port: 22
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
    databases:
      hosts:
        db1.example.com:
          ansible_port: 5432
...
group_vars/all.yml
# 所有主機共用的變數
app_version: "1.4.2"     # 加引號防型別誤判
app_replicas: 3
log_level: info
features:
  - metrics
  - tracing
maintenance_mode: false
playbooks/deploy.yml
---
- name: 部署應用
  hosts: webservers
  tasks:
    - name: 確認變數已注入
      debug:
        msg: "版本 {{ app_version }}"
    - name: 部署容器
      community.docker.docker_container:
        name: app
        image: "myapp:{{ app_version }}"
        restart_policy: unless-stopped
驗證命令與輸出
$ ansible-playbook -i inventory/production.yml playbooks/deploy.yml --syntax-check
playbook: playbooks/deploy.yml

$ ansible-inventory -i inventory/production.yml --list | python3 -m json.tool | head -20
{
    "_meta": {
        "hostvars": {
            "db1.example.com": {"ansible_port": 5432}
        }
    },
    "all": {
        "children": ["databases", "webservers"]
    }
}
為什麼這樣設計

① host 名稱當「無值鍵」= null,是 inventory 慣例
--- / ... 標記文件邊界
"1.4.2" 加引號避免被當浮點數
④ playbook 是「序列裡的對映」,每條 task 是對映
ansible.cfg 是 INI——工具混用格式是常態

② 效能/品質/安全深度

品質:縮排是最大的自動化敵人的檢查面

在佈建自動化中,縮排錯誤可能不是「報錯」而是「部署到錯誤的機器群」:同層縮排差一格,任務就換了 hosts。品質防線:

安全:inventory 是密碼存放的雷區

安全提醒:不要直接把密碼/金鑰寫進 group_vars。Ansible 提供 ansible-vault encrypt 加密整個檔案;敏感值一律用 ansible-vault 加密並將解密密碼存進 CI secret,而不是進 repo。這樣 YAML 檔案仍可審查,但內容不可讀。

效能:inventory 檔案規模

幾千台主機的 inventory 解析與 hostvars 合併是有成本的——Ansible 會對每個 host 執行變數合併。實務上拆成 group_vars / host_vars 目錄,讓 Ansible 只載入需要的部分,而不是一支超長單檔。

③ 站際比較對照表

面向yamlmarkdownjsongithubgitlab
基本單位鍵值對 key: value段落 / 標題 / 列表物件 / 陣列issue / PR / commitissue / MR / pipeline
結構規則縮排即巢狀(禁 Tab)語法極少,結構由標記決定括號與逗號嚴謹branch / tag 命名規則stage / job 命名規則
註解# 原生支援HTML 註解或 frontmatter不支援PR 描述與 review 留言MR 描述與討論串
多文件/分隔--- 分隔多文件一個檔案一份文件不支援(單一文件)多 repo 靠子樹/子模組多 repo 靠 subgroup
空值表示null / ~ / 留空無此概念null無此概念無此概念

④ 互動式檢核清單