完整範例、常見錯誤、工具鏈、風格指南
綜合運用單元 2–7:縮排、序列、環境變數、錨點合併與多行字串。以下是一次完整的實作,右欄逐段拆解:
x-app: &app
restart: unless-stopped
networks: [appnet]
services:
api:
<<: *app
build: .
ports:
- "8080:8080"
environment:
DB_URL: "postgres://db:5432/app"
LOG_LEVEL: info
db:
<<: *app
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
volumes:
db-data:
networks:
appnet:
x-app:用 x- 前綴定義「不是服務」的延伸節點,當錨點用。
<<: *app:合併錨點,兩個服務共用 restart 與 networks。
序列:ports、networks、volumes 都是序列。
環境變數:DB_URL 加引號(內含冒號),LOG_LEVEL plain 即可。
多行健康檢查:用 flow style 一行寫完 test。
volumes 空節點:db-data: 留空 = 空對映。
| 錯誤 | 後果 | 修正 |
|---|---|---|
| 用 Tab 縮排 | 解析錯誤 | 一律用空格 |
| 同層縮排不一致 | 結構錯亂或報錯 | 對齊同層級 |
| 冒號後沒空格 | 被當成一個字串 | key: value 加空格 |
值開頭是 # / - | 被當註解或序列 | 加引號 |
yes / on 當字串 | 變成布林 | 加引號,或寫 true |
| 「序列裡對映」未對齊 | 欄位落到錯誤層級 | - name 與後續鍵對齊 |
| 多行區塊縮排不足 | 解析失敗 | 內容縮排比鍵多 |
| 重複鍵 | 最後一個贏,藏 bug | 用 yamllint / 解析器偵測 |
讀 YAML 時,不要用「不安全」的 load。這非常重要:
# ❌ 危險:可執行任意程式碼(object constructor) yaml.load(data) # ✅ 安全:只載入純資料 yaml.safe_load(data) # 其他語言的對應作法: # Go → gopkg.in/yaml.v3 預設就安全 # Ruby → YAML.safe_load # Node → js-yaml safeLoad / 新版的 load
| 工具 | 用途 |
|---|---|
| yamllint | CLI 檢查縮排、行長、重複鍵——接 CI 最佳 |
| prettier | 自動排版、統一縮排 |
| yq | 命令列讀取/修改 YAML(類似 jq) |
| VS Code YAML 擴充 | 即時辨色、結構錯誤提示、schema 驗證(K8s / Compose 有官方 schema) |
| onlineyamltools | 線上轉 JSON、格式化 |
| yaml-language-server | 編輯器共用的語言伺服器(LSP) |
true / false;要字串就加引號「字串化」yes/no。.yaml 或 .yml 皆可,但同一個專案請統一。- [ ] 縮排全部用空格(兩個空格) - [ ] 所有冒號後都有空格 - [ ] 布林寫 true / false - [ ] 電話/代碼等已加引號 - [ ] 「序列裡的對映」有對齊 - [ ] 多行區塊縮排正確 - [ ] 沒有重複鍵 - [ ] 跑過 yamllint 無 error - [ ] 用解析器讀回並印出驗證
✅ 全部勾選 → 可以交付
❌ 任一未勾 → 修正後再提交
.yml / .yaml,用這 8 個單元的眼光讀一遍;再試著用 yamllint 檢查並修正它。以下是一份整合了單元 1–8 所有概念的生產級 compose.yaml,包含錨點、多行字串、型別安全、flow/block 混用與安全性考量:
# Production Compose — 綜合運用 x-logging: &log logging: driver: json-file options: max-size: "10m" max-file: "3" x-healthcheck: &hc healthcheck: interval: "30s | timeout: 5s | retries: 3 services: api: <<: *log build: . ports: ["8080:8080"] environment: NODE_ENV: production DB_HOST: "db" depends_on: db: {condition: service_healthy} db: <<: *log image: postgres:16-alpine volumes: - pgdata:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: "${DB_PASS}" # 從 .env 讀入 volumes: pgdata:
① x-logging + <<:*log:錨點共用 logging 設定
② ports: ["8080:8080"]:flow 序列,一行搞定
③ "${DB_PASS}":加引號防止空值問題
④ depends_on: {db: {condition:...}}:flow 對映嵌套
⑤ pgdata::留空 = 空對映(volumes 宣告)
關鍵設計決策:敏感值 DB_PASS 不寫死在 YAML 中,而是從 .env 檔注入("${DB_PASS}"),這符合安全性最佳實踐。ports 加引號是因為 "8080:8080" 內含冒號,不加引號會被 YAML 誤判。
大型 YAML 設定檔(如 Helm chart values.yaml、K8s operator CRD)的隱性成本包括:
<<:*base 的值從哪來?IDE 通常無法追蹤| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
Docker Compose 讀不到 .env 變數 | 變數未在 .env 檔中定義,或 docker compose 版本不同 | 用 docker compose config 預覽展開結果 |
| Git merge 衝突不斷 | YAML 空白敏感,自動格式化後差異大 | 統一格式(prettier)+ 限制 PR 大小 |
K8s kubectl apply 報隱性欄位錯誤 | 欄位名打錯但 YAML 語法合法 | 用 kubeval / kubeconform + schema 驗證 |
| Ansible playbook 執行時變數為空 | YAML null vs 空字串 "" 行為不同 | 明確區分 null(無值)與 ""(空字串) |
| CI 中 yamllint 與本地結果不同 | yamllint 版本或 config 不同 | 鎖定 yamllint 版本 + commit .yamllint 配置 |
最後一單元,把全部工具串成一套設定檔治理系統:一個 monorepo 內的所有 YAML,靠 pre-commit + CI + schema 三層自動把關。這是「最佳實踐」從個人習慣升級為團隊工程的完整示範:
platform-config/ ├── .pre-commit-config.yaml # pre-commit 掛鉤(本地門檻) ├── .yamllint # yamllint 設定 ├── .editorconfig # 縮排/換行統一 ├── configs/ │ ├── app.yaml # 應用設定 │ ├── compose.yml # Docker Compose │ └── k8s/ │ ├── deployment.yaml # K8s manifest │ └── service.yaml ├── schemas/ │ └── app.schema.json # 型別契約 └── Makefile # 統一指令入口
repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: - id: yamllint args: [--strict] - repo: https://github.com/koalaman/shellcheck-precommit rev: v0.10.0 hooks: - id: shellcheck
extends: default rules: indentation: {spaces: 2, indent-sequences: true} line-length: {max: 100} comments: {min-spaces-from-content: 2} key-duplicates: enable truthy: {allowed-values: [true, false]}
$ pre-commit run --all-files trailing-whitespace................Passed end-of-file-fixer...................Passed yamllint............................Passed shellcheck..........................Passed $ make lint yamllint -c .yamllint . (無輸出 = 通過) $ make validate check-jsonschema --schemafile schemas/app.schema.json configs/app.yaml PASS: configs/app.yaml
① 本地:pre-commit 在 commit 前擋下壞格式
② CI:make lint + make validate 在 PR 強制重跑
③ Schema:型別與結構契約(app.schema.json)
④ 統一入口:Makefile 讓所有檢查一條指令
⑤ 一致性:.editorconfig 統一縮排/換行
⑥ 版本鎖定:.yamllint + hook rev 都進版控
關鍵洞察:最佳實踐的價值在可重現性——本地與 CI 跑同一套檢查、同一版本、同一規則,才能保證「壞 YAML 進不了 main」。這比任何個人記憶可靠。
| 層級 | 工具 | 抓什麼 | 成本 |
|---|---|---|---|
| 風格層 | yamllint / prettier | 縮排、行長、重複鍵 | 毫秒級 |
| 結構層 | JSON Schema / kubeconform | 欄位、型別、必填 | 毫秒–秒級 |
| 行為層 | 解析 + assert / 乾跑 | 「跑起來對不對」 | 秒–分級 |
分配原則:越便宜的層級越要頻繁跑(pre-commit 每 commit),越貴的層級放 CI 的關鍵 PR 才跑。
POSTGRES_PASSWORD、API key、token 常被順手寫進 compose.yml。工具防線:gitleaks / trufflehog 掃描 repo 歷史、pre-commit 加 secret 掃描 hook、git-secrets 擋 commit。原則:YAML 永遠不放 secret,一律走環境變數 / secret 管理工具。pre-commit 有快取(repo 與環境快取),重複執行很快;CI 每次從零建環境。最佳化:① hook 只跑變更檔案(pre-commit 預設);② CI 用 paths 過濾;③ schema 驗證用 Python 快 (check-jsonschema) 而非起 container;④ 把 lint 放最便宜的 runner。
| 面向 | yaml | markdown | json | github | gitlab |
|---|---|---|---|---|---|
| 風格檢查 | yamllint | markdownlint / prettier | prettier / eslint 設定 | actionlint(workflow) | gitlab-ci-lint |
| 結構驗證 | JSON Schema / kubeconform | remark / 自訂規則 | JSON Schema 原生 | 無(平台語法檢查) | pipeline 編譯即驗證 |
| CI 整合 | pre-commit + Makefile + CI | documentation lint 工作 | JSON Schema CI 工作 | workflow 內建 | pipeline 內建 |
| secret 防護 | gitleaks / git-secrets | 無(純文件) | 同左 | secret scanning(內建) | secret detection(內建) |
| 自動排版 | prettier / yamlfmt | prettier | prettier | 無(手動) | 無(手動) |
--strict 跑通。