📚 Ansibleが理解できない理由はLinuxにあった 【Ansible編】第1回:なぜshellモジュールは危険なのか
⓪ なぜShellの知識がないとAnsibleは壊れるのか
① なぜshellモジュールは危険なのか
② なぜmoduleを使うと安全になるのか
③ なぜ毎回changedになるのか
④ なぜ条件分岐が壊れるのか(Ansible編)
⑤ なぜ変数が意図通りに扱えないのか
⑥ なぜタスクの順序で壊れるのか
⑦ なぜエラー制御が破綻するのか
⑧ なぜ非同期・待機で失敗するのか
⑨ なぜPlaybookが読めないのか
⑩ なぜAnsibleを正しく設計できるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Ansible編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ ※近日公開予定
📑 連載の移動
前の記事:【Ansible編編】第0回 | 次の記事:【Ansible編】第2回 ※近日公開予定
📋 目次
- 前回の振り返り(第0回)
- 逆引き辞典との連動
- はじめに(この記事のスタンス)
- 問題定義:なぜshellは“普通に使えてしまう”のか
- 構造解剖:shellモジュールがやっていること
- 具体例:どこで構造が崩れるのか
- 本質:shellの何が危険なのか
- commandモジュールとの構造的違い
- 設計思想への接続
- まとめ
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Ansible編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Ansible編の第1回」になります。
1. 前回の振り返り(第0回)
前回は、Ansibleの正体が 「Shellの挙動を抽象化したラッパー」 であることを確認しました。
Ansible
↓ (Jinja2で変数展開)
コマンド文字列生成
↓ (SSH転送)
リモートShell
↓ (解釈・実行)
結果 (rc / stdout / stderr)
Ansibleで起きる問題の多くはAnsible自身のバグではなく、背後で動く Shellの仕様(環境差分・単語分割・変数展開) に起因します。
2. 逆引き辞典との連動
「設計上の原因」を特定したい場合は、以下の逆引き辞典を活用してください。
※随時公開予定
Ansibleが理解できない理由はLinuxにあった【Ansible編】:トラブル逆引き辞典(設計パターン版)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
3. はじめに(この記事のスタンス)
今回のテーマは 「shellモジュールの是非」 です。
「shellモジュールは避けるべき」という言葉はエンジニアの間でよく聞きますが、その理由を「なんとなく危ないから」「行儀が悪いから」といった感覚論で済ませてしまうケースは少なくありません。本記事では、shellモジュールが抱えるリスクを 「構造的な不確実性」 という観点から整理し、なぜそれが自動化の信頼性を損なうのかを説明します。
4. 問題定義:なぜshellは“普通に使えてしまう”のか
shellモジュールの問題は、その手軽さにあります。既存のシェルスクリプトやワンライナーをそのままコピー&ペーストすれば、Ansible経由で即座に実行できます。学習コストが低く、手元の操作をそのまま自動化に持ち込めるため、使い始めの段階では「正解」のように見えます。
しかし、この手軽さの実態は「ただの文字列実行」です。中身が保証されない文字列をリモートに投げているだけで、Ansibleが本来持つ状態管理の仕組みは働いていません。この 「動いてしまうが、中身は不透明」 という状態が、運用フェーズで問題として顕在化します。
5. 構造解剖:shellモジュールがやっていること
shellモジュールが実行される際の流れは以下の通りです。
[Ansible 制御ノード]
↓
1. Jinja2展開(変数を文字列に置換)
2. コマンド文字列生成(実行コマンドの完成)
↓
(SSH転送)
↓
[ターゲットノード]
↓
3. リモートShell起動
4. 再解釈(クォートの処理・引数の分割・環境変数の展開)
↓
5. コマンド実行
注目すべきは「4. 再解釈」のステップです。Ansibleが送り出した文字列は、この時点でリモートOS上のShellに渡り、Shellのルールで改めて解釈されます。Ansible側で意図した構造が、リモートShellの仕様(IFSやエスケープルール)によって書き換えられてしまう。 これが、shellモジュールの不確実性の根本原因です。
6. 具体例:どこで構造が崩れるのか
実際の挙動をログ形式で確認します。いずれもShellの仕様に起因する問題です
例①:引数の境界崩壊(単語分割)
ディレクトリ作成をshellで行うケースです。
- name: ディレクトリ作成
shell: mkdir {{ target_dir }}
変数の値にスペースが含まれていた場合(例:/tmp/my app)、ログには一見正常に終了したかのような以下の結果が残ります。
"cmd": "mkdir /tmp/my app"
"rc": 0
"changed": true
Ansibleは「1つのディレクトリ」を作ろうとしましたが、リモートShellはスペースを区切り文字(IFS)と見なし、「2つの独立した引数」として実行しました。
mkdir は複数の引数を受け取れる仕様のため、OSはエラーを出さずに処理を完了します。
結果として、/tmp/my(絶対パス)と app(実行時のカレントディレクトリへの相対パス)という2つのディレクトリが別々の場所に作成されます。
ログ上は成功(rc: 0)しているにもかかわらず、実態は意図しない場所にディレクトリが作成されている。これが単語分割による「サイレントな失敗」です。
この挙動の再現手順や、詳細な実機検証ログ(Playbookやターゲットノードの実態)については、【Shell編】第10回:単語分割のメカニズム で詳しく解説しています。
例②:変数の二重評価による構造破壊(引用符の衝突)
変数内に引用符が含まれる文字列を、shellモジュールでそのまま展開するケースです(例:search_filter の値が name="admin_user" )。
- name: 設定値の検索
shell: mysql -D test_db -e "SELECT * FROM users WHERE {{ search_filter }}"
Jinja2による変数展開の時点では、SQL文として正しく組み立てられているように見えます。しかしリモートでの実行結果は rc: 1(異常終了)となります。
"cmd": "mysql -D test_db -e \"SELECT * FROM users WHERE name=\"admin_user\"\"",
"rc": 1,
"stderr": "ERROR 1054 (42S22): Unknown column 'admin_user' in 'where clause'"
Jinja2は指定された位置に値を埋め込むだけで、SQLの構文チェックは行いません。問題はその後、リモートShellがこの文字列を受け取った段階で発生します。
-e の直後にある外側のダブルクォートと、変数内のダブルクォートをShellが「閉じペア」と判断し、admin_user を囲んでいたクォートが取り除かれた状態でmysqlプロセスへ渡されます。
MySQLは admin_user を文字列ではなくカラム名として解釈するため、構文エラーになります。
💡 ログを読み解く際の注意点
ログの "cmd" 内にあるバックスラッシュ(\")は、AnsibleがログをJSON形式で出力する際に自動でエスケープしたものです。実際のShellには \ は渡っておらず、純粋な " 同士が衝突して剥離が起きています。
この挙動の再現手順や、詳細な実機検証ログ(Playbookやターゲットノードの実態)については、【Shell編】第9回:なぜ変数が意図通りに展開されないのか で詳しく解説しています。
例③:入力ソースの誤認によるデータ不整合(および対話待ちハング)
パイプによる標準入力と、コマンドの引数(ファイル名指定)を同時に記述してしまうケースです。
- name: 設定値の検索(パイプと引数の同時指定)
shell: echo 'DATA_FROM_PIPE' | grep 'DATA' /tmp/test_input.txt
実行ログは changed となり、出力結果は以下のようになります。
"cmd": "echo 'DATA_FROM_PIPE' | grep 'DATA' /tmp/test_input.txt",
"stdout": "DATA_FROM_FILE"
パイプ経由の DATA_FROM_PIPE を検索するつもりでも、出力されるのはファイル内の DATA_FROM_FILE のみです。grep や awk などのコマンドは、引数にファイル名が指定されている場合、パイプからの標準入力を無視する仕様になっているためです。
📌 補足:「引数もパイプもない場合」のハング
引数(ファイル名)もパイプ(標準入力)も与えずに shell: "grep 'search_word'" のように実行した場合、OSはキーボード等からの入力を待ち続けます。Ansibleは対話的な入力ができないため、タスクはその場でハングし、最終的にタイムアウトエラーになります。
引数と標準入力の優先順位(検証1)や、タスクがハングする内部メカニズム(検証2)の詳細については、【Shell編】第3回:なぜgrepで見つからないのか で詳しく解説しています。
7. 本質:shellの何が危険なのか
これまでの例から、shellモジュールの問題は以下の3点に整理できます。
- 文字列を実行している: Ansibleは「何を投げたか」は把握できても、それがShell側で「どう解釈されるか」を保証できません。
- Shellが再解釈する: 実行直前にShellのルール(IFS、クォート、展開)が介在するため、挙動が動的に変化します。
- 環境に依存する: OSやShellの種類、実行コンテキスト(PATH等)に依存するため、環境が変わると再現性が失われます。
これらはバグではなく、Shellというツールの強力すぎる「仕様」です。 この仕様をAnsibleという抽象化レイヤーの上で制御しようとすること自体に、設計上の無理があります。
8. commandモジュールとの構造的違い
shellの代替として推奨される command モジュールは、構造が異なります。
-
shell: リモートで
/bin/shなどのShellを起動し、その上で文字列を評価させる。 - command: 指定されたバイナリを直接実行する。Shellを介さない。
[shell] : Ansible -> SSH -> Shell -> 再解釈 -> 実行
[command] : Ansible -> SSH -> 実行
command はShellを通さないため、パイプやリダイレクト、Shell変数の展開が利用できません。
その代わり、Shellによる再解釈が発生しないため、コマンドが意図しない形に変質するリスクを避けられます。
ただし、command はあくまで「コマンド実行」を安全にする仕組みであり、「状態管理」を行うものではありません。
そのため設計としては、依然としてコマンド実行ベースに留まります。
9. 設計思想への接続
shellモジュールに頼る設計を続ける限り、エンジニアは常に「Shellがどう解釈するか」を意識し続けなければなりません。
Shellの再解釈による不確実性をなくすには、「文字列を実行する」設計から 「状態を定義する」設計 へ移行する必要があります。
コマンドを投げてその成否を追うのではなく、「あるべき状態(Desired State)」を定義し、その実現をツールに任せる。この考え方を実現する仕組みが、次回扱う「module(モジュール)」です。
10. まとめ
-
shellは一見便利だが、二重の解釈プロセスにより挙動が安定しない。
-
発生するトラブルは使い方のミスではなく、Shellの「再解釈」という仕様に起因する。
-
安定した自動化のためには、コマンド実行ではなく
moduleによる「状態管理」への移行が不可欠である。
11. 次回予告
第1回では、shellモジュールが抱える「再解釈」という問題と、それによって生じる不確実性の仕組みを整理しました。
次回は、Shellの解釈プロセスを介さずにターゲットの状態を直接制御する仕組み、すなわちAnsibleの専用モジュールと 「状態管理(State Management)」 という設計思想を解説します。
次回:
【Ansible編】第2回:なぜmoduleを使うと安全になるのか
「命令」から「宣言」へ。Shell編で学んだ不確実性を、moduleの構造がどのように解消するのかを解説します。
📑 連載の移動
前の記事:【Ansible編編】第0回| 次の記事:【Ansible編】第2回 ※近日公開予定
📑 【Ansible編】全体のまとめはこちら
→ Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ ※近日公開予定
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📚12. 連載一覧: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を実務で使いこなすための設計思想を完成させる。 |