SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-09-09

Ansible check mode 與 --diff dry run 怎麼看

了解 Ansible --check 與 --diff 真正能驗證什麼,以及不支援 check mode 的 module 為何不回報結果,導致 dry run 可能與正式執行不同。

Ansible check mode 的作用

Ansible check mode 是 dry run:ansible-playbook --check 會連線至 play 中的每部主機,要求每個 module 檢查目前狀態是否已符合指定狀態,並回報預計會有哪些變更,但不寫入任何內容。加入 --diff 後,也會列出預計修改之檔案的變更前與變更後內容。兩者合併後,可回答每次正式執行前都應先確認的問題:這些伺服器即將發生哪些變更?

Check mode 不是 playbook 的模擬。伺服器上不存在任何模型。每個 module 只會被要求進行檢查,而不是寫入資料。能以唯讀方式判斷的 module 會回報 changed,然後繼續執行。無法判斷的 module 不會執行任何動作,也不會回報任何內容。Ansible 文件用一句話說明這點:「不支援 check mode 的 module 不會回報任何內容,也不會執行任何動作。」dry run 可能因此提供錯誤結果;本指南大多內容都在說明這項落差。

執行 dry run:--check 與 --diff

ansible-playbook -i inventory.ini site.yml --check --diff --limit web1

-C-D 是這兩個旗標的簡寫。--limit 是刻意設定的。一台主機的 diff 易於閱讀。20 台主機的 diff 只會讓人一路捲過。

以下 4 個結果詞涵蓋整份報告。

  • ok: [web1] 表示模組已檢查,且目前狀態已符合要求。不會有任何變更。
  • changed: [web1] 表示模組會寫入內容。搭配 --diff 時,其上方的行會顯示變更內容。
  • skipping: [web1] 表示尚未評估該工作。原因可能是 when 的結果為 false,或模組無法在 check mode 中執行。
  • fatal: [web1] 表示檢查期間工作失敗。先閱讀訊息,再判斷 playbook 是否損壞。

--diff 會為檔案模組輸出 unified diff。刪除的行會以 - 標示,新增的行會以 + 標示。標頭中的行會以 --- before+++ after 開頭,並標示目的地路徑。不會寫入檔案的模組會輸出自己的前後狀態,因此 ansible.builtin.user 顯示的是即將變更的屬性,而不是檔案內容。

ansible.cfg 中永久啟用 diff,避免忘記設定此旗標:

[diff]
always = true
context = 5

check mode 前面應先執行 2 個成本較低的檢查。ansible-playbook site.yml --syntax-check 會解析 YAML 與 play 結構,不會連線到任何主機。ansible-playbook site.yml --list-tasks 會列出即將執行的工作。這能找出原以為已加上標籤、實際上卻沒有的 role。這兩個檢查都不會建立連線,因此會立即完成。

check mode 本身會建立連線。它會對 pattern 中的每台主機開啟 SSH 並蒐集 facts,因此主機離線時 dry run 會失敗。這本身就是有用的訊號;也因此,在將 dry run 放入 CI 前,應先處理判斷 playbook 應如何處理無法連線的主機

為什麼在全新的伺服器上檢查模式會失敗

這個 play 是正確的。使用 --check 在尚未安裝 nginx 的伺服器上執行時,其中大多數工作都會失敗。

- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present

- name: Write the site config
  ansible.builtin.template:
    src: site.conf.j2
    dest: /etc/nginx/conf.d/site.conf

- name: Start and enable nginx
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

apt 工作會回報 changed,而且這是正確的:套件不存在,因此實際執行時會安裝它。檢查模式不會安裝套件。接著 template 工作會失敗,因為此主機上不存在 /etc/nginx/conf.d/,也沒有任何工作建立它。service 工作同樣會失敗,因為沒有可供查詢的 nginx unit。這兩個失敗都不是 playbook 的錯誤。模擬執行缺少所需的狀態;文件所說的「如果工作輸入依賴前一個工作所做的變更,檢查模式無法產生有用輸出」,指的就是這種情況。

