第一份 Playbook:把 shell 腳本翻譯成宣告式

· tech

#ansible#devops#automation

📑 目錄

上一篇說膠帶貼到第二次就該轉正——這篇就是轉正手續。Playbook 說穿了只是「把 ad-hoc 指令寫進 YAML 檔案」,但書的第四章真正想教的,是一個世界觀的切換:腳本描述步驟,playbook 描述狀態。看懂這個差別,後面所有語法都只是細節。

同一件事,兩種世界觀

書用一個最小例子開場。裝一台 Apache,shell 腳本這樣寫:

#!/bin/bash
apt-get update
apt-get install -y apache2
systemctl start apache2

Playbook 這樣寫:

---
- hosts: web
  become: true

  tasks:
    - name: 安裝 Apache
      apt: name=apache2 state=present update_cache=yes

    - name: 確保服務啟動且開機自啟
      service: name=apache2 state=started enabled=yes

行數差不多,但語意完全不同——腳本的每一行是動作,playbook 的每個 task 是斷言:

腳本:描述步驟 「依序做這些動作」 apt-get update apt-get install -y apache2 systemctl start apache2 中斷 = 卡在半套狀態 重跑 = 每一步都可能因「做過了」而爆炸 Playbook:描述狀態 「機器最後要長這樣」 apache2 已安裝 服務 started + enabled 設定檔內容 = 模板產出 每個斷言自帶現狀檢查(冪等) 中斷 = 重跑就好;跑幾次,結果都一樣
腳本的每一行是動作,中斷就留下半套狀態;playbook 的每個 task 是斷言,自帶「先檢查再動手」——這不是語法差異,是可以安心失敗的差異

這個差別的實際體感,就在 PLAY RECAP 的輸出裡:

PLAY RECAP *********************************************************
192.168.60.4  : ok=4  changed=3   ← 第一次:動手改了三件事
192.168.60.4  : ok=4  changed=0   ← 第二次:已在期望狀態,全部跳過

changed=0 就是冪等的證明——你可以把 playbook 排進 cron 定期重跑,把被手動亂改的機器拉回正軌,而不用擔心它「多做了什麼」。

骨架:play、task、handler

一份 playbook 由上到下就三層:play(對哪些機器、用什麼身分)、task(一連串狀態斷言,依序執行)、handler(被 notify 才執行的特殊 task)。Handler 是第一個值得停下來看的設計:

---
- hosts: web
  become: true

  handlers:
    - name: restart apache
      service: name=apache2 state=restarted

  tasks:
    - name: 部署主設定檔
      template: src=apache2.conf.j2 dest=/etc/apache2/apache2.conf
      notify: restart apache

    - name: 部署站台設定檔
      template: src=vhost.conf.j2 dest=/etc/apache2/sites-enabled/vhost.conf
      notify: restart apache
tasks(依序執行) 部署主設定檔 changed 部署站台設定檔 ok(內容沒變) 部署防火牆規則 changed notify ✓ ok 不觸發 ✗ notify ✓ handler: restart apache 被 notify 兩次,只執行一次 執行時機:play 結尾 設定沒變就不重啟;改了三個檔案,也只重啟一次——「重啟服務」該有的樣子
Handler 的兩條規則:只有 changed 的 task 會觸發 notify;同一個 handler 被 notify 再多次,也只在 play 結尾執行一次

這兩條規則把「什麼時候該重啟服務」這個維運判斷,直接寫死進工具的語意:設定沒變就不動它,改了五個檔案也只重啟一次。用 shell 腳本要自己維護 flag 變數才能做到;在 Ansible 裡它是預設行為。

執行與日常開關

ansible-playbook playbook.yml                  # 就這樣
ansible-playbook playbook.yml --limit web1     # 只打一台(先拿一台試!)
ansible-playbook playbook.yml --check          # 彩排:只回報會改什麼
ansible-playbook playbook.yml -v               # 看每個 task 的細節輸出

--limit--check 是上正式環境前的標準組合:先在一台機器上彩排,確認 changed 的項目跟你預期一致,再全面執行。

真實世界的 playbook 都是同一首歌

第四章後半用三個完整範例收尾——Rocky Linux 的 Node.js app server、Ubuntu 的 LAMP + Drupal、Ubuntu 的 Solr。細節各異,但骨架完全是同一個 pattern:

  1. 加套件來源(EPEL / Remi / apt repo)
  2. 裝套件(package / apt / yum)
  3. 鋪設定(template / copy / lineinfile,改了就 notify)
  4. 確保服務跑著(service: started enabled=yes)

外加兩個新面孔:vars_files 把變數抽出去(同一份 playbook,換個變數檔就是另一個環境),pre_tasks 在正式 tasks 前先跑(典型用途:先 apt update 快取)。看懂這個套路之後,讀任何人的 playbook 都像讀同一首歌的變奏——伺服器設定的花樣其實很少,少到可以標準化,這正是它適合被自動化的原因

反思

宣告式真正的紅利,是「可以安心失敗」

我最有感的不是 playbook 比腳本優雅,而是它改變了失敗的代價。部署腳本跑到一半斷線,是我做 backend 以來最討厭的時刻之一——機器卡在半套狀態,你得逐行對照腳本猜「做到哪了」,手動把它救回可重跑的起點。冪等把這件事變成:再跑一次就好。這跟後端設計的直覺完全相通——訊息會 at-least-once 重複投遞、API 要冪等鍵、排程要能重跑,分散式世界的預設就是「同一件事會再來一次」,所有不能安心重來的設計,遲早變成半夜的事故。組態管理只是把這條鐵律應用到伺服器上而已。

name: 是寫給三個月後的自己

Playbook 能當文件讀,是它比腳本高明的第二件事——但這件事不是自動發生的。我看過 task 全用 shell 模組、name 隨便寫的 playbook,那跟腳本一樣難讀。差別在紀律:name: 安裝 Apache 這種寫得像句子的斷言,串起來就是一份會自己執行的 runbook;新人問「這台機器上有什麼」,答案不是過期的 wiki,是 repo 裡這份跑得動的文件。我對團隊的要求也一樣:程式碼的註解可以少,但「意圖」必須留在某個跑得動、review 得到的地方——對 API 是測試案例,對機器就是 playbook 的 name 欄位。


腳本記錄你做過什麼,playbook 宣告機器該是什麼——前者會過期,後者每跑一次就驗證一次。