Playbook 進階:變數、條件,與重新定義成功
· tech
📑 目錄
上一篇的 playbook 是死的:寫死的套件、寫死的路徑,只能打一種環境。第五章一口氣補上讓它活起來的所有機關——變數、條件、錯誤處理。內容很雜,但整理之後其實就三個主題:資料從哪來(變數、facts、register)、怎麼做決策(when)、怎麼定義成敗(changed_when / failed_when / blocks)。
變數:同一份 playbook,吃下所有環境差異
變數讓「dev 和 prod 各養一份 playbook」這種災難不會發生——邏輯只有一份,差異全部抽進變數。但變數可以定義在很多地方,同名時誰贏?Ansible 有一張十幾層的優先序表,背它沒意義,記住結構就好:越靠近執行當下、越具體的定義,優先權越高。
實務上的收斂用法:通用預設放 group_vars/all.yml,環境差異放 group_vars/prod.yml,個別機器的例外放 host_vars/,救火時用 -e "app_version=1.2.3" 從命令列強制覆寫——-e 是逃生門,不是日常入口。
除了你自己定義的,還有兩種「機器告訴你」的變數:
- Facts:執行 playbook 時 Ansible 先跑
setup模組,蒐集機器的自我介紹——ansible_os_family、ansible_memtotal_mb、IP、磁碟。跨發行版的 playbook 就靠它:when: ansible_os_family == "Debian"走 apt、RedHat 走 yum。 register:把某個 task 的輸出存成變數,給後面的 task 用。
條件:when + register,playbook 學會看情況
書裡的範例很實際——app 沒在跑才啟動它:
- name: 檢查 app 是否已在執行
command: forever list
register: forever_list
changed_when: false # 純查詢,永遠不該算 changed
- name: 啟動 app
command: forever start /path/to/app.js
when: "'app.js' not in forever_list.stdout"
register 接住輸出、when 判斷要不要做——這對組合就是 playbook 的 if。注意第一個 task 的 changed_when: false:查詢類指令永遠不該把 RECAP 弄髒,這是保住「changed=0 等於沒動任何東西」這個信任的紀律。
重新定義成功:changed_when、failed_when、blocks
command / shell 模組預設用 exit code 判斷成敗——但 exit code 會說謊:有些工具失敗照樣回 0,錯誤訊息藏在輸出裡。這時候用 failed_when 把「失敗」重新定義成業務語意:
- name: 執行資料庫遷移
command: /opt/app/migrate.sh
register: migrate_result
failed_when: "'ERROR' in migrate_result.stdout"
ignore_errors: false
而多個 task 的錯誤處理,交給 block / rescue / always——如果你寫過任何後端語言,這張圖不用解釋:
Vault:祕密也能進版本控制
變數檔一旦包含資料庫密碼、API key,「進 repo」就從理所當然變成資安事件。Ansible Vault 的解法直接:把變數檔加密後放進 repo,執行時再供鑰匙:
ansible-vault create secrets.yml # 建立加密變數檔
ansible-vault edit secrets.yml # 編輯(自動解密再加密)
ansible-playbook main.yml --ask-vault-pass # 執行時輸入密碼
ansible-playbook main.yml --vault-password-file ~/.vault_pass # 或用檔案/腳本供鑰
於是祕密有了跟程式碼一樣的待遇:有版本、有 review、有出處——而不是散落在誰的 .env、聊天紀錄和交接文件裡。
其他遲早會用到的機關
tags:幫 play / task 貼標籤,--tags "deploy"只跑一角——playbook 長大之後的剛需。delegate_to:這個 task 去別台機器上執行——經典用法:滾動更新前,先到 load balancer 上把這台摘掉。wait_for:等 port 開、等檔案出現再繼續——服務「啟動指令送出」和「真的能服務」中間的那段空窗,就靠它補。vars_prompt:執行時互動式問值,適合偶爾跑、每次參數不同的操作型 playbook。
反思
工具給你十層,不代表你要用十層
變數優先序表有十幾層,我的第一反應不是「好強大」,而是「這會被玩壞」。同名變數散在五個地方,查一個值要開五個檔案——這跟程式裡濫用全域變數、多層繼承是同一種病:每多一層彈性,就多一層讀者的成本。我會給團隊收斂成慣例:變數只准出現在 group_vars、host_vars、-e 三個地方,role defaults 只放真正的預設值,其他層一律不准用。這件事跟我治理 coding style 的體會一致——工具的能力範圍是廠商決定的,但團隊實際使用的子集,是 lead 該畫的線;線畫得好,新人讀專案的成本直接砍半。
exit code 0 不等於成功,HTTP 200 也不等於
changed_when / failed_when 表面上是小工具,背後是一個誠實的承認:工具只懂傳輸語意,不懂業務語意。exit code 0 只代表「程式正常結束」,不代表「事情做對了」——這跟後端每天遇到的「HTTP 200 但 body 裡是 error code」一模一樣,我就被第三方 API 的 200 + errcode: 40001 教育過:監控全綠、功能全掛。所以我現在寫任何整合——不管是 API client 還是 playbook task——第一個問題都是:「成功」由誰定義?用什麼欄位判斷? 把這個判斷寫明(failed_when、或 API client 裡的 response validator),等於把你對外部系統的理解固化成程式;沒寫,就是默默接受「別人說 OK 就是 OK」——那不是信任,是沒設防。
變數讓一份 playbook 走遍所有環境,條件讓它看情況行動——但最重要的機關是 failed_when:成功的定義,永遠該由你寫下,而不是由 exit code 決定。