因此,這項規則更準確的說法是:對已由 playbook 完成收斂的主機,檢查模式的結果是準確的;對全新的主機,結果則會包含許多干擾訊息。--check 執行中每個工作都回報 ok,代表已完成收斂的主機確實不會發生任何變更。對全新的主機而言,--check 多半只表示主機尚未設定。當你針對 VPS 撰寫第一個 Ansible playbook時,預期第一次模擬執行會出現大量紅色錯誤,並以第二次執行的結果評估 playbook。

為何在檢查模式中略過 command 和 shell 工作

ansible.builtin.commandansible.builtin.shell 不知道您的 command 會執行什麼操作。沒有以唯讀方式執行任意 binary 的方法,因此 module 在檢查模式中會拒絕執行它。工作結果會包含 skipped: true,訊息為 Command would have run if not in check mode,而輸出會顯示 skipping: [web1]

module 文件將其檢查模式支援稱為「部分支援」,並指出可採用的替代方式是 createsremoves。為工作指定 creates 路徑後,檢查模式至少可以評估檔案測試:

- name: Extract the release bundle
  ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
  args:
    creates: /opt/app/bin/app

如果 /opt/app/bin/app 已存在,檢查模式會回報 Would not run command since '/opt/app/bin/app' exists,這是真正有效的結果。如果路徑不存在,則會得到 Command would have run if not in check mode,同樣是真正有效的結果。沒有 creates 時,該工作在 dry run 中就只會留下空白結果。

這種連帶影響比留下空白結果更嚴重。略過的工作仍會產生結果,但該結果是 skip result,且不包含 stdout key。接下來工作的條件在評估時會失敗,錯誤訊息接近 'dict object' has no attribute 'stdout'。您的 playbook 在實際執行時可以運作,卻會在 dry run 中失敗;這是整個功能中最令人困惑的失敗情況。

check_mode:false,以及它唯一適用的位置

check_mode: false 用在 task 上時,表示「即使在 --check 下,也要實際執行」。這是解決略過命令問題的方法,但只有在該 task 會讀取資料時才安全。

- name: Read the installed app version
  ansible.builtin.command: /usr/local/bin/app --version
  register: app_version
  check_mode: false
  changed_when: false

該 task 在兩種模式下都能正確運作。它會讀取版本且不會寫入資料,changed_when: false 可避免它回報未實際進行的變更,而 check_mode: false 會讓 app_version.stdout 在 dry run 期間存在,因此以它為依據建立的條件仍能完成評估。

在將這個關鍵字貼到其他位置前,請先按字面理解其用途。包含 check_mode: false 的 task 會在 ansible-playbook --check 期間寫入伺服器。將它加到 apt task 或 template task,只會讓 dry run 看起來更整齊,而你的 dry run 也不再是 dry run。若無法讓寫入資料的 task 安全執行,請改用條件加以保護:

- name: Apply the database migration
  ansible.builtin.command: /usr/local/bin/app migrate --apply
  when: not ansible_check_mode

ansible_check_mode 是 Ansible 設定的特殊變數,在 check run 期間其值為 true。反向的關鍵字也存在。check_mode: true 會讓 task 永遠固定在 check mode,即使在實際執行期間也一樣,因而將它轉為 drift probe:註冊結果後,若 changed 報告,表示主機已不符合 task 所要求的狀態。

為什麼工作每次執行都回報 changed

連續執行兩次 playbook,中間不做任何變更。第二次執行時,每個工作都應回報 ok。仍回報 changed 的工作表示其中一種情況:模組無法看到它所管理的狀態,或是傳入的輸入不穩定。這兩種問題都能修正,不能只將這些回報視為雜訊並加以抑制。

  • commandshell 若沒有 createsremoveschanged_when,每次都會回報 changed,因為模組無法判斷是否發生任何變更。加入 creates,或針對輸出中的字串設定 changed_when
  • ansible.builtin.file 搭配 state: touch 時,設計上每次都會回報 changed,因為觸碰檔案會更新其時間戳記。如果只想設定擁有者或模式,請使用 state: file
  • template 的渲染輸出會變動,就會在每次執行時重寫檔案。來自 ansible_date_time 的時間戳記、呼叫 now(),或每次重新產生的密碼,都會產生不同的位元組,因此模組正確回報變更。請將會變動的值移出範本。
  • ansible.builtin.user 搭配 password: "{{ pw | password_hash('sha512') }}" 時每次執行都會變更,因為 password_hash 每次呼叫都會隨機選取 salt,因此產生的雜湊永遠不會與 /etc/shadow 中已有的值相符。請傳入由穩定值衍生的明確 salt。
  • 套件模組上的 state: latest 只要有可用的升級,就會回報 changed。這是正確的行為,也正是 state: latest 會產生無法預測結果之 playbook 的原因。請使用 state: present,並明確執行升級。
  • ansible.builtin.unarchive 若指向沒有 creates 的 URL,就會重新擷取並重新解壓縮。請提供 creates 路徑。

