0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ansibleが理解できない理由はLinuxにあった【Shell編】第6回:なぜ条件分岐が失敗するのか(Exit Code)

0
Last updated at Posted at 2026-04-15

📚 連載:Ansibleが理解できない理由はLinuxにあった【Shell編】第6回:なぜ条件分岐が失敗するのか(Exit Code)

Shellの仕組みからAnsibleを理解するシリーズです。

① なぜShellを理解しないとAnsibleは使えないのか
② なぜAnsibleで出力が取得できないのか
③ なぜ結果が消えるのか
④ なぜgrepで見つからないのか
⑤ なぜ正規表現で壊れるのか
⑥ なぜ環境が違うのか
⑦ なぜ条件分岐が失敗するのか
⑧ なぜループがうまく動かないのか
⑨ なぜ途中で処理が止まるのか
⑩ なぜ変数が意図通りに展開されないのか
⑪ なぜAnsibleの挙動が読めるようになるのか


🗺️ 初めての方・シリーズの全体像を知りたい方はこちら

本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。

Ansibleが理解できない理由はLinuxにあった|統合ガイド


📑 【Shell編】全体のまとめはこちら
Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ


📑 連載の移動
前の記事:第5回 | 次の記事:第7回


📋 目次

  1. 前回の振り返り(第5回)
  2. 逆引き辞典との連動
  3. 問題定義:OS上では「正常」な挙動が、なぜAnsibleでは「失敗」になるのか
  4. なぜ条件分岐が失敗するのかshellの終わりの合図の仕組み
  5. 【Tips】判定を追跡するための「register」活用法
  6. 実務上の注意点:終了ステータスの誤認とエラーコード
  7. まとめ:判定の食い違いを防ぐためのデバッグ指針
  8. 次回予告
  9. 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】

※本記事の位置とシリーズ全体の関係を先に確認します。


:map: シリーズ全体構造(学習 × 問題解決)


本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。

この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。


📍 現在の位置

現在はこの図の「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は通常、エラーが発生したかどうかを判断するために、コマンドやモジュールのリターンコードを評価します。)

出典:Ansible Documentation - Error handling in playbooks


【テキスト図:判定のバトンタッチ】

[ 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)」の値が採用されるためです。今回のケースでは、以下の順序で処理が行われています。

  1. cat missing.txt が失敗し、内部的にエラーを出す。
  2. しかしパイプにより、空の結果が grep 'hello' に渡される。
  3. grep は「空の入力から 'hello' は見つからなかった」と判断し、正常に終了した上で自身のルール通り 1 を返す。
  4. Ansibleは grep の返した 1 を全体のステータスとして受信する。

テスト結果のまとめ:

パイプラインを使用すると、Ansibleが受け取る終了ステータスは左側ではなく最後のコマンドの値になります。 今回は grep1 を返したため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側の解釈」のズレを特定し、解消を図ります。

【デバッグのステップ】

  1. OS側の「戻り値」を直接確認する
    Ansibleを通す前に、対象ノードのShell上でコマンドを実行し、正常時・異常時にそれぞれ「どの数値(終了ステータス)」が返されるのかを正確に把握します。

  2. Ansibleが「受信した値」を特定する
    register で変数に格納した rcdebug モジュールで出力するか、実行時に -v オプションを付与して、Ansibleが実際に受け取っている生データを確認します。

  3. 判定の優先順位を整理する
    「0以外は一律失敗」というAnsibleの自動判定が、実務上の「想定内」の結果と衝突していないかを確認します。もし衝突している場合は、Ansibleに対して「何をもって本当の失敗とするか」を明示的に定義し直す必要があります。

結論:

タスクの成否をAnsibleのデフォルト判定に委ねるのではなく、「Shellが返す数値の意味」を正しく理解し、それに基づいた制御を行うこと。
この「数値への意識」を持つことが、予期せぬ停止を防ぎ、安定したPlaybookを書くための基本になります。


↑ 目次に戻る


8. 次回予告

ここまでで、Ansibleが受け取る「結果」を正しく判定する仕組みを整理しました。
しかし、単発の実行では問題なくても、「ループ(繰り返し)処理」に組み込んだ途端、意図しない挙動に悩まされるケースが多々あります。

その多くは、繰り返し処理の中で標準入力(stdin)などのデータの流れが干渉し、ロジックが複雑化することに起因します。

次回:
【Shell編】第7回:なぜループがうまく動かないのか

次章では、ループ内でのデータの扱い方を掘り下げ、AnsibleのループをShellレベルで安定させるための具体的な手法を解説します。


↑ 目次に戻る


📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ


📑 連載の移動
前の記事:第5回 | 次の記事:第7回


🗺️ 初めての方・シリーズの全体像を知りたい方はこちら

本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。

Ansibleが理解できない理由はLinuxにあった|統合ガイド


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の実行プロセスから逆算して分解できるようになる。実務で使える“読み方”を完成させる。

↑ 目次に戻る


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?