📚 連載:Ansibleが理解できない理由はLinuxにあった【Shell編】第7回:なぜループがうまく動かないのか(Loop & Stdin)
※Shellの仕組みからAnsibleを理解するシリーズです。
① なぜShellを理解しないとAnsibleは使えないのか
② なぜAnsibleで出力が取得できないのか
③ なぜ結果が消えるのか
④ なぜgrepで見つからないのか
⑤ なぜ正規表現で壊れるのか
⑥ なぜ環境が違うのか
⑦ なぜ条件分岐が失敗するのか
⑧ なぜループがうまく動かないのか
⑨ なぜ途中で処理が止まるのか
⑩ なぜ変数が意図通りに展開されないのか
⑪ なぜAnsibleの挙動が読めるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
📑 連載の移動
前の記事:【Shell編】第6回 | 次の記事:【Shell編】第8回
📋 目次
- 前回の振り返り(第6回)
- 逆引き辞典との連動
- 問題定義:100台回るはずのAnsible Loop が、なぜ1回で止まるのか
- なぜループがうまく動かないのか:Ansibleの裏で起きる「stdinデータの消費」
- 観測方法:Ansible側で「入力の口」を塞いで検証する
- よくある実務ミス2選(ループ編)
- まとめ:ループを完走させるための「調査の原則」
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Shell編の第7回」になります。
📍 はじめに:この記事のスタンス
前回の第6回では、Ansibleがタスクの成否を「終了コード(rc)」という数字だけで判断していることを学びました。これで単発のタスク制御は完璧です。
しかし、Ansibleの真骨頂である 「大量の対象への繰り返し処理(loop)」 を動かしたとき、不可解な挙動に突き当たることがあります。
-
「
loopで100個のアイテムを指定したのに、なぜか処理が開始直後にハングして全台止まってしまう」 -
「Ansible側は正常終了しているのに、リモート側で実行したシェルスクリプトのループが途中で消えている」
-
「
with_itemsの中で実行したコマンドが、次のループ用のデータを標準入力(stdin)から読み取ってしまっている気がする」
これらの原因は、Ansibleの構文ミスではありません。
Ansibleが呼び出した「Shellプロセス」による、標準入力(stdin)の奪い合い
にあります。
Ansibleがループ処理をリモートで実行する際の、stdinの扱いを整理します。
⚠️ 注意事項
この記事は、単に loop: の書き方を解説するものではありません。
目的はただひとつです。
「Ansibleが送るループ処理用のデータと、リモートコマンドが読み取る入力が、ひとつの流路(stdin)の中でどう衝突しているのかを理解すること」
この問題をOSの仕組みから理解します
1. 前回の振り返り(第6回)
前回は、Ansibleが「0以外=失敗」とみなすロジックを学びました。
今回は、その処理を「連続」させたときに発生する、物理的なデータの干渉に焦点を当てます。
2. 逆引き辞典との連動
具体的なエラー名から原因を特定したい場合は、以下の逆引き辞典を活用してください。
【保存版】Ansibleよくあるエラー一覧と原因まとめ(Shell編)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
このサイクルが、OS編から続くトラブル解決の最短ルートです。
3. 問題定義:100台回るはずのAnsible Loop が、なぜ1回で止まるのか
実務でよくある現象です。
- name: リストにある各サーバのセットアップを実行
ansible.builtin.shell: "./setup_env.sh {{ item }}"
loop: "{{ server_list }}"
【期待】: server_list に定義された全サーバに対して、順次セットアップが実行される。
【現実】 : タスクが開始直後にプロンプトが静止し、全台の処理が完了しないままAnsibleがハング状態になる。ループのログが一切表示されず、強制終了するまで応答がない。
一見するとAnsibleのバグに見えますが、実はAnsibleが呼び出した ./setup_env.sh の中で動いている特定のコマンドが、「通信経路(stdin)上に存在している次のループ用データを読み取ってしまっている」 のです。
4. なぜループがうまく動かないのか:Ansibleの裏で起きる「stdinデータの消費」
Ansibleの loop が途中で停止・破綻するメカニズムを、理論と実証データの両面から確認します。
① 理論:stdinデータが消費されるプロセス
Ansibleは loop を実行する際、アイテムごとにコマンドを実行しますが、
リモートで起動されるShellプロセスは「標準入力(stdin)」を持っており、
その入力ストリームに対してコマンドが干渉することがあります。
【実務的な干渉フロー:./setup_env.sh の中身】
例えば、以下のような一見普通に見えるシェルスクリプトをループで回すと、事故が起きます。
#!/bin/bash
# ./setup_env.sh の中身
SERVER_NAME=$1
echo "Setting up ${SERVER_NAME}..."
# stdinを読み取るコマンド(ssh等)が、Ansibleの次ループ用データを
# 自コマンドの入力として読み取ってしまう。
ssh user@remote_node "ls /tmp"
echo "Done."
【フロー図:stdinデータが消費されるプロセス】
💡 技術的背景:stdinを読むコマンドの挙動
-
コマンドの性質:
sshやread、catといったコマンドは、実行されると「標準入力(stdin)に未処理のデータが存在する場合、それを自コマンドの入力値として優先的に読み取る」というUNIX/Linuxの標準的な仕様を持っています。 -
データの消失: Ansibleのループ処理(Pipelining有効時など)では、効率化のために同一の通信経路上に「次以降のループで実行する命令セット」が先行して送出される場合があります。
現在実行中のコマンドが、本来Ansible本体が扱うべきこれらの「後続データ」まで意図せず全て読み取って消費(Drain) してしまうため、通信経路上からデータが消失します。 -
物理的な破綻: 制御ノードが次のループ処理に移ろうとした際、通信経路が既に末端(EOF)に達しているため、「実行すべき命令がもう存在しない」 と判断されます。
これがループが停止する原因です。
② 実証:複数台同時実行におけるハングアップの再現
この「stdinデータの消費」というメカニズムが、実際の複数台環境においてどのような挙動として現れるかを実証データから確認します。
sshの例と同じ原理を、より再現しやすいcatで確認します。
前提条件(環境構成)
本検証では、以下の環境で複数台のターゲットノードに対して同時にPlaybookを実行します。
-
ターゲットノード:
・192.168.1.21
・192.168.1.22 -
認証方式:
sshpass を利用したパスワード認証(非対話実行を担保) -
検証目的:
特定のノードで発生した「標準入力の待ち」が、Ansible全体の実行プロセスにどのような影響を及ぼすかを確認する。
検証手順
Ansibleから呼び出される実行用スクリプトを作成します。
ここでは、スクリプト内で標準入力を読み取るコマンド(cat)を記述します。/dev/nullへリダイレクトすることで、読み取ったデータを破棄しつつstdinを消費する状態を再現します。
(ansible) [root@localhost workspace]# cat setup_env.sh
#!/bin/bash
# 引数を受け取り表示するだけの処理
echo "Processing $1..."
# 標準入力を読み取るコマンド(Ansibleの制御用通信ストリームを消費する)
cat > /dev/null
Ansibleによる実行テスト
実行するPlaybook (loop_stdin_test.yml)
複数台のターゲットに対し、ループ処理を用いて当該スクリプトを連続実行します。
---
- name: 複数台構成における標準入力競合テスト
hosts: test_servers
gather_facts: false
tasks:
- name: スクリプトを3回ループ実行
ansible.builtin.script:
cmd: "./setup_env.sh {{ item }}"
loop:
- item-01
- item-02
- item-03
実行コマンド
ansible-playbook -i inventory.ini loop_stdin_test.yml
検証結果
(ansible) [root@localhost workspace]# ansible-playbook -i inventory.ini loop_stdin_test.yml
PLAY [複数台構成における標準入力競合テスト] *************************************************************************************************************************************************
TASK [スクリプトを3回ループ実行] ************************************************************************************************************************************************************
^C [ERROR]: User interrupted execution
※タスク開始と同時にプロンプトが静止。192.168.1.21/192.168.1.22共にループの実行ログが表示されず、後続の処理へ移行しない
ログの読み解きと考察
-
現象:Playbookの実行中、特定のタスクにおいて出力が完全に停止し、プロンプトが応答しなくなります。この影響は、問題が発生している特定のホスト(192.168.1.21)のみならず、正常に動作するはずの他ホスト(192.1681.22)に対しても波及し、Playbook全体の進行が停止することが確認されました。
-
理由: AnsibleはSSH経由でリモートコマンドを実行し、そのプロセスの終了を待機します。しかし、呼び出されたプロセス(
cat)が標準入力を開いたまま入力を待ち続けると、Ansibleが管理するSSHの通信ストリームを占有します。この状態になると、Ansible側はプロセスの完了を検知できず、かつリモート側はAnsibleからの終了指示を受け取れない「デッドロックに似た未完了状態」となります。結果としてAnsibleのタスクランナー全体がブロックされ、複数台並列実行が機能しなくなります。
テスト結果のまとめ:
Ansibleで実行するすべてのスクリプトおよびコマンドは、非対話型(non-interactive)であることを保証する必要があります。標準入力を要求する可能性のある処理を組み込む際は、COMMAND < /dev/null のように入力を明示的に遮断(リダイレクト)する実装を徹底することが、これがPlaybookを安定させる基本です。
5. 観測方法:Ansible側で「入力の口」を塞いで検証する
この現象が起きているかどうかを特定する最も確実な方法は、「stdin(通信経路)を明示的に空にし、リモートコマンドが読み取るデータそのものを遮断する」 ことです。
Ansibleタスクでの検証方法:stdin オプションの活用
Ansibleの shell モジュールには、標準入力を制御する stdin というパラメータが用意されています。
- name: データの奪い合いが起きていないか検証
ansible.builtin.shell: "./setup_env.sh {{ item }}"
loop: "{{ server_list }}"
# stdin(通信経路)を空(/dev/null相当)にし、コマンドが読み取るデータ自体を無くす
stdin: ""
(補足)
stdinパラメータはansible.builtin.shellモジュールのAnsible 2.7以降で有効です。それ以前のバージョンでは< /dev/nullをコマンドに直接付与する方法で代替できます。ansible.builtin.shell: "./setup_env.sh {{ item }} < /dev/null"この一行を追加してループが最後まで完走するようになった場合、原因は「スクリプト内のコマンド(ssh等)が、通信経路(stdin)上に存在していた後続データを読み取ってしまっていたこと」で確定です。
(補足)
第4セクションの検証で使用したansible.builtin.scriptモジュールでも、同様にstdin: ""を指定することで標準入力を遮断できます。
6. よくある実務ミス2選(ループ編)
stdin以外にもループで詰まるパターンが2つあります
ミス①:サブシェル内での変数更新(Ansible外のShellループ)
-
現象: Ansibleから呼び出したシェルスクリプト内のループで、変数を更新しても反映されない。
-
原因: パイプを使ってループを作ると (
cat list | while read line; do ...)、ループ全体が 「サブシェル(別プロセス)」 で動くため、変数の変更が親プロセスに伝わりません。 -
解決策: Ansible側で
loopを回すか、Shell側でパイプを避ける(プロセス置換など)構成にします。
ミス②:Ansible loop 内での register 参照ミス
-
現象: ループの結果を変数に保存したが、最後の1回分しかデータが入っていない。
-
原因:
registerはループのたびに変数を上書きします。 -
解決策: ループ実行時の
register変数には、自動的にresultsというキーが作られ、全実行結果がリスト形式で格納されます。
・ 正:{{ result.results }}
・ 誤:{{ result.stdout }}(これだと最後の1回分のみ)
7. まとめ:ループを完走させるための「調査の原則」
Ansibleのループを安定させるための手順です。
【ループ 失敗の調査手順】
-
「stdin(通信経路)の消費有無」を切り分ける
→ Ansibleタスクにstdin: /dev/nullを付け、挙動が変わるか確認する。 -
アイテムごとの独立性を担保する
→ 1つのアイテムの処理が、次のループ実行に影響を与えていないか、あるいは前のアイテムの残骸(stdin上に残ったデータ)を拾っていないか確認する。 -
resultsリストをデバッグする
→debug: var=resultを使い、全ループ分のステータスがリストとして保持されているか確認する。
調査の原則
ループが止まる原因の多くは、標準入力の扱いにあります。「1回ごとの処理が標準入力をどう扱っているか」を確認することが、ループ安定化の最短ルートです。
8. 次回予告
これで「繰り返し(第7回)」を制御できるようになりました。
しかし、ループが完走したとしても、その「途中の1回」でエラーが発生した場合、Ansibleはそれをどう扱うべきでしょうか?
「1個失敗したら即止めるべきか?」
「残りは意地でも通すべきか?」
自動化の「止まり方」の美学、
「エラーハンドリングと処理の中断」
を学びます。
次回:
【Shell編】第8回:なぜ途中で処理が止まるのか
set -e や Ansibleの any_errors_fatal の裏側にある「中断の論理」を解剖します。
📑 連載の移動
前の記事:【Shell編】第6回 | 次の記事:【Shell編】第8回
📑 【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の実行プロセスから逆算して分解できるようになる。実務で使える“読み方”を完成させる。 |