📚 Ansibleが理解できない理由はLinuxにあった【Ansible編】第7回:なぜエラー制御が破綻するのか
⓪ なぜShellの知識がないとAnsibleは壊れるのか
① なぜshellモジュールは危険なのか
② なぜmoduleを使うと安全になるのか
③ なぜ毎回changedになるのか
④ なぜ条件分岐が壊れるのか(Ansible編)
⑤ なぜ変数が意図通りに扱えないのか
⑥ なぜタスクの順序で壊れるのか
⑦ なぜエラー制御が破綻するのか
⑧ なぜ非同期・待機で失敗するのか
⑨ なぜPlaybookが読めないのか
⑩ なぜAnsibleを正しく設計できるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Ansible編】全体のまとめはこちら
「Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ」
📑 連載の移動
前の記事:【Ansible編】第6回 | 次の記事:【Ansible編】第8回
📋 目次
- 前回の振り返り(第6回)
- 逆引き辞典との連動
- はじめに
- 問題定義:不適切なエラー制御がもたらす運用リスク
- 構造解剖:Ansibleのエラー評価アルゴリズム
- 実証:エラー判定の仕様と運用リスクの検証
- 設計:failed_whenとlock/rescueによる例外処理
- 実証:failed_whenとblock/rescue/alwaysの適用
- 本質:エラーを「止める」設計と「流す」設計の使い分け
- まとめ
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Ansible編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Ansible編の第7回」になります。
1. 前回の振り返り(第6回)
前回 は、Ansibleのタスク実行がステートレス構造であることを解説しました。各タスクは独立したセッションで実行されるため、ディレクトリ移動や環境変数の設定は後続タスクに引き継がれません。
2. 逆引き辞典との連動
「設計上の原因」を特定したい場合は、以下の逆引き辞典を活用してください。
Ansibleが理解できない理由はLinuxにあった【Ansible編】:トラブル逆引き辞典(設計パターン版)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
3. はじめに
Ansibleでは、タスクが失敗するとそのホストへの後続処理は即座に中断されます。これはデフォルトの動作として合理的ですが、実務では「特定の戻り値は成功として扱いたい」「失敗時でも必ず後処理を走らせたい」といった要件が頻繁に発生します。
本記事では、Ansibleがタスクの成否を判定する仕組みを順を追って整理します。Shellの終了ステータスとAnsibleの failed 判定がどう異なるかを確認したうえで、failed_when や block/rescue を使った明示的なエラーハンドリングの設計手法を解説します。
4. 問題定義:不適切なエラー制御がもたらす運用リスク
エラー制御の設計が不十分なPlaybookは、以下のような運用トラブルを引き起こします。
- 正常系コマンドによる意図しない処理中断
grepのように、マッチしない場合に非ゼロを返すコマンドを使った際、運用上は正常な結果であるにもかかわらず、Ansibleがそれを「失敗」と判定してPlaybookを停止してしまうパターンです。 - エラーの放置による二次障害
ignore_errors: trueを安易に使った結果、前段のタスクが致命的なエラーで終わっているにもかかわらず処理が続行し、後続タスクでデータ不整合や別のエラーを誘発するパターンです。 - 不完全な状態の残留による冪等性の崩壊
タスクが途中で失敗した際、一時ファイルや中途半端な設定変更が残ったまま再実行すると、Playbookが予期しない挙動を起こすパターンです。
これらはいずれも、Ansibleのデフォルトの失敗判定ロジックと各種エラー制御機能の仕様を正確に把握できていないことが根本原因です。
第6セクションでは、具体的なPlaybookの挙動をもとに各パターンを実際に再現・検証します。
🔍 裏付けとなる公式ドキュメント
5. 構造解剖:Ansibleのエラー評価アルゴリズム
Ansibleがタスクを「失敗(failed)」と判定するプロセスは、モジュールの実行結果と条件式の評価によって決まります。
🔄 エラー判定と処理中断の流れ
📌 構造のポイント
-
失敗判定のロジック
デフォルトではrc != 0のタスクを失敗とみなしますが、failed_whenを指定した場合はその評価結果が優先されます。 -
実行中断の範囲
タスクが失敗すると、そのホストへの後続処理はスキップされます。ただし、他のホストの実行は継続されます。 -
ignore_errors の限界
ignore_errors: trueは処理の中断を防ぐだけです。タスク自体はfailedとして記録され続けます。 -
rescue ブロックの前提条件
rescueが実行されるのは、失敗したタスクがblock構文の内側にある場合に限られます。blockを使っていない通常のタスクが失敗した場合、rescueは動作しません。
🔍 裏付けとなる公式ドキュメント
6. 実証:エラー判定の仕様と運用リスクの検証
第4セクションで挙げた3つのトラブルについて、実際の環境で1つずつ再現・検証します。
【検証1】正常系コマンド(grep)による意図しない処理中断
grep で検索文字列が見つからない場合、Linuxの仕様として rc=1(非ゼロ)が返ります。「設定が存在しないことを確認できた」という運用上は正常な結果であっても、Ansibleがそれを失敗と判定してPlaybookを停止してしまう挙動を確認します。
【コントローラーノード側】
ファイル名: verify_grep_failure.yml
---
- name: Verify Grep Failure Behavior
hosts: test_servers
gather_facts: false
tasks:
- name: 1. 特定の設定(ERROR_TRIGGER)が含まれていないかチェック
ansible.builtin.shell: "grep 'ERROR_TRIGGER' /etc/hosts"
register: grep_output
- name: 2. 後続の正常なタスク
ansible.builtin.debug:
msg: "前段のチェックを通過したため、このタスクが実行されました。"
実行コマンド
ansible-playbook -i inventory.ini verify_grep_failure.yml
▼ 実行結果
PLAY [Verify Grep Failure Behavior] *********************************************************************************************************************************************************
TASK [1. 特定の設定(ERROR_TRIGGER)が含まれていないかチェック] *****************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": true, "cmd": "grep 'ERROR_TRIGGER' /etc/hosts", "delta": "0:00:00.020226", "end": "2026-06-07 12:36:46.192319", "msg": "non-zero return code", "rc": 1, "start": "2026-06-07 12:36:46.172093", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
fatal: [192.168.1.22]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": true, "cmd": "grep 'ERROR_TRIGGER' /etc/hosts", "delta": "0:00:00.028837", "end": "2026-06-07 12:36:45.147989", "msg": "non-zero return code", "rc": 1, "start": "2026-06-07 12:36:45.119152", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
192.168.1.22 : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
結果
/etc/hosts に ERROR_TRIGGER が存在しないことは、運用上「正常な状態」を確認できたことを意味します。しかし grep は検索文字列がヒットしない場合に rc=1 を返すLinuxの仕様があるため、Ansibleはこれをタスクの失敗と判定します。
ログの通り、両ノードで FAILED! が出力され、タスク2の debug はスキップされました。Playbookが意図せず中断される典型的なパターンです。
【検証2】ignore_errors の安易な使用による二次障害
ignore_errors: true で前段のエラーを見過ごした結果、後続タスクで別のエラーが発生する挙動を確認します。
【コントローラーノード側】
ファイル名: verify_ignore_errors_risk.yml
---
- name: Verify Ignore Errors Risk
hosts: test_servers
gather_facts: false
tasks:
- name: 1. 必要な資材(app_config.conf)のダウンロード
ansible.builtin.get_url:
url: "http://example.com/invalid_path/app_config.conf"
dest: "/tmp/app_config.conf"
ignore_errors: true
- name: 2. ダウンロードした設定ファイルを本番環境へ反映
ansible.builtin.copy:
src: "/tmp/app_config.conf"
dest: "/etc/myapp/app_config.conf"
remote_src: true
実行コマンド
ansible-playbook -i inventory.ini verify_ignore_errors_risk.yml
▼ 実行結果
PLAY [Verify Ignore Errors Risk] ************************************************************************************************************************************************************
TASK [1. 必要な資材(app_config.conf)のダウンロード(存在しないURLで失敗させる)] **********************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": false, "dest": "/tmp/app_config.conf", "elapsed": 12, "msg": "Request failed: <urlopen error [Errno -2] 名前またはサービスが不明です>", "url": "http://example.com/invalid_path/app_config.conf"}
...ignoring
fatal: [192.168.1.22]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": false, "dest": "/tmp/app_config.conf", "elapsed": 12, "msg": "Request failed: <urlopen error [Errno -2] 名前またはサービスが不明です>", "url": "http://example.com/invalid_path/app_config.conf"}
...ignoring
TASK [2. ダウンロードした設定ファイルを本番環境へ反映] **************************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": false, "msg": "Source /tmp/app_config.conf not found"}
fatal: [192.168.1.22]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": false, "msg": "Source /tmp/app_config.conf not found"}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=1
192.168.1.22 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=1
結果
タスク1は存在しないURLへのアクセスで失敗(FAILED!)しましたが、ignore_errors: true が設定されているためAnsibleはエラーを記録しつつ処理を続行し、ログの ...ignoring が示す通り後続タスクへ進みました。
その結果、タスク2でダウンロードされていないファイル(/tmp/app_config.conf)をコピーしようとし、Source /tmp/app_config.conf not found という別のエラーでPlaybookが停止しました。
エラーの原因(ダウンロード失敗)がその場で処理されず、無関係に見える後続タスクで問題が表面化するパターンです。発生源が隠蔽されるため、トラブルシューティングが困難になります。
【検証内容3】不完全な状態の残留による冪等性の崩壊
タスクが途中で失敗した際、一時ファイルが削除されずに残留します。この残留物をPlaybookが検知できない設計だと、再実行時に「すでに処理が走っている(またはエラー状態が残っている)」という事実を無視したまま処理が続行されてしまいます。この「検知の欠如が招く冪等性の崩壊」を確認します。
【コントローラーノード側】
ファイル名: verify_idempotency_collapse.yml
---
- name: Verify Idempotency Collapse
hosts: test_servers
gather_facts: false
tasks:
- name: 1. アップデート処理用の一時ロックファイルを排他生成
ansible.builtin.shell: "echo 'LOCK' > /tmp/update_process.lock"
- name: 2. メインのアップデート処理(不慮のエラーで必ず失敗するコマンド)
ansible.builtin.shell: "false"
実行コマンド(※1回目の実行)
ansible-playbook -i inventory.ini verify_idempotency_collapse.yml
▼ 実行結果(1回目)
PLAY [Verify Idempotency Collapse] **********************************************************************************************************************************************************
skipping: no hosts matched
PLAY RECAP **********************************************************************************************************************************************************************************
(ansible) [ansible@vbox workspace]$ ansible-playbook -i inventory.ini verify_idempotency_collapse.yml
PLAY [Verify Idempotency Collapse] **********************************************************************************************************************************************************
TASK [1. アップデート処理用の一時ロックファイルを排他生成] **********************************************************************************************************************************
changed: [192.168.1.22]
changed: [192.168.1.21]
TASK [2. メインのアップデート処理(不慮のエラーで必ず失敗するコマンド)] ********************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"changed": true, "cmd": "false", "delta": "0:00:00.018802", "end": "2026-06-07 13:02:49.114549", "msg": "non-zero return code", "rc": 1, "start": "2026-06-07 13:02:49.095747", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
fatal: [192.168.1.22]: FAILED! => {"changed": true, "cmd": "false", "delta": "0:00:00.007351", "end": "2026-06-07 13:02:47.771190", "msg": "non-zero return code", "rc": 1, "start": "2026-06-07 13:02:47.763839", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=1 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
192.168.1.22 : ok=1 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
【リモートノード側の確認】
- リモートノード1(192.168.1.21)
[ansibleuser@localhost ~]$ ls -l /tmp
-rw-r--r--. 1 ansibleuser ansibleuser 5 6月 7 13:02 update_process.lock
- リモートノード2(192.168.1.22)
[ansibleuser2@localhost ~]$ ls -l /tmp
-rw-r--r--. 1 ansibleuser2 ansibleuser2 5 6月 7 13:02 update_process.lock
結果(1回目)
タスク1でロックファイル(/tmp/update_process.lock)が作成されましたが、タスク2の失敗でPlaybookが停止したため、一時ファイルが削除されずに残りました。
▼ 実行結果(2回目・再実行)
実行コマンド(※原因調査後に全く同じコマンドで「再実行」した場合)
ansible-playbook -i inventory.ini verify_idempotency_collapse.yml
TASK [1. アップデート処理用の一時ロックファイルを排他生成] **********************************************************************************************************************************
changed: [192.168.1.22]
changed: [192.168.1.21]
TASK [2. メインのアップデート処理(不慮のエラーで必ず失敗するコマンド)] ********************************************************************************************************************
fatal: [192.168.1.22]: FAILED! => {"changed": true, "cmd": "false", "delta": "0:00:00.012446", "end": "2026-06-07 13:12:16.509818", "msg": "non-zero return code", "rc": 1, "start": "2026-06-07 13:12:16.497372", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
fatal: [192.168.1.21]: FAILED! => {"changed": true, "cmd": "false", "delta": "0:00:00.008831", "end": "2026-06-07 13:12:17.936969", "msg": "non-zero return code", "rc": 1, "start": "2026-06-07 13:12:17.928138", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=1 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
192.168.1.22 : ok=1 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
【リモートノード側の確認】
- リモートノード1(192.168.1.21)
[ansibleuser@localhost ~]$ ls -l /tmp
-rw-r--r--. 1 ansibleuser ansibleuser 5 6月 7 13:12 update_process.lock
- リモートノード2(192.168.1.22)
[ansibleuser2@localhost ~]$ ls -l /tmp
-rw-r--r--. 1 ansibleuser2 ansibleuser2 5 6月 7 13:12 update_process.lock
結果2回目
ロックファイルが残ったまま再実行した結果、タスク1は再びファイルの上書き(changed: true)を行いました。
これは、このPlaybookにロックファイルの存在を事前に確認する設計がないことが原因です。排他制御の文脈では、再実行時にロックファイルが残っていた場合、「前回の処理が異常終了した状態が残っている」と検知し、処理をスキップするか明示的にエラーを返す必要があります。
しかし今回のPlaybookにはその検知ロジックがないため、残留物を無視したままタスク1が実行され、タイムスタンプ(13:12)が更新されるだけで素通りしています。
「残留状態を検知できないまま意図しない上書きを繰り返す」設計は、何度実行しても同じ状態に収束するという冪等性を損ないます。
🔍 裏付けとなる公式ドキュメント
7. 設計:failed_whenとlock/rescueによる例外処理
第6セクションで確認した通り、デフォルトの rc != 0 判定や ignore_errors による強引なエラー無視は、意図しない処理中断や冪等性の崩壊を招きます。本番運用に耐えるPlaybookを構築するには、明示的な例外処理の設計が必要です。
ここでは中核となる2つの制御機能を整理します。
1. 失敗条件の再定義(failed_when)
failed_when は、タスクの失敗条件を任意に定義できる機能です。終了ステータス(rc)によらず、標準出力や変数の内容をもとに成否を上書きできます。
- name: 特定の出力が含まれる場合のみ失敗とみなす
ansible.builtin.shell: /usr/bin/check_system.sh
register: result
failed_when: "'CRITICAL_ERROR' in result.stdout"
📌 設計のポイント
-
判定の上書き:
failed_whenの条件が真になると、rc=0でも失敗とみなされます。逆に条件が偽であれば、rc=1でも成功として扱えます。 -
複数条件の評価: 複数の条件を組み合わせる場合は、
and / orを明示した単一式で記述します。
2. 構造的な例外処理(block / rescue / always)
プログラミングの try-catch-finally に相当する仕組みです。複数のタスクを block でグループ化し、エラー発生時のリカバリ処理を rescue、成否に関わらず実行する後始末を always に記述します。
- block:
- name: 1. 不安定な可能性のあるタスク
ansible.builtin.command: /usr/bin/deploy_app.sh
rescue:
- name: 2. 失敗時のロールバック処理
ansible.builtin.command: /usr/bin/rollback_app.sh
always:
- name: 3. 成功・失敗に関わらず実行(一時ファイルの削除など)
ansible.builtin.file:
path: /tmp/deploy.lock
state: absent
📌 設計のポイント
-
rescue ブロックの挙動:
block内のタスクが1つでも失敗すると、残りのタスクはスキップされrescueが実行されます。rescueが正常に完了すると、Ansibleはそのホストを「救済済み」とみなしてPlaybookの処理を継続します。 -
always ブロックの保証:
blockの成否やrescueの実行有無に関わらず、最後に必ず実行されます。一時ファイルの削除など、環境をクリーンな状態に戻すための処理を置く領域です。
第8セクションでは、これらを第6セクションの3つの課題に適用し、実際の挙動を検証します。
🔍 裏付けとなる公式ドキュメント
8. 実証:failed_whenとblock/rescue/alwaysの適用
第7セクションで解説した failed_when と block/rescue/always を第6セクションの3つの課題に適用し、実際の挙動を検証します。
【検証1の解決】failed_when による正常系の再定義
grep で文字列が見つからない状態(rc=1)を想定内の正常系として再定義し、後続タスクへ処理を継続できるかを検証します。
【コントローラーノード側】
ファイル名: resolved_failed_when.yml
---
- name: Resolve Grep Failure with failed_when
hosts: test_servers
gather_facts: false
tasks:
- name: 1. 特定の設定(ERROR_TRIGGER)が含まれていないかチェック
ansible.builtin.shell: "grep 'ERROR_TRIGGER' /etc/hosts"
register: grep_output
# rcが2以上(ファイルなし、構文エラーなど)の場合のみ失敗とし、1(ヒットなし)は許容する
failed_when: grep_output.rc > 1
- name: 2. 後続の正常なタスク
ansible.builtin.debug:
msg: "前段のチェックを正常に通過(rc=1を許容)したため、このタスクが実行されました。"
実行コマンド
ansible-playbook -i inventory.ini resolved_failed_when.yml
▼ 実行結果
PLAY [Resolve Grep Failure with failed_when] ************************************************************************************************************************************************
TASK [1. 特定の設定(ERROR_TRIGGER)が含まれていないかチェック] *****************************************************************************************************************************
changed: [192.168.1.22]
changed: [192.168.1.21]
TASK [2. 後続の正常なタスク(実行される)] **************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": "前段のチェックを正常に通過(rc=1を許容)したため、このタスクが実行されました。"
}
ok: [192.168.1.22] => {
"msg": "前段のチェックを正常に通過(rc=1を許容)したため、このタスクが実行されました。"
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.1.22 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
結果
failed_when: grep_output.rc > 1 を追加したことで、grep が rc=1 を返してもAnsibleは失敗と判定せず、正常終了(changed)として処理を継続しました。
第6セクションの検証1ではこの時点でPlaybookが中断されましたが、今回は判定ロジックが上書きされたため、タスク2の debug まで実行されていることがログから確認できます。
failed_when で非ゼロ終了を適切にコントロールすることで、Playbookを止めることなく完走(failed=0)させられることが実証できました。
【検証2の解決】stat モジュールによる事前確認と条件分岐
ignore_errors: true による強引なエラー無視をやめ、stat モジュールでファイルの存在を事前に確認し、when で分岐する設計に改善します。
【コントローラーノード側】
ファイル名: resolved_error_handling.yml
---
- name: Resolve Ignore Errors Risk with Conditional Task
hosts: test_servers
gather_facts: false
tasks:
- name: 1. 事前にリモート側に対象の設定ファイルが存在するか確認
ansible.builtin.stat:
path: "/tmp/app_config.conf"
register: config_stat
- name: 2. 設定ファイルが存在する場合のみ本番環境へ反映(存在しない場合はスキップ)
ansible.builtin.copy:
src: "/tmp/app_config.conf"
dest: "/etc/myapp/app_config.conf"
remote_src: true
when: config_stat.stat.exists
実行コマンド
ansible-playbook -i inventory.ini resolved_error_handling.yml
▼ 実行結果
PLAY [Resolve Ignore Errors Risk with Conditional Task] *************************************************************************************************************************************
TASK [1. 事前にリモート側に対象の設定ファイルが存在するか確認] ******************************************************************************************************************************
ok: [192.168.1.21]
ok: [192.168.1.22]
TASK [2. 設定ファイルが存在する場合のみ本番環境へ反映(存在しない場合はスキップ)] **********************************************************************************************************
skipping: [192.168.1.21]
skipping: [192.168.1.22]
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
192.168.1.22 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
結果
stat モジュールによる事前確認と when による条件分岐を組み合わせた設計に改善しました。
タスク1でファイルの存在を確認(ok)した後、ファイルが存在しないためタスク2はスキップ(skipping)されています。
第6セクションの検証2では不整合な状態のまま処理を強行してエラーを誘発しましたが、今回はエラーなく正常終了(failed=0)しています。
エラーを無視して流すのではなく、事前に状態を検知して安全なルートへ分岐する設計が、Playbookの信頼性を保つうえで重要です。
【検証3の解決】block/rescue/always による後始末と冪等性の維持
block/rescue/always を導入し、メイン処理が失敗した場合でも always でロックファイルを確実に削除し、冪等性を維持できるかを検証します。
【コントローラーノード側】
ファイル名: resolved_idempotency.yml
---
- name: Resolve Idempotency Collapse with Block/Rescue/Always
hosts: test_servers
gather_facts: false
tasks:
- block:
- name: 1. アップデート処理用の一時ロックファイルを排他生成
ansible.builtin.shell: "echo 'LOCK' > /tmp/update_process.lock"
- name: 2. メインのアップデート処理
ansible.builtin.shell: "false"
rescue:
- name: 3. 失敗時のリカバリ(ログへの記録など)
ansible.builtin.debug:
msg: "メイン処理でエラーを検知しました。rescueブロックで救済処理を実行します。"
always:
- name: 4. [後始末] 成功・失敗に関わらず一時ロックファイルを必ず削除
ansible.builtin.file:
path: "/tmp/update_process.lock"
state: absent
実行コマンド
ansible-playbook -i inventory.ini resolved_idempotency.yml
▼ 実行結果
PLAY [Resolve Idempotency Collapse with Block/Rescue/Always] ********************************************************************************************************************************
TASK [1. アップデート処理用の一時ロックファイルを排他生成] **********************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]
TASK [2. メインのアップデート処理(不慮のエラーで必ず失敗するコマンド)] ********************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"changed": true, "cmd": "false", "delta": "0:00:00.010625", "end": "2026-06-07 14:54:06.399950", "msg": "non-zero return code", "rc": 1, "start": "2026-06-07 14:54:06.389325", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
fatal: [192.168.1.22]: FAILED! => {"changed": true, "cmd": "false", "delta": "0:00:00.019686", "end": "2026-06-07 14:54:05.112192", "msg": "non-zero return code", "rc": 1, "start": "2026-06-07 14:54:05.092506", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
TASK [3. 失敗時のリカバリ(ログへの記録など)] **********************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": "メイン処理でエラーを検知しました。rescueブロックで救済処理を実行します。"
}
ok: [192.168.1.22] => {
"msg": "メイン処理でエラーを検知しました。rescueブロックで救済処理を実行します。"
}
TASK [4. [後始末] 成功・失敗に関わらず一時ロックファイルを必ず削除] *************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
192.168.1.22 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
【リモートノード側の確認】
- リモートノード1(192.168.1.21)
[ansibleuser@localhost ~]$ ls -l /tmp/update_process.lock
ls: '/tmp/update_process.lock' にアクセスできません: そのようなファイルやディレクトリはありません
- リモートノード2(192.168.1.22)
[ansibleuser2@localhost ~]$ ls -l /tmp/update_process.lock
ls: '/tmp/update_process.lock' にアクセスできません: そのようなファイルやディレクトリはありません
結果
タスク2が失敗した瞬間、Ansibleは残りの block 処理を中断してタスク3(rescue)へ分岐しました。
PLAY RECAP の failed=0 かつ rescued=1 が示す通り、Playbookは異常を検知しつつも例外処理によって正常系に復帰しています。
さらに、タスク2の失敗にもかかわらずタスク4(always)が確実に実行され、両ノードでロックファイルが削除されていることがリモート側の確認で実証されました。
途中で処理が失敗しても環境に残留物が残らず、何度再実行しても安全にやり直せる状態、すなわち冪等性を担保できる設計です。
🔍 裏付けとなる公式ドキュメント
9. 本質:エラーを「止める」設計と「流す」設計の使い分け
実機検証を通して確認した通り、Playbookのエラー制御の本質は「仕様上想定される非ゼロ終了」と「予期せぬ本当の異常」を明確に切り分けることにあります。
すべてのエラーを一括りに無視するのではなく、各事象の意味を理解したうえで適切にハンドリングするための設計指針を整理します。
1. 予期される状態には「流す」設計
grep や diff のように、仕様として非ゼロ(rc=1)を返すものの、インフラの文脈では正常とみなせるケースです。
-
設計のアプローチ:
failed_whenでAnsibleのデフォルト判定を上書きします。 -
検証からの学び:
rc=1を許容して後続処理へ流すことで、Playbookの不必要な中断を防ぎ、自動化のフローを維持できます。
2. 予期せぬ異常には「止める」または「先回りする」設計
想定外のサーバーダウンやネットワーク障害、前段タスクの致命的な失敗(ダウンロード失敗など)が該当します。
-
設計のアプローチ:
ignore_errors: trueによる強引なエラー無視をやめ、その場で処理を停止させるか、statモジュールで事前に状態を確認してwhenで分岐させます。 - 検証からの学び: ファイルが存在しない場合に後続タスクをスキップさせる設計にシフトすることで、データ不整合や二次エラーを防げます。
3. 副作用を伴う操作には「状態を戻す」設計
設定ファイルの書き換えやパッケージ更新など、システムの状態を変更するタスクが該当します。
-
設計のアプローチ: 副作用を伴う処理は
blockで囲み、rescueでリカバリ、alwaysで確実なクリーンアップを実装します。 -
検証からの学び: メイン処理が失敗しても
alwaysでロックファイルを削除(state: absent)することで、環境に残留物を残さず冪等性を維持できます。
🔍 裏付けとなる公式ドキュメント
10. まとめ
本記事では、Ansibleのエラー評価アルゴリズムを整理し、実機検証を通じて適切な例外処理の設計手法を解説しました。要点は以下の3点です。
-
終了ステータスと成否の整合
grepやdiffのように正常系で非ゼロを返すコマンドには、failed_whenで判定ロジックを上書きし、不必要な中断を防ぎます。 -
ignore_errors に頼らないエラー管理
ignore_errors: trueはエラーを隠蔽するだけです。statモジュールによる事前確認とwhenによる条件分岐で、安全なルートへ誘導します。 -
block/rescue/always による冪等性の維持
副作用を伴うタスクはblockで囲み、rescueでリカバリ、alwaysでロックファイルなどの後始末を確実に実行することで、何度でも安全に再実行できる状態を維持します。
エラー制御の本質は、想定内の非ゼロ終了を適切に流し、予期せぬ異常で安全に止め、失敗時には確実に元の状態へ戻す設計にあります。
11. 次回予告
本記事(第7回)では、エラー判定の仕組みと例外処理の設計手法を解説しました。
次回は「時間のかかる処理」への対応がテーマです。OSの再起動、大規模なパッケージ展開、外部APIの応答待ちなど、同期実行だけではタイムアウトや非効率を招く場面があります。
- なぜタイムアウトで処理が途切れるのか
- なぜバックグラウンド実行の結果が回収できないのか
Ansibleの非同期実行(async)とポーリング、待機(wait_for)の仕組みを整理します。
次回:【Ansible編】第8回:なぜ非同期・待機で失敗するのか
📑 連載の移動
前の記事:【Ansible編】第6回 | 次の記事:【Ansible編】第8回
📑 【Ansible編】全体のまとめはこちら
「Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ」
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📚12. 連載一覧:Ansibleが理解できない理由はLinuxにあった【Ansible編】
※以下随時更新
| 回数とタイトル | 内容(概要) |
|---|---|
| 【Ansible編】第0回:なぜShellの知識がないとAnsibleは壊れるのか | AnsibleはShellのラッパーとして動作している。Shell編で学んだ構造(Ansible → SSH → Shell → Linux)を再確認し、「なぜmoduleを使うべきなのか」の前提を整理する。 |
| 【Ansible編】第1回:なぜshellモジュールは危険なのか | shellモジュールはShellの仕様(分割・展開・環境)に依存するため壊れやすい。commandとの違いを理解し、「どこまでShellを許容すべきか」を判断できるようになる。 |
| 【Ansible編】第2回:なぜmoduleを使うと安全になるのか | file / copy / lineinfile などのmoduleは状態管理を前提としている。「結果」ではなく「状態」を扱うことで、Shell依存を排除し安全な構成を実現する。 |
| 【Ansible編】第3回:なぜ毎回changedになるのか | shellモジュールは状態を判定できないため、常にchangedになる。changed_whenを使った制御と冪等性(idempotency)の設計を理解する。 |
| 【Ansible編】第4回:なぜ条件分岐が壊れるのか(Ansible編) | when条件が意図通りに動かない原因は、rc / stdout / stderrの扱いにある。Shell編で学んだ終了ステータスと組み合わせ、正しい条件分岐を設計する。 |
| 【Ansible編】第5回:なぜ変数が意図通りに扱えないのか | vars / host_vars / group_vars のスコープや、registerの扱いを誤ると値が壊れる。Ansible内での変数管理と評価タイミングを整理する。 |
| 【Ansible編】第6回:なぜタスクの順序で壊れるのか | Ansibleのタスクはそれぞれ独立したプロセスとして実行される。chdir / environment の制御と、「前の結果に依存する設計」の危険性を理解する。 |
| 【Ansible編】第7回:なぜエラー制御が破綻するのか | ignore_errors や failed_when の使い方を誤るとエラーが隠蔽または暴発する。Shellのrcと連動させた正しいエラー制御を設計する。 |
| 【Ansible編】第8回:なぜ非同期・待機で失敗するのか | サービス起動や外部依存処理はタイミング問題を引き起こす。async / poll / wait_for / retries を使い、安定した実行制御を実現する。 |
| 【Ansible編】第9回:なぜPlaybookが読めないのか | role構成やタスク分割が不適切だと可読性が崩壊する。実務で保守できるPlaybook設計と構造化の原則を理解する。 |
| 【Ansible編】第10回:なぜAnsibleを正しく設計できるようになるのか | これまでの知識を統合し、Shell編と接続することで「壊れない設計」ができるようになる。Ansibleを実務で使いこなすための設計思想を完成させる。 |