📚 Ansibleが理解できない理由はLinuxにあった【Shell編】第11回:なぜAnsibleの挙動が読めるようになるのか
※Shellの仕組みからAnsibleを理解するシリーズです。
① なぜShellを理解しないとAnsibleは使えないのか
② なぜAnsibleで出力が取得できないのか
③ なぜ結果が消えるのか
④ なぜgrepで見つからないのか
⑤ なぜ正規表現で壊れるのか
⑥ なぜ環境が違うのか
⑦ なぜ条件分岐が失敗するのか
⑧ なぜループがうまく動かないのか
⑨ なぜ途中で処理が止まるのか
⑩ なぜ変数が意図通りに展開されないのか
⑪ なぜAnsibleの挙動が読めるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
📑 連載の移動
前の記事:第10回 | 次の記事:Ansible編 第0回
📋 目次
- 前回の振り返り(第10回)
- 逆引き辞典との連動
- これまでの総整理:Ansibleは何をしているのか
- エラーログを前にした「視点」の決定的な違い
- 実務で迷わないためのデバッグ手順とログの読み解き方
- 実例:ログから逆算する
- 次のステップ:Ansibleの「実装・設計」フェーズへ
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Shell編の第11回」になります。
📍 はじめに:この記事のスタンス
第1回から第10回までで、Shellの挙動を構成する以下の要素を整理してきました。
-
stdout / stderr(出力の分離)
-
パイプ・リダイレクト(データの流れ)
-
stdin(入力の構造)
-
正規表現(解釈ルール)
-
環境差分(PATH / 実行コンテキスト)
-
exit code(成功・失敗の判定)
-
エラーハンドリング(
set -e/pipefail) -
変数展開(Jinja2とShellの二重評価)
-
単語分割(
Word Splitting/IFS)
本記事(最終回)が目指すのは、Ansibleのエラーを「Shellの実行プロセス」から逆算して分解できるようになること です。
画面に出たエラーログをただ眺めるのではなく、Shellのどのレイヤーで処理が破綻したのかを正確に追いかけ、問題の根本をピンポイントで突き止める。その視点をここで整理します
1. 前回の振り返り(第10回)
前回は、変数が展開される 「二段階の評価シーケンス」の仕組みを整理しました。今回は、これまで整理した全要素を統合し、エラーログから原因を逆算する「読み解き(デバッグ)」 に焦点を当てます。
2. 逆引き辞典との連動
具体的なエラー名から原因を特定したい場合は、以下の逆引き辞典を活用してください。
【保存版】Ansibleよくあるエラー一覧と原因まとめ(Shell編)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
このサイクルが、OS編から続くトラブル解決の最短ルートです。
3. これまでの総整理:Ansibleは何をしているのか
ここで一度、Ansibleがリモートに対して行っている処理の全貌を、Shellの視点からおさらいしておきましょう。
Ansibleからコマンドが実行され、結果が画面に返ってくるまでの裏側は、次のようなプロセスで構成されています。
こうして全体の流れを分解してみると、Ansibleは実質的に、Shellの実行結果を受け取って成否を判定しているだけだと分かります。
そのため、タスクがエラー(FAILED)になる場合、その原因はAnsible側の制御ロジックではなく、必ずこの処理プロセスのどこかでShellの解釈やコマンドの実行が破綻していることにあります。
4. エラーログを前にした「視点」の決定的な違い
同じエラーログを見ているはずなのに、原因へ一直線にたどり着けるケースと、解決の糸口すら掴めないケースがあります。この差は、ログの「どこ」を「どういう解像度」で見ているか、にあります。
エラーを「結果」としてしか捉えないアプローチ
トラブルシューティングで行き詰まりやすいのは、画面に表示された FAILED という赤文字や、大枠のエラーメッセージばかりに目を奪われてしまうケースです。
「Playbookが失敗した」「何かエラーが出ている」という外側の結果だけで思考が止まってしまうため、次に何を検証すべきかの仮説が立たず、あてずっぽうにPlaybookのパラメータを書き換えるような、手探りの対応に陥りがちです。
エラーを「Shellのプロセス」として分解するアプローチ
一方で、的確に原因を特定できる場合は、Ansibleの表面的なエラー表示を通過して、その奥にあるOS側の挙動へ即座に視線を移します。
-
戻り値(
rc)が1(一般的なエラー)なのか127(コマンド未検出)なのかを確認する -
パイプラインで繋いだ複数のコマンドのうち、実際にこのステータスを返したのはどれかを特定する
-
直前で変数が展開された際、クォートの剥離や単語分割に引っかかっていないかを検証する
ここで追っているのは FAILED という結果ではなく、先ほど整理した「文字列の展開からコマンド実行にいたるまでのプロセスのどこで処理が破綻したのか」という、具体的な原因の所在です。
エラーの「結果」だけを見て迷走するか、その裏側にある「Shellのプロセス」を追って原因を絞り込めるか。この視点を持つことが、トラブルシューティングの基本になります。
5. 実務で迷わないためのデバッグ手順とログの読み解き方
Ansibleがエラー(FAILED)を返した際、実務で行うべきアプローチはシンプルです。-vvv などの詳細ログ(JSON出力)に目を通し、以下の要素を順番に追いかけていくことで、原因の所在を正確に絞り込むことができます。
-
戻り値(
rc)からエラーの性質を掴む
まず確認すべきは、タスク結果に含まれる"rc":(exit code)の値です。ここで「OSやプロセスがどういう状態で処理を諦めたのか」の方向性を特定します。
・rc: 127:Shellが「実行すべきコマンド(バイナリ)を見つけられなかった(command not found)」ことを意味します (第6回 で解説した環境変数のパス差分などが疑われます)。
・rc: 1:コマンドの起動自体は成功したものの、そのプロセス内部の処理、あるいはShellの構文解釈で何らかの異常が発生したことを示します。 -
cmdで「Shellへ引き渡された最終形態」を検証する
次に確認するのが、"cmd": の項目です。ここには、Jinja2による変数展開がすべて完了し、ターゲットノードのShellにそのまま投入された「生の文字列」が記録されています。
Jinja2の構文ミスにより{{ my_var }}のような記述がそのまま残っていないか、あるいは変数内の引用符と干渉してコマンドの構造がパース段階で崩れていないかを、この文字列から見極めます。 -
stderr から「OS側が下した解釈」を読み取る
そして、最も具体的なエラーのヒントが詰まっているのが"stderr":(標準エラー出力)です。
例えば/bin/sh: php: command not foundと出力されていれば、Shellがどの文字列を「コマンド名」として認識し、どこで探索に失敗したのかがはっきりと分かります。cmdの中身とstderrのメッセージを突き合わせることで、「Shellがコマンドをどう誤認したか」のギャップが完全に浮き彫りになります。
6. 実例:ログから逆算する
ケース①:command not found / No such file or directory
TASK [検証:become時のPATHを確認する] **************************************************
fatal: [192.168.1.21]: FAILED! => {"changed": false, "cmd": "my_command", "msg": "[Errno 2] No such file or directory: 'my_command'", "rc": 2}
🔍 ログからの逆算
-
rc:2とmsgの確認:OSが指定された実行ファイル(my_command)を発見できずにエラーを出している状態です。 -
cmdとタスクの確認:手動で実行できるコマンド名が正しく記述されているにもかかわらず失敗しており、かつタスク側でbecome: true(特権昇格)を使用している場合、差異が生じている可能性があります。
🔗 参照すべき解説
トラブルの「仕組み」を知りたいか、「実例」を見たいかに応じて、以下の解説を参照してください。
-
仕組みを理解する(Shell編):
Ansibleが理解できない理由はLinuxにあった【Shell編】第5回:なぜ環境が違うのか
※Ansibleを介した際のPATHの評価プロセスについて解説しています。 -
実機検証のログを確認する(OS編):
OS編 第2回 6. 実機検証:Ansible実行環境における環境変数の差異
※become実行時に Linuxのsecure_pathなどの仕様によって環境変数がリセットされる、検証ログを公開しています。
・「6-3. 【検証3】Ansible経由(特権昇格あり)での挙動:失敗事例」
ケース②:実行結果(stdout)が「空」になる
TASK [変数の中身を確認] *********************************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": "STDOUTの中身: "
}
🔍 ログからの逆算
-
STDOUTの中身 が空("")の確認:タスク自体は正常終了(ok)しているにもかかわらず、Ansibleがキャッチした標準出力が何も表示されていない状態です。 -
cmdとタスクの確認:実行したコマンドに>(リダイレクト)や|(パイプ)といった流路制御の記号が含まれている場合、OSによって出力の行先が意図的に書き換えられています。
🔗 参照すべき解説
OSがどのようなステップで出力の転送先を決定しているのか、その物理的な挙動(仕組みと検証)については以下の解説を参照してください。
-
仕組みと検証を理解する(Shell編 第2回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第2回:なぜ結果が消えるのか(Redirect / Pipe)
※この記事内の以下の実機検証セクションが、今回のログの直接の答え合わせになります。
-
6-1. 【検証1】: リダイレクトによって stdout が完全に空化する検証ログ。
-
6-2. 【検証2】: パイプラインによって前段のデータ(中間ログ)が次段のプロセスに消費され、Ansibleまで届かなくなる挙動の検証。
ケース③:特定のタスクから進まず、処理がハング(タイムアウト)する
TASK [引数もパイプもなしでgrepを実行] *******************************************************************************************************************************************************
(※この状態で画面が停止し、応答がなくなる)
🔍 ログからの逆算
-
ログが途中で止まる現象の確認:
タスクの実行ログが表示されたまま、changedやFAILEDといった次の出力に進まず、処理が完全に停滞(ハング)している状態です。 -
cmdとタスクの確認:
実行しているコマンド(例:grep 'search_word')に対して、「検索対象のファイル名(引数)」も「前段からのパイプ(標準入力)」もどちらも指定されていない状態です。 -
原因の特定:
これは 第3回 で解説した「OSの入力源判定(Step 2)」による挙動です。OSがデータソースを検知できなかったため、キーボード等の入力を待ち続ける「待機状態」へ移行しています。自動実行環境であるAnsibleでは対話的な入力が供給されないため、そのままタイムアウトするまで停止し続けます。
🔗 参照すべき解説
OSがどのような判定ステップで入力を選択し、なぜ処理がハングしてしまうのか。その物理的な挙動については、以下の記事を参照してください。
-
仕組みと検証を理解する(Shell編 第3回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第3回:なぜgrepで見つからないのか(stdin)
※この記事内の以下の実機検証セクションが、今回のハング現象の直接的な証明になります。
- 6-2. 【検証2】入力待ちによるタスクの停止(Step 2の空振り): 引数もパイプも与えずに実行した際、Ansibleの画面が完全にストップする検証ログを公開しています。
ケース④:正規表現や文字列が、ファイル名や環境変数へと置換されてしまう
ok: [192.168.1.21] => {
"msg": [
"標準出力(stdout): . .. .ansible .bash_history .bash_logout .bash_profile .bashrc .ssh",
"OSが実際に実行したコマンド(stderr): + echo . .. .ansible .bash_history .bash_logout .bash_profile .bashrc .ssh"
]
}
🔍 ログからの逆算
-
stdoutおよびstderrの確認:
Ansible側で指定した正規表現(.*)の文字列が消失し、代わりに.bashrcや.sshといったターゲットノード内のファイル・ディレクトリ名が展開されている状態です。 -
cmd(実行コマンド)のクォートの確認:
Ansibleの変数展開({{ }})の周囲が、OS側のシングルクォート(')で適切に保護されていません。 -
原因の特定:
これは 第4回 で解説した、AnsibleとOSによる「二段階解釈モデル(Step 2:POSIX Shell評価)」の挙動です。クォートによる保護がないため、OS側のShellが*などの記号をコマンドの命令(パス名展開 / Glob)として解釈し、実行直前に文字列をカレントディレクトリ内のファイルリストへ書き換えたことを示しています。
🔗 参照すべき解説
Ansibleから送出された文字列が、OSのShellによって実行直前にどのように評価・変換されるのか。その「解釈の境界線」とクォートによる抑止策については、以下の記事を参照してください。
-
仕組みと検証を理解する(Shell編 第4回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第4回:なぜ正規表現で壊れるのか
※この記事内の以下の実機検証セクションが、今回の文字列変質の直接的な解説になります。
-
6-1. 【検証1】: クォートによる保護がない状態で特殊記号を渡した際、OS側でパス名展開が起きて正規表現が破壊される検証ログ。
-
6-2. 【検証2】: ダブルクォート(弱評価)の指定により、
$記号がOS側の環境変数($USERなど)に上書きされてしまう挙動の比較検証。
ケース⑤:コマンドは正常終了しているが、検索ヒットなしでタスクが赤色停止(FAILED)する
fatal: [192.168.1.21]: FAILED! => {
"changed": true,
"cmd": "grep 'goodbye' test.txt",
"msg": "non-zero return code",
"rc": 1,
"stderr": "",
"stdout": ""
}
🔍 ログからの逆算
-
rcとstderrの確認:
rc: 1(0以外の戻り値)が返却されているものの、stderr(標準エラー出力)は空であり、システム的なエラーメッセージは一切出力されていない状態です。 -
cmd(実行コマンド)の確認:
grepコマンド等を用いて、特定の文字列検索や状態確認を行っています。 -
原因の特定:
これは 第6回 で解説した「Linuxプロセスの終了ステータス」とAnsibleの判定ロジックの乖離によるものです。grepは「検索対象が見つからない場合に1を返す」というOS上の正常な仕様に基づき挙動していますが、Ansibleは「0以外は一律で異常(FAILED)」と見なす一律のデフォルトルールを持っているため、実務上の「想定内の結果(ヒットなし)」であってもタスクが自動的に赤色停止します。
🔗 参照すべき解説
OSが返却する終了ステータスの仕様と、Ansibleの判定プロセスの連動メカニズムについては、以下の記事を参照してください。
-
仕組みと検証を理解する(Shell編 第6回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第6回:なぜ判定で失敗するのか
※この記事内の以下の実機検証セクションが、終了コードによる自動判定の直接的な解説になります。 -
4. 【検証1】grepの返答:
0(成功)と1(ヒットなし)の境界線: 文字列がヒットしなかった際にrc: 1でタスクが停止する実際の挙動と、Ansible内部の判定優先順位を整理しています。
ケース⑥:タスク実行時に rc: 127 が返却され、処理が失敗する
fatal: [192.168.1.21]: FAILED! => {
"changed": true,
"cmd": "my-custom-cmd",
"msg": "non-zero return code",
"rc": 127,
"stderr": "/bin/sh: 行 1: my-custom-cmd: コマンドが見つかりません",
"stdout": ""
}
🔍 ログからの逆算
-
rcの確認:
終了ステータスとして、特定の数値である127が返却されている状態です。 -
stderr(標準エラー出力)の確認:
/bin/sh: ...コマンドが見つかりません(Command not found)というOS側のShellからのエラーメッセージが記録されています。 -
原因の特定:
これは 第6回 で解説した「コマンド未検出時のエラーコード」です。Linuxにおいてrc: 127は、実行を試みたバイナリやスクリプトがシステム内に存在しない(またはPATHが通っていない)ことを示す共通の定義コードです。プログラムの実行自体に失敗しているため、Playbook側の条件分岐(failed_when等)を修正するのではなく、環境側の不備を解消する必要があります。
🔗 参照すべき解説
特定の数値が持つOS側の意味と、トラブル時の切り分け基準については、以下の記事を参照してください。
-
仕組みと検証を理解する(Shell編 第6回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第6回:なぜ判定で失敗するのか
※この記事内の以下の実機検証セクションが、今回のエラーコードの直接的な解説になります。
-
6. 【ケース2】コマンド未検出時のステータス:
rc: 127: ターゲットノードに存在しない架空のコマンドを実行した際のログを基に、Playbookのロジックエラーではなく「実行環境の不備」として最優先で切り分けるべき根拠を解説しています。
ケース⑦:複数台構成でのループ(loop)処理の開始直後に、ログが進まず完全にハングアップする
TASK [スクリプトを3回ループ実行] ************************************************************************************************************************************************************
(※タスクが開始された瞬間に画面の出力が静止し、複数台のターゲットノード共に応答がなくなる)
^C [ERROR]: User interrupted execution
🔍 ログからの逆算
-
画面停止(ハングアップ)の確認:
loopを指定したタスクが開始された直後、成功(ok)も失敗(FAILED)も出力されず、進捗ログが完全に停止している状態です。手動で強制終了(Ctrl + C)せざるを得ない停止状態になっています。 -
実行スクリプト・コマンドの内部確認:
ループ内で実行しているシェルスクリプトやコマンドの内部で、ssh、cat、readなどの標準入力(stdin)を読み取る特性を持つコマンドが使用されています。 -
原因の特定:
これは 第7回 で解説した「標準入力ストリームの競合(Draining)」によるストリームが消費されることで発生します。Ansibleの通信経路(stdin)上に存在している「後続ループ用のデータ(命令セット)」を、スクリプト内のコマンドが自プロセスへの入力値として意図せず全て読み尽くして(消費して)しまったために発生します。これにより制御側とリモート側でプロセスの完了を検知できない状態になり、タスクランナー全体がブロックされます。
🔗 参照すべき解説
Ansibleのループの裏側で起きている標準入力の奪い合いのメカニズムと、通信ストリームを保護する具体的な防御策については、以下の記事を参照してください。
-
仕組みと検証を理解する(Shell編 第7回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第7回:なぜループがうまく動かないのか
※この記事内の以下のセクションが、ループにおけるデッドロック現象の直接的な解説になります。
-
4-②. 【実証】複数台同時実行におけるハングアップの再現: スクリプト内の
catコマンドが通信ストリームを占有し、並列実行中の全ホストが同時にブロックされる実際の実証結果を提示しています。 -
5. 観測方法:Ansible側で「入力の口」を塞いで検証する: タスクに
stdin: ""を付与して通信経路を遮断(/dev/null相当)し、本現象が発生しているかを確定させる切り分け手法(リダイレクトによる安全化)について解説しています。
ケース⑧:ignore_errors: yes を指定しているタスクが正常に継続(ignoring)しているが、スクリプト内の後続処理が実行されていない
(コントローラーノード側:Ansibleの出力)
TASK [検証:set -e と ignore_errors の組み合わせ] *******************************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"changed": true, "cmd": "set -e\n...", "rc": 127, "stderr": "/bin/sh: 行 4: non_existent_command_here: コマンドが見つかりません"}
...ignoring
TASK [デバッグ情報の表示] *******************************************************************************************************************************************************************
ok: [192.168.1.21]
(リモートノード側:実際の実行ログファイル等の中身)
# スクリプトの最初のステップしか実行されておらず、末尾の処理が未実行のまま途切れている
STARTED_STEP_1
🔍 ログからの逆算
-
Ansible側の挙動確認:
タスクの戻り値(rc)が 0 以外(例:rc: 127)であるため一度FAILED!と出力されていますが、ignore_errors: yesの効果により、直後に...ignoringと処理が許容され、Playbook自体は次のタスクへと継続している状態です。 -
ターゲットノード側の実行結果確認:
Ansible上は処理が継続したにもかかわらず、リモートノード側で実行されたシェルスクリプトのログや生成ファイルを確認すると、エラーが発生した行を境界として、それ以降のコマンドが一切実行されていません。 -
原因の特定:
これは 第8回 で解説した「AnsibleとShellプロセスの制御境界」の分離による挙動です。
スクリプト内でset -e(エラー発生時に即時終了)が有効になっているため、個別コマンドの失敗を検知した瞬間に Shellプロセス自体が自律的に終了 しています。
Ansible側のignore_errorsは、あくまで「戻り値を受け取った後にPlaybook全体を止めるか」を制御する上位レイヤーの機能であり、既に終了したShellプロセスを再開させることはできないため、この事象が発生します。
🔗 参照すべき解説
エラーが「個別コマンド → Shellプロセス → Ansible」へと伝播するメカニズムと、各制御レイヤーの境界線については、以下の記事を参照してください。
-
仕組みと検証を理解する(Shell編 第8回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第8回:なぜエラーハンドリングが意図通りに動かないのか
※この記事内の以下のセクションが、制御層の食い違いに関する直接的な解説になります。
-
4. 【実機検証】ignore_errorsを貫通する「Shellの自己停止」を追う: Ansibleの画面上は完走(
failed=0 / ignored=1)しているにもかかわらず、リモート側のログファイル(/tmp/ansible_test.log)は途中で切れている実際の検証ログを公開しています。 -
7. 実務における注意点:エラー検知を阻害する構成: パイプライン(
|)の途中でエラーが起きた際、set -eを定義していても終了コードが上書きされて処理が突き進んでしまう問題(pipefailによる対策)についても併せて網羅しています。
ケース⑨:Ansible変数は正しく展開されているように見えるが、実行先プロセスで構文エラー(クォートの消失)が発生する
fatal: [192.168.1.21]: FAILED! => {
"changed": true,
"cmd": "mysql -D test_db -e \"SELECT * FROM users WHERE name=\"admin_user\"\"",
"msg": "non-zero return code",
"rc": 1,
"stderr": "SELECT * FROM users WHERE name=admin_user\nERROR 1054 (42S22) at line 1: Unknown column 'admin_user' in 'where clause'"
}
🔍 ログからの逆算
-
cmd(構築されたコマンド)の確認:
"cmd"の中身を見ると、WHERE name=\"admin_user\"\"となっており、Ansible(Jinja2)による変数の埋め込み処理自体は機械的に成功している状態です。 -
stderr(プロセス側のエラー)の確認:
実際にMySQLプロセスへ渡されたクエリはWHERE name=admin_userとダブルクォートが消失しており、その結果MySQLが文字列ではなく存在しない列名(カラム)と誤認してUnknown columnエラーを返しています。 -
原因の特定:
これは 第9回 で解説した「引用符の衝突と剥離(Quote Removal)」の挙動です。Ansibleがコマンドライン文字列を正常に構築(Step A)したとしても、それをリモートノードのShellが再評価(Step B)する際、外側のクォートと変数内のクォートを「ペア」として誤認し、プロセスに引き渡す直前に記号を剥ぎ取って(分断して)しまったために発生します。
🔗 参照すべき解説
Jinja2展開とOSのShellパースという「二段階評価」のシーケンスと、値の変質を防ぐ自動エスケープ処理については、以下の記事を参照してください。
-
仕組みと検証を理解する(Shell編 第9回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第9回:なぜ変数が意図通りに伝わらないのか
※この記事内の以下のセクションが、クォート衝突に関する直接的な解説になります。
-
4. なぜ展開されないのか: 二重評価のシーケンス: リモートShellがどの記号を開始・終了のペアと誤認し、引数を露出させるのかを詳細なステップ別で図解しています。
-
5. 【実機検証】:
"cmd"内でJSONエスケープされた文字列と、stderrでクォートが消失するMySQLの実際の構文エラーログを提示しています。 -
7. 解決策: 適切な変数展開と引用符の自動処理: 外側を囲むだけの対策を排し、ターゲットOSの仕様に合わせて自動エスケープを行う
| quoteフィルタの原則適用について解説しています。
ケース⑩:タスクは正常終了(rc: 0)しているが、期待した場所にフォルダが作成されず、意図しない場所に分散して作成される
(コントローラーノード側:Ansibleの出力)
TASK [検証:クォートなしでのディレクトリ作成] **********************************
changed: [192.168.1.21]
TASK [実行結果の分析] **********************************************************
ok: [192.168.1.21] => {
"msg": [
"1. Ansibleが構築したコマンドライン: mkdir /tmp/my app",
"2. 終了ステータス (rc): 0"
]
}
(リモートノード側:実際のカレントディレクトリ等の中身)
[ansibleuser@localhost ~]$ ls -l /tmp
drwxr-xr-x. 2 root root 6 5月 16 14:51 my
[ansibleuser@localhost ~]$ ls -l
drwxr-xr-x. 2 root root 6 5月 16 14:51 app
🔍 ログからの逆算
-
rc: 0(正常終了)と標準ログの確認:
Ansible側のログはchanged(正常終了)を示しており、cmdにもmkdir /tmp/my appとJinja2の展開データが記録されています。一見すると自動化タスクは成功しているように見える状態です。 -
ターゲットノード側の実態確認:
期待していた/tmp/my appというスペースを含んだ1つのフォルダは存在せず、代わりに/tmp/myと、コマンド実行時のカレントディレクトリの直下にappという2つの独立したディレクトリに分散して作成されている状態です。 -
原因の特定:
これは 第10回 で解説した、リモートOSのShellが持つ「単語分割(Word Splitting)」の挙動です。
Ansibleの変数展開の周囲に、値を保護するクォート(" "や' ')が存在しないため、OS側のShellが環境変数IFS(デフォルトではスペースや改行)のルールを機械的に適用しました。これにより文字列がスペースの位置で分断され、mkdirに対して「絶対パス(/tmp/my)」と「相対パス(app)」の2つの引数として引き渡されたことを示しています。mkdir自体は複数引数をエラーなく処理する仕様であるため、この事象がサイレントに発生します。
🔗 参照すべき解説
人間が「1つの塊」と認識しているデータが、OSのShellによってどのように分断・解釈されるのか。その内部シーケンスと可視化デバッグ手法については、以下の記事を参照してください。
-
仕組みと検証を理解する(Shell編 第10回):
Ansibleが理解できない理由はLinuxにあった【Shell編】第10回:なぜスペースで引数が分断されるのか
※この記事内の以下の実機検証セクションが、今回の引数断片化に関する直接的な解説になります。
-
5. 【実機検証】単語分割が引き起こす「引数の意図しない断片化」の再現: ログ上は
rc: 0でありながら、OS側で絶対パスと相対パスに分断されてディレクトリが分散生成される実際の検証ログを公開しています。 -
6. 観測方法:分割された「引数の数」を可視化する:
echoでは見抜けない単語分割の発生を、Shellのループ(for arg in ...)を介してstdout_linesの配列要素数(行数)へと変換し、確実に特定するデバッグ手法を提示しています。
7. 次のステップ:Ansibleの「実装・設計」フェーズへ
この「Shell編」では、Ansibleの裏側で起きているトラブルのメカニズム、つまり「なぜ処理が壊れるのか」という構造の部分を整理してきました。
エラーが起きているレイヤーの見分け方が分かったところで、ここからは「では、どう書けば最初からトラブルの起きにくいPlaybookを作れるのか」という、実装と設計の話に移っていきます。
続く「Ansible編」では、今回のShellの知識を前提としつつ、次のような実務的なトピックを扱っていく予定です。
-
shell / commandモジュールを本当に使うべきかの判断基準 -
標準モジュール(
apt,copy,template等)への適切な置き換え戦略 -
実務の運用に耐えうる
failed_when/changed_whenの厳密な設計 -
Ansibleの核心である「冪等性(
Idempotency)」を担保するための実装パターン -
予期せぬ挙動を防ぐ、安全でメンテナンス性の高いPlaybookの共通規約
Shell編の知識を土台に、Ansible編では実装と設計の話に移ります。
それでは、また次回の 「Ansible編」 で。
📚連載一覧: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を実務で使いこなすための設計思想を完成させる。 |
📑 連載の移動
前の記事:第10回 | 次の記事:Ansible編 第0回
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
8. 連載一覧: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の実行プロセスから逆算して分解できるようになる。実務で使える“読み方”を完成させる。 |