📚 Ansibleが理解できない理由はLinuxにあった【Ansible編】第10回:なぜAnsibleを正しく設計できるようになるのか
⓪ なぜShellの知識がないとAnsibleは壊れるのか
① なぜshellモジュールは危険なのか
② なぜmoduleを使うと安全になるのか
③ なぜ毎回changedになるのか
④ なぜ条件分岐が壊れるのか(Ansible編)
⑤ なぜ変数が意図通りに扱えないのか
⑥ なぜタスクの順序で壊れるのか
⑦ なぜエラー制御が破綻するのか
⑧ なぜ非同期・待機で失敗するのか
⑨ なぜPlaybookが読めないのか
⑩ なぜAnsibleを正しく設計できるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Ansible編】全体のまとめはこちら
「Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ」
📑 連載の移動
前の記事:【Ansible編】第9回 | 次の記事:【冪等性編】第0回
📋 目次
- 前回の振り返り(第9回)
- 逆引き辞典との連動
- はじめに(この記事のスタンス)
- 問題定義:なぜPlaybookは壊れるのか
- 構造整理:トラブルの発生レイヤー
- 原因分析:壊れる設計の共通点
- 設計:前提依存を排除するための具体戦略
- 本質:設計とは「前提を潰すこと」
- まとめ
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Ansible編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Ansible編の第10回」になります。
1. 前回の振り返り(第9回)
前回 は、Roleによる構造化を扱いました。
処理を一つのYAMLに詰め込むのではなく、責務ごとにディレクトリを分割することで、Playbookの可読性と保守性が上がります。
2. 逆引き辞典との連動
「設計上の原因」を特定したい場合は、以下の逆引き辞典を合わせて参照してください。
Ansibleが理解できない理由はLinuxにあった【Ansible編】:トラブル逆引き辞典(設計パターン版
逆引き辞典で該当ステップを特定し、本編の該当記事で仕組みを確認するという使い方を想定しています。
3. はじめに(この記事のスタンス)
これまでの連載では、Ansibleで発生するトラブルの原因を以下の3層に分けて扱ってきました。
OS :なぜ動かないのか(環境・権限・プロセス)
Shell :なぜそう見えるのか(入出力・展開・解釈)
Ansible:なぜ壊れるのか(実行モデル・設計)
本記事ではこれらをまとめ、「再現性が高く、壊れないPlaybookをどのように設計するか」 という点を整理します。
4. 問題定義:なぜPlaybookは壊れるのか
実務では、同じPlaybookでも環境によって成否が分かれたり、再実行時に失敗したりすることがあります。原因はPlaybookそのものの不備というより、設計が「不確定な前提」に依存していることがほとんどです。
Ansibleは宣言的なツールに見えますが、内部では以下のレイヤー構造で動作しています。
この階層のどこか一層でも「暗黙の前提」が崩れると、Playbookは意図通りに動作しなくなります。
5. 構造整理:トラブルの発生レイヤー
これまでの問題は、すべて以下のレイヤーで分類できます。トラブルはランダムに発生するのではなく、必ず特定の階層における不整合として現れます。
■ OS起因
| 事象 | 原因 | 参考記事 |
|---|---|---|
| command not found | PATH / 環境変数 | OS編第2回 |
| Permission denied | ユーザー / 権限 | OS編第4回 |
| SSH接続できない | 鍵認証 / ポート | OS編第3回 |
| サービスが起動しない | systemd | OS編第5回 |
| cronが動かない | 実行環境差 | OS編第6回 |
■ Shell起因
| 事象 | 原因 | 参考記事 |
|---|---|---|
| 結果が取れない | stdout / stderr | Shell編第1回 |
| 途中で消える | pipe / redirect | Shell編第2回 |
| grepで見つからない | stdin未渡し | Shell編第3回 |
| 条件分岐が壊れる | exit code誤解 | Shell編第6回 |
| 値が分割される | word splitting | Shell編第10回 |
| コマンドが壊れる | クォートミス | Shell編第9回 |
■ Ansible起因
| 事象 | 原因 | 参考記事 |
|---|---|---|
| 条件分岐失敗 | rc誤解 | Ansible編第4回 |
| タスク停止 | エラーハンドリング不足 | Ansible編第7回 |
| 不安定実行 | async未使用 | Ansible編第8回 |
| Playbookが読めない | Role未分割 | Ansible編第9回 |
| 変数が効かない | スコープ誤解 | Ansible編第5回 |
6. 原因分析:壊れる設計の共通点
壊れる設計に共通するのは、「内部構造への理解不足」と「暗黙の前提への依存」です。
① 環境依存(PATH)
- name: PHP確認
shell: php -v
内部動作:Ansibleは非ログインシェルで実行されるため、ユーザーのログイン時に設定される PATH を継承しません。
結果:環境によってコマンドが見つからず command not found となります。
詳しくは OS編第2回 にて解説しています。
② sudo環境の特権昇格
- name: 環境変数確認
shell: echo $API_KEY
become: yes
内部動作:become によるユーザー切り替え時、セキュリティ上の制約により環境変数は原則として初期化されます。
結果:定義済みの変数が消失し、空の値を参照します。
詳しくは OS編第4回 にて解説しています。
③ Shell解釈の動的評価(word splitting)
- name: mkdir
shell: mkdir /var/log/my app
内部動作:引数は文字列として /bin/sh に渡され、スペースが区切り文字として再解釈されます。
結果:一つのディレクトリではなく、二つの引数として処理されます。
詳しくは Shell編第10回 にて解説しています。
④ 実行プロセスの独立性
- name: 移動
shell: cd /opt
- name: 実行
shell: ./run.sh
内部動作:Ansibleはタスクごとに独立したプロセスを生成します。
結果:前段タスクの cd 状態は保持されず、カレントディレクトリで実行が試みられます。
詳しくは Ansible編第6回 にて解説しています。
7. 設計:前提依存を排除するための具体戦略
これらの「前提依存」を排除するには、対策(Tips)を適用するのではなく、なぜその設計が必要かを理解することが重要です。以下はすべて「対処法」ではなく、前提依存を排除するための設計上の判断です。
① 環境の明示化:PATHは保証されないという前提に立ち、絶対パス指定(/usr/bin/php)や environment キーワードで実行環境を固定します。
shell: /usr/bin/php -v
または
environment:
PATH: "/usr/local/bin:{{ ansible_env.PATH }}"
Ansibleは非ログインシェルで実行されるため、PATHは保証されません。詳しくは OS編第2回 にて解説しています。
② 環境変数の再注入:特権昇格時に変数がリセットされる特性を考慮し、タスク単位で必要な変数を定義します。
become: yes
environment:
API_KEY: "{{ vault_api_key }}"
become で環境変数はリセットされます。必要な値はタスク単位で再定義します。詳しくは OS編第4回 にて解説しています。
③ 文字列の保護:引数に含まれる空白が分割されないよう、変数は常にクォートで保護します。
shell: mkdir -p "{{ log_path }}"
Shellはスペースで分割するため、クォートなしでは意図しない引数になります。詳しくは Shell編第9回 にて解説しています。
④ 実行コンテキストの固定:プロセスの独立性を前提とし、args: chdir を用いてタスクごとに実行起点を明示します。
shell: ./run.sh
args:
chdir: /opt/app
タスクは独立プロセスのため、前のタスクの状態は引き継がれません。詳しくは Ansible編第6回 にて解説しています。
⑤ エラー伝播の適正化:パイプライン中の失敗を検知するため、set -o pipefail を指定し、シェルの評価方法を固定します。
shell: |
set -o pipefail
cat nofile | grep a
args:
executable: /bin/bash
Shellはパイプの最後のコマンドの結果しか見ないため、途中の失敗が無視されます。詳しくは Shell編第8回 にて解説しています。
⑥ モジュールを優先する:状態を扱う処理は shell に任せず、file や template などの専用モジュールで定義します。
file:
path: /tmp/file
state: touch
shell はコマンドを実行するだけで状態を管理しないため、実行のたびに結果が変わる可能性があります。詳しくは Ansible編第2回 にて解説しています。
8. 本質:設計とは「前提を潰すこと」
「想定するな、前提を潰せ」 ── これが本シリーズの結論です。ここで言う「前提」とは、OS / Shell / Ansibleそれぞれに存在する、暗黙の期待を指します。
例えば、以下のようなものです。
- OS:PATHは通っている「はず」、権限はある「はず」
- Shell:スペースは問題にならない「はず」、パイプは正しく動く「はず」
- Ansible:一度成功した処理は次も成功する「はず」
しかし実際には、これらの「はず」は通用しないことがあります。OSは環境差分を持ち、Shellは動的に解釈し、Ansibleはそれらの上で非対話・非ログイン環境として実行されるためです。
設計の出発点はこうなります。
OSの制約を理解する
↓
Shellの解釈を理解する
↓
Ansibleの実行モデルを理解する
↓
前提に依存しない構造を作る
重要なのは、「前提を消す」ことはできないという点です。環境差分やShellの挙動は本質的に存在し続けます。前提は消せないので、外に出さない構造を作ることが設計です。第9回で扱ったRoleは、まさにそのための仕組みです。Roleは単なるディレクトリ分割ではなく、不確定な要素を内部にまとめ、外部から切り離す構造です。
これは「手続き(How)に依存するか、定義(What)として固定するか」という設計上の違いです。
壊れる条件を事前に特定し、それが外部に波及しない構造を作ることが設計です。
- PATHが通っていなければ
command not foundになる → だから絶対パスで固定する - パイプの途中で失敗しても検知できない → だから
pipefailを指定する - 実行順序に依存すると再現性がなくなる → だから状態で制御する
「どう動くか」ではなく「どうあるべきか」を定義する構造が、Playbookを再現可能にします。
9. まとめ
- トラブルは OS / Shell / Ansible の3層の不整合として説明できる。
- 問題の主因は「暗黙の前提への依存」であり、対策は「前提の排除」にある。
- Playbookは「書く」ものではなく、構造から壊れないように「設計する」ものである。
以上が全10回を通じた結論です。
10. 次回予告
次のシリーズでは、「Ansibleは本当に冪等なのか」 をテーマに扱います。
「Ansibleは何度実行しても安全」という認識は広く浸透していますが、実際には冪等性が保証されないケースが多く存在します。次シリーズでは、その構造的な原因を全11回で掘り下げます。
- 新シリーズ目次一覧:「Ansibleは本当に冪等なのか」〜冪等性が崩れる構造と設計〜
| 回 | タイトル | 内容(概要) |
|---|---|---|
| 第0回 | なぜAnsibleは「何度実行しても安全」だと思われているのか | 冪等性(idempotency)の本来の意味を整理し、「同じコマンドを繰り返せる」ことと「状態が収束する」ことの違いを理解する。 |
| 第1回 | shellモジュールはなぜ“状態”を扱えないのか | shell/command が desired state を持てない理由を構造から整理する。changed_when は表示制御であり、冪等性保証ではないことを理解する。 |
| 第2回 | なぜAnsible moduleは「変更不要」を判断できるのか | file/copy/template などの module が、現在状態と desired state の差分比較によって動作していることを理解する。Ansibleが宣言的管理に見える理由を整理する。 |
| 第3回 | なぜファイル操作は簡単に非冪等になるのか | 改行コード、owner/group、Jinja2レンダリング差分など、「見えない差分」が changed を発生させる構造を理解する。 |
| 第4回 | なぜ“最新化”は冪等性を壊すのか | yum/apt の state: latest やバージョン未固定が、再実行ごとに状態を変化させる理由を理解する。desired state と「現在の最新版」は別物であることを学ぶ。 |
| 第5回 | なぜサービス制御は再起動ループを生むのか | service/systemd/handler の notify 連鎖を通して、「変更検知」がどのように状態遷移を引き起こすのかを理解する。reload/restart の違いも整理する。 |
| 第6回 | なぜlineinfileは“安全そうに見えて危険”なのか | lineinfile は「状態管理」ではなく「部分パッチ」である。正規表現・重複挿入・文脈依存編集によって冪等性が崩れる構造を理解する。 |
| 第7回 | なぜ同じPlaybookなのに環境ごとに結果が変わるのか | OS・ディストリビューション・systemd・Python・locale 差分によって、module の内部動作が変化することを理解する。 |
| 第8回 | なぜタスクの依存関係は冪等性を壊すのか | handler、register、条件分岐、タスク順序によって「前回実行結果」に依存したPlaybookが生まれる。状態遷移が閉じなくなる理由を整理する。 |
| 第9回 | 冪等性はどうやって検証すべきなのか | check mode の限界、diff mode、molecule、CI、自動テストを通して、「2回目 changed=0」をどう保証するのかを理解する。 |
| 第10回 | 「冪等に設計する」とは何を設計することなのか | imperative(手続き)ではなく declarative(状態宣言)としてPlaybookを設計する考え方を整理し、「壊れない構成管理」の原則を完成させる。 |
📑 連載の移動
前の記事:【Ansible編】第9回 | 次の記事:【冪等性編】第0回
📑 【Ansible編】全体のまとめはこちら
「Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ」
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📚11. 連載一覧: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を実務で使いこなすための設計思想を完成させる。 |