📚 連載:Ansibleが理解できない理由はLinuxにあった【Shell編】第6回:なぜ条件分岐が失敗するのか(Exit Code)
Shellの仕組みからAnsibleを理解するシリーズです。
① なぜShellを理解しないとAnsibleは使えないのか
② なぜAnsibleで出力が取得できないのか
③ なぜ結果が消えるのか
④ なぜgrepで見つからないのか
⑤ なぜ正規表現で壊れるのか
⑥ なぜ環境が違うのか
⑦ なぜ条件分岐が失敗するのか
⑧ なぜループがうまく動かないのか
⑨ なぜ途中で処理が止まるのか
⑩ なぜ変数が意図通りに展開されないのか
⑪ なぜAnsibleの挙動が読めるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
📋 目次
- 前回の振り返り(第5回)
- 逆引き辞典との連動
- 問題定義:OS上では「正常」な挙動が、なぜAnsibleでは「失敗」になるのか
- なぜ条件分岐が失敗するのかshellの終わりの合図の仕組み
- 【Tips】判定を追跡するための「register」活用法
- 実務上の注意点:終了ステータスの誤認とエラーコード
- まとめ:判定の食い違いを防ぐためのデバッグ指針
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Shell編の第6回」になります。
📍 はじめに:この記事のスタンス
前回の第5回では、Ansibleから渡した文字列がShellによってどう解釈されるかを整理しました。これにより「意図した通りにコマンドを実行する」土台は整いました。
しかし、実行の制御ができても、判定において次のような問題が頻発します。
-
「grepで検索してヒットしなかっただけなのに、タスクが『FAILED』で赤色停止する」
-
「OS上では正常終了しているのに、Ansibleの
when句が期待通りに評価されない」 -
「エラーを無視して後続に繋げたいが、どの値を基準に条件分岐(スキップ)を組めばいいか分からない」
これらの問題は、Playbookのロジック以前に、「Shellが返す終了ステータス(exit code)」と「Ansibleの判定ルール」がどう連動しているかを把握していないことが原因です。
自動化における「成功」と「失敗」の境界線を明確にするために、本記事ではShellの終了ステータスの正体と、その伝播の仕組みを解明します。
⚠️ 注意事項
この記事の目的は、便利な設定オプションを紹介することではありません。
目的は、「OSが返した数値を、Ansibleがどのようなメカニズムで『失敗』と判定しているのか」を実機ログから正確に理解することにあります。
「なぜか止まる」という曖昧なデバッグから脱却するために、終了コードとAnsibleの判定がどう連動しているかを整理します。
1. 前回の振り返り(第5回)
前回の第5回では、コマンドが「どの環境で実行されるか(実行コンテキスト)」を学びました。
その結果、「どこで実行するか」は制御できるようになりました。
今回はその続きとして、
「実行結果がどう判定されるか」 に焦点を当てます。
2. 逆引き辞典との連動
具体的なエラー名から原因を特定したい場合は、以下の逆引き辞典を活用してください。
【保存版】Ansibleよくあるエラー一覧と原因まとめ(Shell編)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
このサイクルが、OS編から続くトラブル解決の最短ルートです。
3. 問題定義:OS上では「正常」な挙動が、なぜAnsibleでは「失敗」になるのか
実務において混乱を招くのが、OS側の実行結果とAnsible側の判定が一致しないケースです。以下の例を見てください。
【手動(OS上の操作)】
$ grep "nothing" test.txt
(何も表示されない。エラーメッセージも出ない)
$ echo $?
1 <-- Shellは「検索対象なし」としてステータス1を返している
【Ansible(shellモジュールでの実行)】
- name: 設定値の有無を確認
ansible.builtin.shell: grep "nothing" test.txt
register: result
# => 結果:fatal: [localhost]: FAILED!(赤文字でタスク停止)
実務上、「検索してヒットしない」ことはエラーではなく、一つの「想定内の結果」です。しかし、AnsibleはOSから返ってきた数値(終了ステータス)が 0 以外であれば、一律で「異常(FAILED)」と見なします。
この「数値の解釈」にズレがある状態でPlaybookを組むと、意図しないタスク停止や、後続の条件分岐がスキップされるといった問題が引き起こされます。次章では、この「数字のバトンタッチ」の仕組みを詳しく確認します。
4. なぜ条件分岐が失敗するのか:Shellの「終わりの合図」の仕組み
Linuxのすべてのプロセスは、終了時に必ず 0〜255の整数 を親プロセス(この場合はShell、その先はAnsible)に返します。これを「終了ステータス(exit code / return code)」と呼びます。
※慣例として「0 = 成功」「0以外 = 失敗」と扱われる
💡 技術的裏付け:Ansible公式による「失敗」の定義
- Ansibleの判定基準については、公式ドキュメントの以下のセクションに明記されています。
"Ansible normally has to evaluate the return code of commands and modules to fail a task if an error occurred."
(Ansibleは通常、エラーが発生したかどうかを判断するために、コマンドやモジュールのリターンコードを評価します。)
【テキスト図:判定のバトンタッチ】
[ 1. OS/Shell側 (Target Node) ]
実行: grep 'hello' test.txt
|
結果: 1件もヒットしなかった (想定内)
|
終了: exit 1 <-- Shellは「見つからない=1」を返すルール
|
============|==[ ネットワーク (SSH経由で数値を返却) ]============
|
[ 2. Ansible側 (Control Node) ]
|
受信: { "rc": 1, "stdout": "", "stderr": "" }
|
判定プロセス:
(A) ユーザーが 'failed_when' を定義しているか?
└ YES --> ユーザー定義の条件で判定 (上書き)
└ NO --> (B) へ
|
(B) 終了コード(rc) は 0 か?
└ YES --> 【 SUCCESS 】(ok / changed)
└ NO --> 【 FAILED 】(赤色で停止!)
この「0以外=即失敗」というAnsibleの自動判定が、実務上の「想定内の結果(検索ヒットなし等)」と衝突したとき、後続の when 句を評価する前にプレイブックが止まってしまうのです。
実際にコマンドがどのような数値を返し、Ansibleがそれをどう受け取っているのか、実機での挙動を確認してみましょう。
【検証1】grepの返答:0(成功)と 1(ヒットなし)の境界線
① 検証手順(リモートノード側)
検索対象となるテキストファイルを作成しておきます。
# 検証用ファイルの作成
[ansibleuser@localhost ~]$ echo "hello ansible" > test.txt
② Ansibleによる実行テスト
実行するPlaybook (case1_grep_rc.yml)
「ヒットあり」「ヒットなし」それぞれの状態で、Ansibleが受け取る rc(Return Code)の値を確認します。
---
- name: 検証1 grepが返す終了ステータスの確認
hosts: test_servers
gather_facts: false
tasks:
- name: パターンA:文字列がヒットする場合
ansible.builtin.shell: "grep 'hello' test.txt"
register: res_hit
- name: パターンB:文字列がヒットしない場合
ansible.builtin.shell: "grep 'goodbye' test.txt"
register: res_miss
ignore_errors: true
- name: 結果の比較
ansible.builtin.debug:
msg:
- "ヒットあり(rc): {{ res_hit.rc }}"
- "ヒットなし(rc): {{ res_miss.rc }}"
実行コマンド
ansible-playbook -i inventory.ini case1_grep_rc.yml
③ 検証結果
(ansible) [root@localhost workspace]# ansible-playbook -i inventory.ini case1_grep_rc.yml
PLAY [検証1 grepが返す終了ステータスの確認] *************************************************************************************************************************************************
TASK [パターンA:文字列がヒットする場合] ****************************************************************************************************************************************************
changed: [192.168.1.21]
TASK [パターンB:文字列がヒットしない場合] **************************************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"changed": true, "cmd": "grep 'goodbye' test.txt", "delta": "0:00:00.024562", "end": "2026-05-08 16:23:14.338807", "msg": "non-zero return code", "rc": 1, "start": "2026-05-08 16:23:14.314245", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring
TASK [結果の比較] ***************************************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": [
"ヒットあり(rc): 0",
"ヒットなし(rc): 1"
]
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
ログの読み解きと考察
-
現象:パターンB(ヒットなし)において、標準エラー(
stderr)は空であり、システム的なエラーメッセージは一切出力されていません。しかし、Ansibleはrc: 1を受け取った瞬間にfatal: ... FAILED!と判定し、タスクを赤色で停止させています(今回はignore_errorsで継続させています)。 -
理由:これは
grepコマンド自体の仕様によるものです。grepは「検索対象が見つかれば 0」「見つからなければ 1」を終了ステータスとして親プロセスへ返すルールを持っています。OSからすれば「検索した結果、見つからなかった」という正常な応答ですが、Ansibleは「0以外は異常」という一律のルールで判定を下すため、実行結果の解釈に乖離が生じます。
この「乖離」がある状態で判定プロセス(B)に進むと、たとえ後続に when 句を書いていたとしても、その評価前にプレイブックが停止してしまいます。
テスト結果のまとめ:
OSがプログラムごとに定義している「数値の意味」を考慮せずにAnsibleを実行すると、「動作としては正常(検索完了)なのに、自動化処理としては異常停止する」という矛盾が生じます。この数値(rc)の正体を正しく把握することが、安定したPlaybook作成の前提条件となります。
5. 【Tips】判定を追跡するための「register」活用法
第4セクションの検証でも使用した通り、Ansibleが受け取った「生の数値」を正確に把握するには、register 句が不可欠です。
「なぜか意図通りに when が動かない」という事態に直面した際は、まず以下の最短構成で、Ansibleの視点(変数の中身)を可視化することをお勧めします。
- name: コマンドを実行して結果を保存
ansible.builtin.shell: "grep 'target' /var/log/syslog"
register: res
ignore_errors: true # rcが1でもタスクを停止させず、デバッグを継続させる
- name: 生の終了ステータス(rc)を表示
ansible.builtin.debug:
var: res.rc
この res.rc に格納されている数字こそが、ShellがAnsibleに渡した「最終回答」です。
次章の「実務上の注意点」で詳しく見ますが、この数字はコマンドの書き方一つで簡単に上書きされてしまいます。デバッグ時には「OS上の挙動」と「この変数の中身」を照らし合わせることが、トラブル解決の最短ルートになります。
6. 実務上の注意点:終了ステータスの誤認とエラーコード
コマンドの組み合わせ方や実行環境の状態によって、Ansibleが予期せぬ判定を下すケースがあります。実機ログに基づき、注意すべき2つの挙動を確認します。
【ケース1】パイプライン実行時の終了ステータス
複数のコマンドを |(パイプ)で繋いだ際、Ansibleが受け取る終了ステータスが末尾のコマンドの値になる挙動を確認します。
① 検証手順(リモートノード側)
存在しないファイルを cat し、その出力を grep に渡す操作を想定します。
# ファイルが存在しないことを確認
[ansibleuser@localhost ~]$ ls missing.txt
ls: 'missing.txt' にアクセスできません: そのようなファイルやディレクトリはありません
② Ansibleによる実行テスト
実行するPlaybook (case3_pipe_status.yml)
---
- name: 検証2 パイプラインの終了コード
hosts: test_servers
gather_facts: false
tasks:
- name: 途中のコマンドが失敗するパイプライン
# cat は失敗するが、grep の結果が rc となる
ansible.builtin.shell: "cat missing.txt | grep 'hello'"
register: res_pipe
- name: 判定結果の確認
ansible.builtin.debug:
var: res_pipe.rc
実行コマンド
ansible-playbook -i inventory.ini case3_pipe_status.yml
③ 検証結果
(ansible) [root@localhost workspace]# ansible-playbook -i inventory.ini case3_pipe_status.yml
PLAY [検証2 パイプラインの終了コード] *******************************************************************************************************************************************************
TASK [途中のコマンドが失敗するパイプライン] *************************************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": true, "cmd": "cat missing.txt | grep 'hello'", "delta": "0:00:00.011115", "end": "2026-05-08 17:07:31.420511", "msg": "non-zero return code", "rc": 1, "start": "2026-05-08 17:07:31.409396", "stderr": "cat: missing.txt: そのようなファイルやディレクトリはありま せん", "stderr_lines": ["cat: missing.txt: そのようなファイルやディレクトリはありません"], "stdout": "", "stdout_lines": []}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
ログの読み解きと考察
-
現象:
catコマンドが「そのようなファイルはありません」というエラーを標準エラー(stderr)に出力しているにもかかわらず、Ansibleが受け取った最終的な終了ステータスはrc: 1となっています。 -
理由:Shellの仕様により、パイプライン全体の終了ステータスは「最後に実行されたコマンド(この場合は
grep)」の値が採用されるためです。今回のケースでは、以下の順序で処理が行われています。
-
cat missing.txtが失敗し、内部的にエラーを出す。 - しかしパイプにより、空の結果が
grep 'hello'に渡される。 -
grepは「空の入力から 'hello' は見つからなかった」と判断し、正常に終了した上で自身のルール通り 1 を返す。 - Ansibleは
grepの返した1を全体のステータスとして受信する。
テスト結果のまとめ:
パイプラインを使用すると、Ansibleが受け取る終了ステータスは左側ではなく最後のコマンドの値になります。 今回は grep が 1 を返したためAnsibleもFAILEDと判定しましたが、もし右側が「常に 0 を返すコマンド」だった場合、左側の失敗が見えないまま処理が継続されるリスクがあります。
【ケース2】コマンド未検出時のステータス:rc: 127
指定したコマンドがシステムに見つからない場合に返される特定の数値とその意味を確認します。
① 検証手順
ターゲットノードに存在しない架空のコマンドを実行します。
# コマンドが存在しないことを確認
[ansibleuser@localhost ~]$ my-custom-cmd
-bash: my-custom-cmd: コマンドが見つかりません
② Ansibleによる実行テスト
実行するPlaybook (case4_rc127.yml)
---
- name: 検証3 コマンド未検出の確認
hosts: test_servers
gather_facts: false
tasks:
- name: 存在しないコマンドを実行
ansible.builtin.shell: "my-custom-cmd"
register: res_notfound
ignore_errors: true
実行コマンド
ansible-playbook -i inventory.ini case4_rc127.yml
③ 検証結果
(ansible) [root@localhost workspace]# ansible-playbook -i inventory.ini case4_rc127.yml
PLAY [検証3 コマンド未検出の確認] ***********************************************************************************************************************************************************
TASK [存在しないコマンドを実行] *************************************************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": true, "cmd": "my-custom-cmd", "delta": "0:00:00.013947", "end": "2026-05-08 17:20:57.697523", "msg": "non-zero return code", "rc": 127, "start": "2026-05-08 17:20:57.683576", "stderr": "/bin/sh: 行 1: my-custom-cmd: コマンドが見つかりません", "stderr_lines": ["/bin/sh: 行 1: my-custom-cmd: コマンドが見つかりません"], "stdout": "", "stdout_lines": []}
...ignoring
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
ログの読み解きと考察
-
現象:終了ステータスとして
rc: 127が返却されました。標準エラー(stderr)には「コマンドが見つかりません」というOS(/bin/sh)からのメッセージが記録されており、Ansibleはこれを受けて即座にタスクをFAILEDと判定しています。 -
理由:
rc: 127は、Linuxにおいて「実行しようとしたコマンドがシステム内に見つからない(Command not found)」ことを示す共通のエラーコードです。これはコマンド実行そのものが失敗しているため、当然ながら後続の処理に必要な出力(stdout)も生成されません。
テスト結果のまとめ:
rc: 1(実行はされたが結果が不一致)とは異なり、rc: 127 は「実行環境の不備」を意味します。デバッグ中にこの数値を確認した際は、Playbookの条件分岐を修正するのではなく、まず「コマンドのスペルミス(タイポ)」や「対象ツールがインストールされているか」を最優先で確認すべきであるという、明確な切り分け基準になります。
7. まとめ:判定の食い違いを防ぐためのデバッグ指針
Ansibleの条件分岐やタスクの成否が意図通りに動かない場合、以下のステップで「OS側の数値」と「Ansible側の解釈」のズレを特定し、解消を図ります。
【デバッグのステップ】
-
OS側の「戻り値」を直接確認する
Ansibleを通す前に、対象ノードのShell上でコマンドを実行し、正常時・異常時にそれぞれ「どの数値(終了ステータス)」が返されるのかを正確に把握します。 -
Ansibleが「受信した値」を特定する
registerで変数に格納したrcをdebugモジュールで出力するか、実行時に-vオプションを付与して、Ansibleが実際に受け取っている生データを確認します。 -
判定の優先順位を整理する
「0以外は一律失敗」というAnsibleの自動判定が、実務上の「想定内」の結果と衝突していないかを確認します。もし衝突している場合は、Ansibleに対して「何をもって本当の失敗とするか」を明示的に定義し直す必要があります。
結論:
タスクの成否をAnsibleのデフォルト判定に委ねるのではなく、「Shellが返す数値の意味」を正しく理解し、それに基づいた制御を行うこと。
この「数値への意識」を持つことが、予期せぬ停止を防ぎ、安定したPlaybookを書くための基本になります。
8. 次回予告
ここまでで、Ansibleが受け取る「結果」を正しく判定する仕組みを整理しました。
しかし、単発の実行では問題なくても、「ループ(繰り返し)処理」に組み込んだ途端、意図しない挙動に悩まされるケースが多々あります。
その多くは、繰り返し処理の中で標準入力(stdin)などのデータの流れが干渉し、ロジックが複雑化することに起因します。
次回:
【Shell編】第7回:なぜループがうまく動かないのか
次章では、ループ内でのデータの扱い方を掘り下げ、AnsibleのループをShellレベルで安定させるための具体的な手法を解説します。
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
9. 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】
| 回数とタイトル | 内容(概要) |
|---|---|
| 【Shell編】第0回:なぜShellを理解しないとAnsibleは使えないのか | AnsibleはShellを通してコマンドを実行している。Ansible → SSH → Shell → Linux の構造を理解し、なぜShell理解が必須なのかを整理する。 |
| 【Shell編】第1回:なぜAnsibleで出力が取得できないのか | Ansibleでregisterが空になる・エラーが見えない原因はstdout / stderrの違いにある。Shellの出力構造を理解することで原因を特定できる。 |
| 【Shell編】第2回:なぜ結果が消えるのか | Ansibleで実行結果が見えなくなる原因はパイプ・リダイレクトによる出力先の変化にある。どこに出力が流れたのかを追うことで原因を特定できる。 |
| 【Shell編】第3回:なぜgrepで見つからないのか | Ansibleでgrepがヒットしない原因は検索対象ではなく入力(stdin)の問題にある。Shellの入力(ストリーム)構造を理解することで原因を特定できる。 |
| 【Shell編】第4回:なぜ正規表現で壊れるのか | Ansibleで検索や置換が壊れる原因は正規表現の評価ルールにある。文字列ではなくルールとして解釈される仕組みを理解し、リテラルの境界を学ぶ。 |
| 【Shell編】第5回:なぜ環境が違うのか | 手動では動くのにAnsibleで失敗する原因は実行環境(PATH・環境変数)の差分にある。Shellの実行コンテキストの違いを理解し、環境を制御する。 |
| 【Shell編】第6回:なぜ条件分岐が失敗するのか | Ansibleのwhen条件が意図通りに動かない原因はexit codeにある。Shellの終了ステータスの判定ロジックを理解することで、正しく条件を組める。 |
| 【Shell編】第7回:なぜループがうまく動かないのか | Ansibleで繰り返し処理が期待通り動かない原因はShellのループと入力・サブシェルの干渉にある。データの扱い方を理解し、ループを安定させる。 |
| 【Shell編】第8回:なぜ途中で処理が止まるのか | Ansible実行中に意図せず処理が止まる原因はShellのエラーハンドリング(set -e 等)にある。エラー伝播の仕組みを理解し、停止条件を制御する。 |
| 【Shell編】第9回:なぜ変数が意図通りに展開されないのか | Ansibleで変数が空になる・壊れる原因はShellのクォートと展開順序にある。値の保護と展開のルールを理解し、安定したコマンドを記述する。 |
| 【Shell編】第10回:なぜ値が分割されてしまうのか | スペースや改行で意図せず値が壊れる原因はShellの単語分割(word splitting)にある。IFSなどの内部処理の本質を理解し、データの整合性を守る。 |
| 【Shell編】第11回:なぜAnsibleの挙動が読めるようになるのか | これまでの知識を統合し、AnsibleのエラーをShellの実行プロセスから逆算して分解できるようになる。実務で使える“読み方”を完成させる。 |