--diff 是區分這些情況最快的方法。如果工作回報 changed,而差異顯示位元組有所不同,表示輸入不穩定。如果回報 changed,而差異完全沒有內容,表示模組無法表達它所進行的變更。這通常代表是 command 工作,或是只變更中繼資料的寫入操作,例如更新時間戳記。

不要使用 changed_when: false 來壓制吵雜的工作。它會抑制回報,因此 notify 永遠不會觸發,負責重新啟動服務的處理常式也不會執行。請直接修正工作。

縮小影響範圍:--limit、--tags 與 --step

Check mode 會顯示即將變更的內容。這些旗標則決定一次有多少台機器會套用變更。

--limit 會將 play 限制在 inventory 的部分主機上。它接受與 hosts: 相同的模式,因此 --limit web1--limit 'webservers:!web3' 都可使用。請將模式加上引號。在互動式 bash 工作階段中,未加引號的 ! 會觸發驚嘆號的 歷史擴充,shell 會在 Ansible 取得命令前先改寫命令。

在信任模式前,請先確認模式是否正確。ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts 會列出符合的主機,並在未連線到任何主機的情況下結束。沒有符合項目的模式是安全的,因為 Ansible 不會改用整個 inventory。它會顯示找不到符合主機模式的警告,接著輸出錯誤,指出主機與 --limit 不符合任何主機。先了解 inventory 檔案如何定義這些群組,才能讓模式的結果具備可預期性。

--tags deploy 只執行帶有指定標籤的工作,而 --skip-tags packages 會執行其他所有工作。--list-tags 會列出可用的標籤。當 play 的規模大到不適合每次全部執行時,標籤就能發揮作用;這也是 將長 playbook 拆分為 roles 的原因之一。

--start-at-task "Write the site config" 會從指定名稱的工作繼續執行失敗的工作階段。可用它進行復原,但必須了解代價:該工作之前的所有內容都會略過,包括設定 facts 或註冊變數的工作,而後續工作可能會讀取這些 facts 或變數。

--step 會在每個工作前提示,並等待你回答 yes、no 或 continue。這種方式速度較慢,但第一次執行具破壞性的操作時很適合使用,因為你可以在兩個工作之間停止,而不必等到執行二十個工作後才停止。

以序列方式推出變更

Ansible 預設會先對 play 中的每部主機執行同一項工作,再開始下一項工作。這樣速度很快,但也代表錯誤的工作會在同一秒內套用到整個主機群組。等你讀完錯誤訊息並按下 Ctrl-C 時,變更通常已經套用到所有主機。

serial 會將 play 分成多個批次。整個 play 會先對第一個批次執行,再處理下一個批次。

- name: Roll out the web tier
  hosts: webservers
  serial: [1, 5, "30%"]
  max_fail_percentage: 0
  tasks:
    - name: Deploy the release
      ansible.builtin.include_role:
        name: webapp

第一個批次只有 1 部主機。如果該主機順利完成,第二個批次會包含 5 部主機,之後的每個批次則包含 play 中 30% 的主機。max_fail_percentage: 0 會在批次中的任何主機失敗時立即結束 play,因此有問題的版本只會套用到 1 部主機。any_errors_fatal: true 是較強硬的版本,會在第一部主機失敗時結束所有主機的 play。

