📚 連載:Ansibleが理解できない理由はLinuxにあった【Shell編】第8回:なぜ途中で処理が止まるのか(Error Handling)
※Shellの仕組みからAnsibleを理解するシリーズです。
① なぜShellを理解しないとAnsibleは使えないのか
② なぜAnsibleで出力が取得できないのか
③ なぜ結果が消えるのか
④ なぜgrepで見つからないのか
⑤ なぜ正規表現で壊れるのか
⑥ なぜ環境が違うのか
⑦ なぜ条件分岐が失敗するのか
⑧ なぜループがうまく動かないのか
⑨ なぜ途中で処理が止まるのか
⑩ なぜ変数が意図通りに展開されないのか
⑪ なぜAnsibleの挙動が読めるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
📋 目次
- 前回の振り返り(第7回)
- 逆引き辞典との連動
- 問題定義:エラーを無視したいのに止まる、止めたいのに進む矛盾
- 【実機検証】ignore_errorsを貫通する「Shellの自己停止」を追う
- メカニズム解説:AnsibleとShellプロセスの制御境界
- 観測方法:停止位置をShellとAnsibleの両面から特定する
- 実務における注意点:エラー検知を阻害する構成
- まとめ:処理を確実に実行・制御するための「調査および設計の手順」
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Shell編の第8回」になります。
📍 はじめに:この記事のスタンス
前回の第7回では、Ansibleのループ処理において標準入力(stdin)の競合が発生し、処理が正常に実行されない現象について解説しました。
ループの実行が安定した後、次に直面する課題は「エラー発生時の制御」です。実務では以下のような事象が頻繁に発生します。
-
シェルスクリプト内でエラーが発生しているにもかかわらず、Ansible が「SUCCESS」と判定して処理を継続してしまう
-
許容可能なエラーであるにもかかわらず、Ansible がタスクを「FAILED」と判定して Playbook を停止させてしまう
-
ignore_errors: yesを設定しているのに、スクリプト内の後続処理が実行されない
これらの事象の多くは、Ansible の設定不備ではなく、リモートノード上で動作する「Shell プロセスのエラー伝播ルール(set -e 等)」と Ansible の判定ロジックの乖離に起因します。
エラーがどのレイヤーで検知され、どの範囲まで影響を及ぼすのか。この制御構造を正しく設計できなければ、意図しない継続や停止が発生します。
今回は、実機検証を通じて Shell プロセスと Ansible の間にあるエラー伝播のメカニズムを明らかにし、適切なハンドリング手法を提示します。
1. 前回の振り返り(第7回)
前回は、複数のアイテムを処理する際の「データの衝突」を学びました。今回は、個別の処理が「エラーを吐いた瞬間」に何が起きているかに焦点を当てます。
2. 逆引き辞典との連動
具体的なエラー名から原因を特定したい場合は、以下の逆引き辞典を活用してください。
【保存版】Ansibleよくあるエラー一覧と原因まとめ(Shell編)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
このサイクルが、OS編から続くトラブル解決の最短ルートです。
3. 問題定義:エラーを無視したいのに止まる、止めたいのに進む矛盾
実務で最も厄介なのは、「Shell側が感じているエラー」と「Ansible側が感じているエラー」の乖離です。
※次のセクションの【実機検証】で以下の例を単純化して検証します
- name: スクリプトの実行(一部エラーは許容したい)
ansible.builtin.shell: |
set -e
./command_1.sh # 成功
./command_2.sh # ここでエラー(でも致命的ではないので続行したい)
./command_3.sh # 実行されない!
ignore_errors: yes
【期待】: ignore_errors: yes なので、command_2 が失敗しても command_3 は実行され、Ansibleは最後まで進む。
【現実】: command_2 の時点でShellプロセス自体が即座に終了し、command_3 は呼ばれません。
Ansible側で「失敗を無視しろ(ignore)」と言っているのに、なぜShellは勝手に止まってしまうのでしょうか?
4. 【実機検証】ignore_errorsを貫通する「Shellの自己停止」を追う
ブログで示した「期待と現実の乖離」を証明するため、エラーが発生した瞬間にシェルプロセスがどのように挙動するかを、外部ファイルへの書き込みログで追跡します。
① 検証準備
実行するPlaybook(test_behavior.yml)
- name: セクション3の挙動検証
hosts: all
gather_facts: false
tasks:
- name: "検証:set -e と ignore_errors の組み合わせ"
ansible.builtin.shell: |
set -e
echo "STARTED_STEP_1" > /tmp/ansible_test.log
# あえて失敗させる(存在しないコマンドの実行)
non_existent_command_here
echo "FINISHED_STEP_3" >> /tmp/ansible_test.log
ignore_errors: yes
register: shell_result
- name: デバッグ情報の表示
ansible.builtin.debug:
var: shell_result
実行コマンド
ansible-playbook -i inventory.ini test_behavior.yml
② 検証結果
(コントローラーノード側:Ansibleの出力)
(ansible) [ansible@localhost workspace]$ ansible-playbook -i inventory.ini test_behavior.yml
PLAY [セクション3の挙動検証] ****************************************************************************************************************************************************************
TASK [検証:set -e と ignore_errors の組み合わせ] *******************************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": true, "cmd": "set -e\necho \"STARTED_STEP_1\" > /tmp/ansible_test.log\n# あえて失敗させる(存在しないコマンドの実行)\nnon_existent_command_here \necho \"FINISHED_STEP_3\" >> /tmp/ansible_test.log\n", "delta": "0:00:00.014470", "end": "2026-05-10 17:40:36.428756", "msg": "non-zero return code", "rc": 127, "start": "2026-05-10 17:40:36.414286", "stderr": "/bin/sh: 行 4: non_existent_command_here: コマンドが見つかりません", "stderr_lines": ["/bin/sh: 行 4: non_existent_command_here: コマンドが見つかりません"], "stdout": "", "stdout_lines": []}
...ignoring
TASK [デバッグ情報の表示] *******************************************************************************************************************************************************************
ok: [192.168.1.21] => {
"shell_result": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"cmd": "set -e\necho \"STARTED_STEP_1\" > /tmp/ansible_test.log\n# あえて失敗させる(存在しないコマンドの実行)\nnon_existent_command_here \necho \"FINISHED_STEP_3\" >> /tmp/ansible_test.log\n",
"delta": "0:00:00.014470",
"end": "2026-05-10 17:40:36.428756",
"failed": true,
"msg": "non-zero return code",
"rc": 127,
"start": "2026-05-10 17:40:36.414286",
"stderr": "/bin/sh: 行 4: non_existent_command_here: コマンドが見つかりません",
"stderr_lines": [
"/bin/sh: 行 4: non_existent_command_here: コマンドが見つかりません"
],
"stdout": "",
"stdout_lines": []
}
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
(リモートノード側:実行ログファイルの内容)
[ansibleuser@localhost ~]$ cat /tmp/ansible_test.log
STARTED_STEP_1
ログの読み解きと考察
-
現象:
Ansible側では...ignoringと表示され、Playbookの実行は最後まで継続(SUCCESS扱いで終了)した。
PLAY RECAPでfailed=0かつignored=1となっており、Playbookが中断されずに完走したことが示されています。
しかし、リモート側のログファイルには最初の1行しか書き込まれておらず、スクリプト内の後続処理(STEP 3)は実行されていなかった。 -
理由:
set -eが有効な場合、シェルは実行したコマンドの異常終了(非ゼロの戻り値)を検知した直後に、シェルプロセス自体の実行を停止する。Ansibleのignore_errors: yesは「タスクが失敗してもPlaybook全体の実行を継続させる」ための設定であり、「既に終了したシェルプロセスを継続させる」制御能力は持たないため、プロセスの終了と共にタスク内の後続処理も未実行のまま終了した。
テスト結果のまとめ:
「Ansible側でエラーを無視(ignore)する設定をしても、Shell側がエラーによりプロセスを即時終了(set -e)させた場合、処理は完走しない」
自動化を安定させるためには、Ansibleの設定のみに頼るのではなく、実行されるシェルスクリプト内部のエラー伝播(終了条件)を適切に制御する必要があることが実証されました。
5. メカニズム解説:AnsibleとShellプロセスの制御境界
検証結果が示した「Ansible は後続タスクへ進むが、Shell 内部の処理は中断される」という現象。この裏側にある エラー伝播の構造 を解説します。
エラー伝播のメカニズム
制御レイヤーの分離
-
プロセスの実行権限と生存期間
Ansible のignore_errors: yesは、あくまで「受け取った終了コード(rc)を元に、Playbook全体を止めるかどうか」を判断する Ansible 側のロジックです。
一方で、リモートで起動した Shell プロセス自体の継続・停止 は、そのプロセス内部の設定(set -e等)に依存します。一度停止したプロセスを Ansible が外部から再開させることはできません。 -
ステータスの伝播構造
エラーは「個別コマンド → Shell プロセス → Ansible」という順序で伝播します。set -eは「コマンドの異常終了を検知した時点で、Shell プロセス自体の実行を打ち切る」というルールであるため、この時点で Shell 内部のタイムラインは消失します。Ansible にステータスが届いたときには、すでに Shell 側の実行環境は破棄されています。
6. 観測方法:停止位置をShellとAnsibleの両面から特定する
実機検証の結果が示す通り、トラブル解決の鍵は「Shell側の実行状況」と「Ansibleが受け取った結果」を突き合わせることにあります。停止原因を特定するための具体的な観測手順を整理します。
① Shell レイヤー:「どこでプロセスが切れたか」を追う
スクリプトが複雑な場合、どの行で set -e が発動したかを特定する必要があります。実行トレース(-x オプション)を有効化し、標準エラー出力から「最後に実行を試みた行」を確認します。
- name: 実行トレースを有効化してデバッグ
ansible.builtin.shell: |
set -ex # eでエラー停止、xでトレース表示
./command_1.sh
./command_2.sh # ここで止まれば、トレース出力もここで途絶える
./command_3.sh
ignore_errors: yes
- 観測ポイント:
stderrに表示される+ ./command_2.shのような行がどこで止まっているかを確認します。
② Ansible レイヤー:「何を受け取って終了したか」を見る
Ansible のタスク結果(register 変数)を確認し、Shell プロセスから返却された最終的な状態を分析します。
-
rc (Return Code):
今回の検証結果のようにrc: 127であればコマンド未検出、rc: 1であれば一般的なエラーです。後半の処理ログが出ていないのにrcが0以外であれば、その直前のコマンドでプロセスが即時終了したと判断できます。 -
stdout_lines/stderr_lines:
今回の/tmp/ansible_test.logの検証と同様に、「期待される出力がどこまで書き出されているか」を Ansible の出力結果から確認します。
7. 実務における注意点:エラー検知を阻害する構成
セクション4の検証では set -e による停止動作を確認しましたが、特定の記述方法によってはエラーが正しく伝播せず、意図しないタスクの継続が発生します。
① パイプライン実行時のエラー判定
-
事象:
set -eを定義しているにもかかわらず、パイプライン(|)の途中でコマンドが失敗しても、後続の処理が実行されてしまう。 -
原因:
POSIX準拠のシェルのデフォルト仕様では、パイプライン全体の終了ステータス(rc)は「最後に実行されたコマンド」の結果が採用されるためです。前段のコマンドが異常終了していても、最終段のコマンドが正常終了すれば、シェルプロセスはエラーを検知できず実行を継続します。 -
対策:
set -o pipefailを有効化します。これにより、パイプライン内のいずれかのコマンドが非ゼロのステータスを返した場合、その値をパイプライン全体の終了ステータスとして扱うことが可能になり、set -eによる停止が正しく機能します。
② 終了ステータスの明示的な上書きによる影響
-
事象:
command || true等の記述により、コマンドの失敗が Ansible 側に通知されず、タスクが「SUCCESS」として処理される。 -
原因:
シェルレイヤーで終了ステータスを強制的に0(正常)へ書き換えているためです。セクション5で解説した通り、Ansible はシェルプロセスから返却される終了ステータスを元に成否を判定するため、この情報の遮断により Ansible 側での正確な判定が不可能になります。 -
対策:
シェル側でステータスを加工せず、生の終了ステータスを Ansible に渡す構成を推奨します。その上で、Ansible のfailed_whenパラメータを使用し、「どの終了ステータスをエラーと定義するか」を Ansible レイヤーで管理することで、判定を明確にします。
8. まとめ:処理を確実に実行・制御するための「調査および設計の手順」
シェルスクリプトを含むタスクが意図通りに停止、あるいは継続しない場合の調査および設計手順を整理します。
【調査および設計の手順】
-
set -eの定義状況を確認する
スクリプト内にset -eが記述されている場合、途中のコマンドで非ゼロの終了ステータスが発生した際にシェルプロセスが即時終了するのは、シェルの仕様に基づいた正常な挙動です。まずはプロセスの停止が「シェルの自律的な判断」によるものかを確認してください。 -
エラー許容範囲に応じた実装を選択する
特定のコマンドの失敗後もシェルプロセスを継続させる必要がある場合は、その行に対してのみ終了ステータスを0として扱う(例:command || :)か、一時的にset +eで保護する等の個別対応を検討します。 -
Ansible の
failed_whenによる最終判定を行う
シェル側で終了ステータスを加工せず、生の値を Ansible に渡す構成を基本とします。その上で、Ansible 側のfailed_whenパラメータを用いて、システムとして許容できる状態(成功)と、停止すべき状態(失敗)を定義し直すことが、最も制御の透明性が高い設計となります。
運用の原則
調査においては「なぜ停止したか」という事象に加え、「どのレイヤーが停止を決定したか(Shellプロセスの自己停止か、Ansibleによるタスクの中断か)」 を明確に切り分けることが重要です。
終了ステータスが Shell から Ansible へどのように伝達され、どのレイヤーで判定を下すかを論理的に設計することが、安定したPlaybookを書くための基本になります。
9. 次回予告
これで「処理が停止する仕組み(第8回)」を理解しました。しかし、正しく停止するようになっても、まだ解決できない問題があります。
「シングルクォートで囲ったのに、変数が展開されない/されてしまう」
「エスケープしたはずの記号が、リモートに届くと変化している」
これらはすべて、
「クォートと変数展開の評価順序」
によって引き起こされます。
次回:
【Shell編】第9回:なぜ変数が意図通りに展開されないのか
Ansible変数とShell変数がどの順序で評価されるのか、検証します。
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
10. 連載一覧: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の実行プロセスから逆算して分解できるようになる。実務で使える“読み方”を完成させる。 |