📚 連載:Ansibleが理解できない理由はLinuxにあった【Shell編】第1回:なぜAnsibleで出力が取得できないのか(stdout / stderr)
※Shellの仕組みからAnsibleを理解するシリーズです。
① なぜShellを理解しないとAnsibleは使えないのか
② なぜAnsibleで出力が取得できないのか
③ なぜ結果が消えるのか
④ なぜgrepで見つからないのか
⑤ なぜ正規表現で壊れるのか
⑥ なぜ環境が違うのか
⑦ なぜ条件分岐が失敗するのか
⑧ なぜループがうまく動かないのか
⑨ なぜ途中で処理が止まるのか
⑩ なぜ変数が意図通りに展開されないのか
⑪ なぜAnsibleの挙動が読めるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Shell編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Shell編】まとめ
📑 連載の移動
前の記事:【Shell編】第0回 | 次の記事:【Shell編】第2回
📋 目次
- 前回の振り返り(第0回)
- 逆引き辞典との連動
- Shellの出力構造:2つのストリームの分離
- なぜ取得できないのか:出力先の分岐ロジック
- 状態の確認方法:Ansibleが保持する「出力の実体」を見る
- 【実機検証】ストリームの分離と消失:Ansibleが受け取る「出力の実体」
- まとめ:出力取得に失敗した際の調査手順
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Shell編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Shell編の第1回」になります
📍 はじめに:この記事のスタンス
OSの構造を把握し、権限や環境変数(PATH)の設定が正しく行われている状況下でも、Ansibleでのコマンド実行において以下のような事象が発生する場合があります。
-
registerで受け取った結果のうち、stdoutが空である -
エラーが発生しているにもかかわらず、Ansibleのログに出力されない
-
デバッグ実行を行っても、特定のメッセージがキャプチャされない
-
スクリプトの実行痕跡はあるが、戻り値が取得できない
これらの要因は、Shellの出力構造(stdout / stderr) の挙動にあります。
⚠️ 注意事項
本稿の目的は、ファイル記述子の理論解説ではなく、実務における原因特定に特化しています。
Ansibleで実行結果が取得できない際に、
-
「どのストリームにデータが出力されたのか」を特定する
ことに主眼を置きます。
「タスクは成功しているが出力が空である」という状況に対し、「2つのストリーム(標準出力・標準エラー出力)」の構造からアプローチします。
1. 前回の振り返り(第0回)
前回は、AnsibleがOSを直接操作するのではなく、「Shellに対して文字列を転送している」 という構造を整理しました。
今回は、その命令によって生成されたデータがどのような経路で戻ってくるのか、「出力の仕組み」 を解説します。
2. 逆引き辞典との連動
具体的なエラー名から原因を特定したい場合は、以下の逆引き辞典を活用してください。
【保存版】Ansibleよくあるエラー一覧と原因まとめ(Shell編)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
このサイクルが、OS編から続くトラブル解決の最短ルートです。
3. Shellの出力構造:2つのストリームの分離
Linuxでコマンドを実行すると、出力は内部で2つの経路に分けて扱われます。
| 構成要素 | 役割 | Ansibleでの格納先 |
|---|---|---|
| stdout(標準出力) | 正常な処理結果を出力する経路 | result.stdout |
| stderr(標準エラー出力) | エラー・警告・進捗を出力する経路 | result.stderr |
重要:この2つの出力は、最初から完全に分離されています
どちらに出力するかはShellではなく、コマンド(プログラム)が決定します
つまり
出力が取得できない
↓
Shellの問題ではない
↓
「どの経路に出力されたか」の問題
Ansibleはこの2つの出力をそのまま別々に受け取っているだけです
4. なぜ取得できないのか:出力先の分岐ロジック
コマンド実行時、出力は以下の 「2つのステップ」 で分岐されます。
-
重要:出力はこの時点で分離され、その後は合流しません
-
この2ステップを「上から順に確認する」と、原因を特定できます
🔍 出力分岐ロジック(チェックリスト)
| ステップ | 出力先 | 判定内容 | 取得できない主な理由 |
|---|---|---|---|
| Step 1: stdout(1) | 正常出力 | 結果として扱うべき内容か? | 実際にはstderrに出ている |
| Step 2: stderr(2) | エラー出力 | エラー・進捗として扱う内容か? | stdoutだけ見ていて見落としている |
【出力分岐フロー(Shell内部)】
- トラブル時は「どのStepに出たか」をこの図に当てはめて考えます
💡 結論
結果が出ているのに取得できない
↓
出力されていないのではない
↓
「別のStepに出ている」だけ
※多くのツール(curl / git など)は、進捗や警告も stderr に出力する設計になっています
※ 注意:パイプ(|)の使用
パイプは Step 1(stdout) のデータのみを次段へ渡します。上流コマンドが Step 2(stderr) に出力した情報は、合流処理を行わない限りパイプラインの先へは届きません。
5. 状態の確認方法:Ansibleが保持する「出力の実体」を見る
出力問題はログではなく、Ansibleが保持している
「実際の出力データ」 を直接確認します。
🔍 出力から問題箇所を特定する(Step逆引き)
| 確認対象 | 判定内容 | 該当ストリーム |
|---|---|---|
result.stdout |
正常出力が存在するか | Step 1(stdout) |
result.stderr |
エラー・進捗が出ていないか | Step 2(stderr) |
-
stdoutが空 = 出力がない、ではない -
stderrに出ている可能性を必ず確認する
- 例
- name: 結果確認
debug:
var: result
- 💡 判定の考え方
stdoutにある → Step1で取得できている
stderrにある → Step2に出ている(見落とし)
両方空 → コマンド自体が出力していない
「registerが空」の正体は、Stepの見誤りです
-
Ansibleは出力を加工していない
-
OSが分けた結果をそのまま保持しているだけ
6. 【実機検証】ストリームの分離と消失:Ansibleが受け取る「出力の実体」
セクション3および4で解説した「2つのストリーム(stdout / stderr)」の構造が、実務上のコマンド実行においてAnsibleの挙動にどのような影響を与えるのか。Linux OSの挙動を直接観測することで、その正体を特定します。
6-1. 【検証1】正常系ログが別の出口(stderr)に流れるケース
コマンド自体は正常に終了しているにもかかわらず、Ansibleの stdout が空になり、結果が取得できないように見える現象を確認します。
① 検証環境の構築(リモートノード側)
検証用コマンドとして、詳細な通信ログを出力する curl を使用します。標準的な実行では通信の詳細は画面に出力されますが、これがどちらのストリームに属しているかを確認します。
OS上でのストリーム分離確認
# 標準出力(1)を捨て、標準エラー出力(2)のみを表示
[ansibleuser@localhost ~]$ curl -v https://example.com > /dev/null
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 2606:4700:10::6814:179a:443...
* Trying 104.20.23.154:443...
* Connected to example.com (104.20.23.154) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* TLSv1.2 (IN), TLS header, Certificate Status (22):
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* TLSv1.2 (IN), TLS header, Finished (20):
{ [5 bytes data]
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
{ [1 bytes data]
* TLSv1.2 (IN), TLS header, Unknown (23):
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
{ [19 bytes data]
* TLSv1.3 (IN), TLS handshake, Certificate (11):
{ [3668 bytes data]
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
{ [78 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* TLSv1.2 (OUT), TLS header, Finished (20):
} [5 bytes data]
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.2 (OUT), TLS header, Unknown (23):
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
* subject: CN=example.com
* start date: Apr 2 21:18:57 2026 GMT
* expire date: Jul 1 21:24:46 2026 GMT
* subjectAltName: host "example.com" matched cert's "example.com"
* issuer: C=US; O=CLOUDFLARE, INC.; CN=Cloudflare TLS Issuing ECC CA 1
* SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Unknown (23):
} [5 bytes data]
* TLSv1.2 (OUT), TLS header, Unknown (23):
} [5 bytes data]
* TLSv1.2 (OUT), TLS header, Unknown (23):
} [5 bytes data]
* Using Stream ID: 1 (easy handle 0x5556bbb567d0)
* TLSv1.2 (OUT), TLS header, Unknown (23):
} [5 bytes data]
> GET / HTTP/2
> Host: example.com
> user-agent: curl/7.76.1
> accept: */*
>
* TLSv1.2 (IN), TLS header, Unknown (23):
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [230 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [230 bytes data]
* old SSL session ID is stale, removing
* TLSv1.2 (IN), TLS header, Unknown (23):
{ [5 bytes data]
* TLSv1.2 (OUT), TLS header, Unknown (23):
} [5 bytes data]
* TLSv1.2 (IN), TLS header, Unknown (23):
{ [5 bytes data]
* TLSv1.2 (IN), TLS header, Unknown (23):
{ [5 bytes data]
< HTTP/2 200
< date: Wed, 06 May 2026 07:46:30 GMT
< content-type: text/html
< server: cloudflare
< last-modified: Fri, 01 May 2026 01:24:29 GMT
< allow: GET, HEAD
< accept-ranges: bytes
< age: 491
< cf-cache-status: HIT
< cf-ray: 9f765f1a4b0f1a01-KIX
<
* TLSv1.2 (IN), TLS header, Unknown (23):
{ [5 bytes data]
* TLSv1.2 (IN), TLS header, Unknown (23):
{ [5 bytes data]
100 528 0 528 0 0 1147 0 --:--:-- --:--:-- --:--:-- 1150
* Connection #0 to host example.com left intact
> /dev/null によって標準出力(1)を明示的に破棄した条件下でも、通信ログは依然としてターミナルに表示されました。この挙動は、curl -v の詳細ログが標準出力ではなく、Step 2(stderr) を通じて出力されていることを物理的に裏付けています。
② Ansibleによる実行テスト(コントローラーノード側)
この curl コマンドを Ansible の ansible.builtin.shell モジュールで実行し、register で受け取った際の結果を確認します。
実行するPlaybook (case1_stream_check.yml)
---
- name: 検証1 出力ストリームの判定
hosts: test_servers
gather_facts: false
tasks:
- name: curlで詳細ログを取得
ansible.builtin.shell: "curl -v https://example.com"
register: curl_result
- name: 各ストリームの中身を表示
ansible.builtin.debug:
msg:
- "STDOUTの中身: {{ curl_result.stdout }}"
- "STDERRの中身: {{ curl_result.stderr }}"
実行コマンド
ansible-playbook -i inventory.ini case1_stream_check.yml
③ 検証結果
(ansible) [root@localhost workspace]# ansible-playbook -i inventory.ini case1_stream_check.yml
PLAY [検証1 出力ストリームの判定] ***********************************************************************************************************************************************************
TASK [curlで詳細ログを取得] *****************************************************************************************************************************************************************
changed: [192.168.1.21]
TASK [各ストリームの中身を表示] *************************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": [
"STDOUTの中身: <!doctype html><html lang=\"en\"><head><title>Example Domain</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><style>body{background:#eee;width:60vw;margin:15vh auto;font-family:system-ui,sans-serif}h1{font-size:1.5em}div{opacity:0.8}a:link,a:visited{color:#348}</style></head><body><div><h1>Example Domain</h1><p>This domain is for use in documentation examples without needing permission. Avoid use in operations.</p><p><a href=\"https://iana.org/domains/example\">Learn more</a></p></div></body></html>",
"STDERRの中身: % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 2606:4700:10::6814:179a:443...\n* Trying 104.20.23.154:443...\n* Connected to example.com (104.20.23.154) port 443 (#0)\n* ALPN, offering h2\n* ALPN, offering http/1.1\n* CAfile: /etc/pki/tls/certs/ca-bundle.crt\n* TLSv1.0 (OUT), TLS header, Certificate Status (22):\n} [5 bytes data]\n* TLSv1.3 (OUT), TLS handshake, Client hello (1):\n} [512 bytes data]\n* TLSv1.2 (IN), TLS header, Certificate Status (22):\n{ [5 bytes data]\n* TLSv1.3 (IN), TLS handshake, Server hello (2):\n{ [122 bytes data]\n* TLSv1.2 (IN), TLS header, Finished (20):\n{ [5 bytes data]\n* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):\n{ [1 bytes data]\n* TLSv1.2 (IN), TLS header, Unknown (23):\n{ [5 bytes data]\n* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):\n{ [19 bytes data]\n* TLSv1.3 (IN), TLS handshake, Certificate (11):\n{ [3668 bytes data]\n* TLSv1.3 (IN), TLS handshake, CERT verify (15):\n{ [78 bytes data]\n* TLSv1.3 (IN), TLS handshake, Finished (20):\n{ [52 bytes data]\n* TLSv1.2 (OUT), TLS header, Finished (20):\n} [5 bytes data]\n* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):\n} [1 bytes data]\n* TLSv1.2 (OUT), TLS header, Unknown (23):\n} [5 bytes data]\n* TLSv1.3 (OUT), TLS handshake, Finished (20):\n} [52 bytes data]\n* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384\n* ALPN, server accepted to use h2\n* Server certificate:\n* subject: CN=example.com\n* start date: Apr 2 21:18:57 2026 GMT\n* expire date: Jul 1 21:24:46 2026 GMT\n* subjectAltName: host \"example.com\" matched cert's \"example.com\"\n* issuer: C=US; O=CLOUDFLARE, INC.; CN=Cloudflare TLS Issuing ECC CA 1\n* SSL certificate verify ok.\n* Using HTTP2, server supports multi-use\n* Connection state changed (HTTP/2 confirmed)\n* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0\n* TLSv1.2 (OUT), TLS header, Unknown (23):\n} [5 bytes data]\n* TLSv1.2 (OUT), TLS header, Unknown (23):\n} [5 bytes data]\n* TLSv1.2 (OUT), TLS header, Unknown (23):\n} [5 bytes data]\n* Using Stream ID: 1 (easy handle 0x5562c95257d0)\n* TLSv1.2 (OUT), TLS header, Unknown (23):\n} [5 bytes data]\n> GET / HTTP/2\r\n> Host: example.com\r\n> user-agent: curl/7.76.1\r\n> accept: */*\r\n> \r\n* TLSv1.2 (IN), TLS header, Unknown (23):\n{ [5 bytes data]\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n{ [230 bytes data]\n* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):\n{ [230 bytes data]\n* old SSL session ID is stale, removing\n* TLSv1.2 (IN), TLS header, Unknown (23):\n{ [5 bytes data]\n* TLSv1.2 (OUT), TLS header, Unknown (23):\n} [5 bytes data]\n* TLSv1.2 (IN), TLS header, Unknown (23):\n{ [5 bytes data]\n* TLSv1.2 (IN), TLS header, Unknown (23):\n{ [5 bytes data]\n< HTTP/2 200 \r\n< date: Wed, 06 May 2026 07:37:42 GMT\r\n< content-type: text/html\r\n< server: cloudflare\r\n< last-modified: Fri, 01 May 2026 01:24:29 GMT\r\n< allow: GET, HEAD\r\n< accept-ranges: bytes\r\n< age: 14365\r\n< cf-cache-status: HIT\r\n< cf-ray: 9f76523ae9e8833b-KIX\r\n< \r\n* TLSv1.2 (IN), TLS header, Unknown (23):\n{ [5 bytes data]\n* TLSv1.2 (IN), TLS header, Unknown (23):\n{ [5 bytes data]\n\r100 528 0 528 0 0 1170 0 --:--:-- --:--:-- --:--:-- 1170\n* Connection #0 to host example.com left intact"
]
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ログの読み解きと考察
今回の実機検証により、OSの出力構造とAnsibleの変数格納ロジックについて、以下の事実が証明されました。
-
出力出口(ストリーム)の厳格な分離
curl -vの実行において、本来の目的データであるHTMLソースは Step 1 (stdout) に、通信のメタ情報(TLSハンドシェイクや進捗状況)は Step 2 (stderr) に、OSレベルで明確に切り分けられています。事前のコマンド検証で> /dev/null(標準出力の破棄)を行ってもログが表示され続けたのは、OSがこれらを別々のファイル記述子として扱っているためです。 -
Ansibleによる「生データ」の保持
実行結果のJSON構造を確認すると、AnsibleはOSから受け取ったデータを加工することなく、それぞれのストリームに対応する変数(stdout/stderr)へ忠実に振り分けています。デバッグログに表示されている\n(改行)や\r(キャリッジリターン)の混在は、プログラムがリアルタイムに出力した情報を、Ansibleがそのままの状態でキャプチャしている証左です。 -
「実行成功」と「出力の有無」の非連動性
PLAY RECAPが示す通り、タスクはok(成功)で終了していますが、stdoutのみを参照している後続タスクや監視システムがあった場合、通信ログ側に記録された重要な警告や詳細情報を一切検知できない状態に陥ります。
テスト結果のまとめ:
Ansibleにおける「出力が取得できない」という現象の本質は、データの消失ではなく、OSが選択した出力出口(Step 2)と、ユーザーが参照している変数(stdout)の不一致にあります。
6-2. 【検証2】パイプによる「エラーメッセージ」の消失
複数のコマンドをパイプ(|)で繋いだ際、上流で発生したエラー内容が後続コマンドに引き継がれず、Ansible側で原因特定が困難になる現象を確認します。
① 検証手順(OS上での挙動確認)
存在しないディレクトリに対して ls を実行し、その出力を grep でフィルタリングします。
# 存在しないディレクトリを指定して実行
[ansibleuser@localhost ~]$ ls /non_existent_dir | grep 'test'
ls: '/non_existent_dir' にアクセスできません: そのようなファイルやディレクトリはありません
画面上にはエラーが表示されます。このメッセージが「標準出力」としてパイプの先(grep)へ流れているのか、それとも「標準エラー出力」としてその場に留まっているのかをAnsibleで検証します。
② Ansibleによる実行テスト
パイプラインを含むコマンドを ansible.builtin.shell モジュールで実行し、各ストリームの保持状況を確認します。
実行するPlaybook (case2_pipe_test.yml)
---
- name: 検証2 パイプによるストリームの消失
hosts: test_servers
gather_facts: false
tasks:
- name: パイプを含むコマンドの実行
ansible.builtin.shell: "ls /non_existent_dir | grep 'test'"
register: pipe_result
ignore_errors: true
- name: 実行結果の確認
ansible.builtin.debug:
msg:
- "STDOUTの中身: {{ pipe_result.stdout }}"
- "STDERRの中身: {{ pipe_result.stderr }}"
- "終了ステータス: {{ pipe_result.rc }}"
実行コマンド
ansible-playbook -i inventory.ini case2_pipe_test.yml
③ 検証結果
(ansible) [root@localhost workspace]# ansible-playbook -i inventory.ini case2_pipe_test.yml
PLAY [検証2 パイプによるストリームの消失] ***************************************************************************************************************************************************
TASK [パイプを含むコマンドの実行] ***********************************************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": true, "cmd": "ls /non_existent_dir | grep 'test'", "delta": "0:00:00.010733", "end": "2026-05-06 17:16:13.565126", "msg": "non-zero return code", "rc": 1, "start": "2026-05-06 17:16:13.554393", "stderr": "ls: '/non_existent_dir' にアクセスできません: そのよう なファイルやディレクトリはありません", "stderr_lines": ["ls: '/non_existent_dir' にアクセスできません: そのようなファイルやディレクトリはありません"], "stdout": "", "stdout_lines": []}
...ignoring
TASK [実行結果の確認] ***********************************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": [
"STDOUTの中身: ",
"STDERRの中身: ls: '/non_existent_dir' にアクセスできません: そのようなファイルやディレクトリはありません",
"終了ステータス: 1"
]
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
ログの読み解きと考察
-
ストリームの遮断と隔離
STDOUTの中身が空であるのに対し、STDERRの中身にエラーメッセージが記録されています。これは、lsが出力したエラー情報(stderr)がパイプ(|)を通過せず、その場に留まったことを示しています。 -
終了ステータスの非対称性
lsコマンドは存在しないパスを指定された場合、OSや実装に応じて0以外のエラーコード(1や2など)を返します。しかし、今回の検証結果ではrc=1となっています。これは、パイプライン全体の終了ステータスが、最終段である grep(検索対象が見つからない場合のステータス)の結果で上書きされたことを示しています。
テスト結果のまとめ:
「パイプを使うとエラー内容が取得できない」原因は、OSが標準エラー出力(stderr)をパイプラインの転送対象から除外しているという基本仕様にあります。
7. まとめ:出力取得に失敗した際の調査手順
Ansibleで「結果が取得できない」 ときは、以下の3ステップを順番に確認してください。
【出力 取得失敗の調査手順】
1. stdout(Step 1)に出力されているか
→ result.stdout を確認
→ ここが空であれば「標準出力には何も書き込まれていない」
2. stderr(Step 2)に出力されていないか
→ result.stderr を確認
→ ここに値があれば「エラー・進捗が別のストリームに流れている」
3. ストリームを合流させているか
→ 2>&1 等で stderr を stdout に合流させる
→ 参照する変数を一つに絞り、情報の見落としを防ぐ
調査のポイント
出力が取得できない原因は、「出力の欠如」ではなく「出力先の相違」に集約されます。
stdout が空である状態は、プログラムが標準エラー出力(stderr)を情報の排出先に選択した結果であり、OSの仕様通りの挙動です。
この構造を整理すると以下の通りです。
-
現象: Ansibleが情報を取得できていない
-
実態: 参照している変数(出口)が、実際の出力先と一致していない
ストリームの役割
-
stdout (Step 1): 処理結果を出力する出口
-
stderr (Step 2): 異常、進捗、メタ情報を出力する出口
OSはこれら2つのストリームを独立したファイル記述子として扱います。Ansibleもこの仕様に基づき、それぞれのデータを別々の変数へ格納します。
結論
-
情報の種類に応じて、どのストリームに出力されたか
-
その出力に対応する変数(stdout または stderr)を参照しているか
この2点を確認することで、原因の切り分けが完了します。出力に関するトラブルは、ストリーム(stdout / stderr)の判定によって論理的に解消可能です。
8. 次回予告
本稿では「出力の出口(stdout / stderr)」について解説しました。これを踏まえ、次回はこれらの出口を操作する「出力の流路制御」を扱います。
実務においては、ストリームの概念を把握していても、以下のような事象が発生します。
-
パイプ(
|)の使用により、stderrのログが後続へ渡らず消失する -
リダイレクト(
>)の記述によって、Ansible のregister変数が空になる
これらの要因は Ansible の仕様ではなく、OSレベルでの「データの流路」の変化にあります。
-
パイプやリダイレクトが、OS内部でどのようにデータを転送しているのか
-
特定の記号の介在によって、取得可能なデータが制限される理由
-
合流(
2>&1)と破棄(/dev/null)を組み合わせた、適切なログキャプチャの手法
次回、
【Shell編】第2回:なぜ結果が消えるのか(パイプ / リダイレクト)
にて、ストリームの合流と分岐を制御し、Ansible での出力を正確に特定する手法を解説します。
「結果がない」のではなく、「意図しない場所に流れている」という物理的な挙動を整理していきます。
📑 連載の移動
前の記事:【Shell編】第0回 | 次の記事:【Shell編】第2回
📑 【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の実行プロセスから逆算して分解できるようになる。実務で使える“読み方”を完成させる。 |