先對 1 部主機執行並非過度謹慎,而是有明確原因。Inventory 群組的狀態會逐漸產生差異。某部主機可能在其他主機加入 6 個月後才加入,因此執行不同的 distribution 版本、帶有他人手動安裝的服務,或採用不同的磁碟配置。Playbook 對整個群組而言可能正確,對那部主機卻可能錯誤;而且,針對已完成設定的主機進行 dry run 不會顯示這種問題。管理 Linux 伺服器群組 很大一部分工作,就是在變更影響異常主機之前找出它。

執行順序

  1. ansible-playbook site.yml --syntax-check 會在完全不連線的情況下,檢查 YAML 與結構錯誤。
  2. ansible-playbook site.yml --limit web1 --list-hosts 可確認模式是否符合預期。
  3. ansible-playbook site.yml --limit web1 --check --diff 是試執行。請閱讀差異內容。
  4. ansible-playbook site.yml --limit web1 --diff 會套用至該主機。
  5. 再次執行步驟 4。所有項目都應回報 ok。任何仍回報 changed 的項目,都必須先修正,才能套用至其餘主機。
  6. 現在對整個 inventory 執行 ansible-playbook site.yml --check --diff,即可取得有意義的結果,因為已完成收斂的主機不會產生輸出,剩餘內容才是真正的差異。

提醒步驟 3 一點。--diff 會將檔案內容輸出至終端機及 CI 工作日誌,因此,如果範本產生資料庫密碼,該密碼也會寫入日誌。請在該工作上設定 diff: false 以抑制輸出,或設定 no_log: true 以隱藏整個結果;此外,請將密碼本身放在 加密的 Ansible Vault 檔案 中,不要放在 repository 內。

FAQ

ansible-playbook --check 會變更伺服器上的任何內容嗎?

不會,只有一個例外,而且可由你控制。在 check mode 中,系統會要求每個模組回報結果,而不是寫入變更;無法執行這項操作的模組則不會回報,也不會執行任何動作。例外是 check_mode: false 工作關鍵字。即使在 --check 執行期間,這個關鍵字也會強制該工作實際執行。信任 dry run 前,請在 playbook 和 role 中搜尋 check_mode: false,並確認每個符合項目都只會讀取狀態。

--check 和 --diff 有什麼差異?

--check 決定是否實際執行變更。--diff 決定你能看到多少詳細資訊。單獨使用 --check 時,只會告訴你檔案將會變更。單獨使用 --diff 時,會套用變更並顯示變更的行。將兩者一起使用,取得實際可讀的 dry run;實際執行時也應保持 --diff 啟用,方法是在 ansible.cfg[diff] 下設定 always = true

為什麼我的 Ansible 工作每次執行都回報 changed?

因為模組無法查看它所管理的狀態,或你提供的值每次都不同。commandshell 預設總是回報 changed,除非加入 createschanged_when。搭配 state: touch 使用 file 時,狀態會依設計變更。若範本會產生時間戳記或新產生的密碼,每次產生的位元組都不同,因此檔案確實會被重新寫入。連續執行 playbook 兩次:第二次執行時仍然是 changed 的項目,就是需要修正的工作。

為什麼我的 command 和 shell 工作在 dry run 中會被略過?

因為沒有唯讀方式可以執行任意 command。在 check mode 中,command 模組會設定 skipped: true,並顯示訊息 Command would have run if not in check mode。加入 createsremoves,讓 check mode 改為評估檔案測試。對於只讀取狀態的工作,請將 check_mode: falsechanged_when: false 一起設定,讓 registered result 在 dry run 期間仍然存在,依賴該結果建立的條件也能繼續運作。

為什麼 check mode 在新伺服器上失敗,但在既有伺服器上可以通過?

因為 check mode 不會建立後續工作所依賴的狀態。對沒有 nginx 的主機執行 dry run 時,安裝工作會回報為 changed,接著在寫入 /etc/nginx/conf.d/ 的工作上失敗,因為該目錄從未建立。這是預期行為。Check mode 是用來偵測 playbook 已完成收斂之主機的漂移狀態,無法驗證第一次執行。在新主機上,先將 playbook 套用至一台機器,再查看第二次執行的結果。

#ansible#check-mode#idempotency#automation#safety