📚 Ansibleが理解できない理由はLinuxにあった【Ansible編】第6回:なぜタスクの順序で壊れるのか
⓪ なぜShellの知識がないとAnsibleは壊れるのか
① なぜshellモジュールは危険なのか
② なぜmoduleを使うと安全になるのか
③ なぜ毎回changedになるのか
④ なぜ条件分岐が壊れるのか(Ansible編)
⑤ なぜ変数が意図通りに扱えないのか
⑥ なぜタスクの順序で壊れるのか
⑦ なぜエラー制御が破綻するのか
⑧ なぜ非同期・待機で失敗するのか
⑨ なぜPlaybookが読めないのか
⑩ なぜAnsibleを正しく設計できるようになるのか
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📑 【Ansible編】全体のまとめはこちら
「Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ」
📑 連載の移動
前の記事:【Ansible編】第5回 | 次の記事:【Ansible編】第7回
📋 目次
- 前回の振り返り(第5回)
- 逆引き辞典との連動
- はじめに(この記事のスタンス)
- 問題定義:Shellの「逐次実行」とAnsibleの「タスク実行」の乖離
- 構造解剖:プロセスの分離とステートレスな実行構造
- 原因分析:ディレクトリ移動や環境変数が「リセット」される理由
- 設計:chdirとenvironmentキーワードによるコンテキストの明示
- まとめ:副作用に頼らないタスク設計を
- 次回予告
- 連載一覧:Ansibleが理解できない理由はLinuxにあった【Ansible編】
※本記事の位置とシリーズ全体の関係を先に確認します。
シリーズ全体構造(学習 × 問題解決)
本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。
この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。
📍 現在の位置
現在はこの図の「Ansible編の第6回」になります。
1. 前回の振り返り(第5回)
前回 は、Ansibleにおける「変数の優先順位」と「遅延評価」のメカニズムを解説しました。変数は定義された瞬間ではなく、使用される瞬間に評価されるため、実行時のコンテキストによって値が変動する可能性があることを確認しました。
2. 逆引き辞典との連動
「設計上の原因」を特定したい場合は、以下の逆引き辞典を活用してください。
Ansibleが理解できない理由はLinuxにあった【Ansible編】:トラブル逆引き辞典(設計パターン版)
- まず「逆引き辞典」で該当ステップを特定する
- 次に「本編(各連載記事)」で仕組みを理解する
3. はじめに(この記事のスタンス)
AnsibleのPlaybookを記述する際、多くのエンジニアがShellスクリプトのように「上から下へ、環境を引き継ぎながら実行される」というイメージを持ちます。しかし実際には、Ansibleの各タスクはそれぞれ独立したプロセスとしてリモートホスト上で実行されます。
本記事では、この「タスク間のプロセス分離」という構造を掘り下げ、なぜディレクトリ移動(cd)や環境変数の設定が後続のタスクに引き継がれないのか、その根本的な理由を明らかにします。ステートレスな実行構造を正しく理解することで、順序依存のない堅牢な自動化を実現するための設計手法を解説します。
4. 問題定義:Shellの「逐次実行」とAnsibleの「タスク実行」の乖離
実務では、以下のような順序に起因する不具合が発生することがあります。
-
カレントディレクトリの消失: shell モジュールで
cdを実行しても、次のタスクでは元のディレクトリ(ホームディレクトリ等)に戻ってしまいます。 -
環境変数のリセット:
exportコマンドで設定した環境変数は後続のタスクに引き継がれないため、コマンドが失敗する原因となります。 -
暗黙的な副作用への依存: 前のタスクで作成したファイルやディレクトリの存在を前提にしている場合、条件分岐やスキップによってそのタスクが実行されないと、Playbook全体の整合性が崩れる可能性があります。
これらの問題は、Ansibleが各タスクを 「独立した実行コンテキスト(プロセス)」として扱う設計 に起因しています。Shellスクリプトのように「前の状態を引き継ぐ前提」でPlaybookを記述すると、意図しない挙動を引き起こすため注意が必要です。
🔍 裏付けとなる公式ドキュメント
- ansible.builtin.shell module – Execute shell commands on targets
- ansible.builtin.command module – Execute commands on targets
- Setting the remote environment
- Ansible tips and tricks
5. 構造解剖:プロセスの分離とステートレスな実行構造
Ansibleがタスクを実行する際、内部ではタスクごとにモジュールの転送と実行が行われます。なお、SSH接続はControlPersistによって既存の接続が再利用される場合があります。
🔄 実行プロセスの視覚化
6. 原因分析:ディレクトリ移動や環境変数が「リセット」される理由
Linuxのプロセス管理では、子プロセスが行ったディレクトリ移動(chdir)や環境変数の変更は、親プロセスや他の独立したプロセスには影響を与えません。
Ansibleの各タスクはそれぞれ新しいプロセスとして実行されます。そのため、あるタスクが終了した時点で、そのプロセス内で保持されていたカレントディレクトリや環境変数といった状態は破棄され、次のタスクでは常に初期状態から処理が開始されます。
💡 重要なポイント
Shellスクリプトのように「前の状態を引き継ぐ前提」でタスクを記述すると、Ansibleでは意図しない挙動を引き起こします。
【検証内容】プロセス分離による「状態リセット」の再現
実際にタスク間で状態がリセットされる様子をPlaybookで確認してみましょう。以下のPlaybookは、「あるタスクで移動・設定した状態が、後続のタスクに引き継がれるか」をリモートノード上で検証するものです。
【コントローラーノード側】
ファイル名: verify_reset.yml
- name: プロセス分離の検証
hosts: test_servers
gather_facts: false
tasks:
- name: "1. ディレクトリ移動と環境変数の設定(このタスク内のみ有効)"
ansible.builtin.shell: |
cd /tmp
export MY_ENV_VAR="HelloAnsible"
echo "【タスク1内部】現在のディレクトリ: $(pwd)"
echo "【タスク1内部】環境変数: $MY_ENV_VAR"
register: task1_result
- name: "2. 後続タスクでのディレクトリ確認"
ansible.builtin.shell: pwd
register: pwd_result
- name: "3. 後続タスクでの環境変数確認"
ansible.builtin.shell: echo "値は[$MY_ENV_VAR]です"
register: env_result
- name: 各タスクの実行結果を出力
ansible.builtin.debug:
msg:
- "--- タスク1の実態 ---"
- "{{ task1_result.stdout_lines }}"
- "--- タスク2(引き継ぎ確認) ---"
- "現在のディレクトリ: {{ pwd_result.stdout }}"
- "--- タスク3(引き継ぎ確認) ---"
- "環境変数の結果: {{ env_result.stdout }}"
実行コマンド
ansible-playbook -i inventory.ini verify_reset.yml
▼ 実行結果
PLAY [プロセス分離の検証] *******************************************************************************************************************************************************************
TASK [1. ディレクトリ移動と環境変数の設定(このタスク内のみ有効)] **************************************************************************************************************************
changed: [192.168.1.22]
changed: [192.168.1.21]
TASK [2. 後続タスクでのディレクトリ確認] ****************************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]
TASK [3. 後続タスクでの環境変数確認] ********************************************************************************************************************************************************
changed: [192.168.1.22]
changed: [192.168.1.21]
TASK [各タスクの実行結果を出力] *************************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": [
"--- タスク1の実態 ---",
[
"【タスク1内部】現在のディレクトリ: /tmp",
"【タスク1内部】環境変数: HelloAnsible"
],
"--- タスク2(引き継ぎ確認) ---",
"現在のディレクトリ: /home/ansibleuser",
"--- タスク3(引き継ぎ確認) ---",
"環境変数の結果: 値は[]です"
]
}
ok: [192.168.1.22] => {
"msg": [
"--- タスク1の実態 ---",
[
"【タスク1内部】現在のディレクトリ: /tmp",
"【タスク1内部】環境変数: HelloAnsible"
],
"--- タスク2(引き継ぎ確認) ---",
"現在のディレクトリ: /home/ansibleuser2",
"--- タスク3(引き継ぎ確認) ---",
"環境変数の結果: 値は[]です"
]
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.1.22 : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
結果
-
タスク1内では一時的に有効になっている
--- タスク1の実態 ---を見ると、同一タスク(プロセス)内では/tmpへのディレクトリ移動と環境変数MY_ENV_VAR="HelloAnsible"の設定がどちらも正常に機能していることが確認できます。 -
次のタスクではディレクトリが「ホーム」にリセットされる
一方、--- タスク2(引き継ぎ確認) ---では、カレントディレクトリが各ユーザーの初期ホームディレクトリ(192.168.1.21 は/home/ansibleuser、192.168.1.22 は/home/ansibleuser2)に戻っています。 -
環境変数も「未定義(空)」にリセットされる
--- タスク3(引き継ぎ確認) ---では、環境変数の結果が値は[]ですとなっており、前のタスクで設定したはずの変数が引き継がれていないことが確認できます。
🔍 裏付けとなる公式ドキュメント
- ansible.builtin.shell module – Execute shell commands on targets
- Setting the remote environment
- Ansible module architecture
7. 設計:chdirとenvironmentキーワードによるコンテキストの明示
専用モジュールが使えず、やむを得ず shell モジュールで独自のスクリプトやコマンドを実行する場合は、タスク間のプロセス分離を前提とした設計が必要です。
実行環境(カレントディレクトリや環境変数)を指定する際は、Shellスクリプトのように cd や export を使うのではなく、Ansibleが提供するキーワードを明示的に使用します。
また、当ブログで一貫して推奨している通り、呼び出すコマンドやスクリプトは必ず「フルパス」で指定することが、堅牢な自動化の基本原則です。
❌ 不適切な設計(状態の引き継ぎを期待している)
- name: ディレクトリ移動
shell: cd /opt/myapp
- name: 実行 (失敗する)
shell: ./start.sh
✅ 正しい設計(コンテキストの明示)
- name: 実行環境とフルパスを明示して実行
shell: /opt/myapp/bin/start.sh # コマンドは必ずフルパスで指定する
args:
chdir: /opt/myapp # スクリプトが動作するカレントディレクトリを固定する
environment:
APP_ENV: production # そのタスクのプロセスにのみ必要な環境変数を注入
📌 設計意図
-
args: chdir
→ タスクが実行されるカレントディレクトリを確実に固定します。スクリプト内で相対パスを使用している場合に特に重要です。 -
environment
→ 指定した環境変数をそのタスクのプロセスにのみ適用します。他のタスクへの影響がなく、スコープを局所化できます。
「状態を引き継ぐ」のではなく、「実行環境をタスクごとに毎回明示する」ことがAnsibleの設計原則です。
【検証内容】「実行コンテキスト」の明示
第6セクションで確認した「ディレクトリ移動の引き継ぎ」と「環境変数の引き継ぎ」の問題が、フルパス指定とコンテキストの明示によってどのように解決されるかを検証します。
7-1. 【検証前の準備】リモートノード側への資材配置
まずは検証の前提条件として、コントローラーノード側で実行スクリプトを用意し、事前にリモートノード側へに配置(転送)します。
【コントローラーノード側】
ファイル名(実行スクリプト): files/start.sh
#!/bin/bash
# このスクリプトが実行された時の情報を出力する
echo "--- start.sh が実行されました ---"
echo "実行ディレクトリ: $(pwd)"
echo "適用された環境変数 APP_ENV: $APP_ENV"
ファイル名(検証準備用Playbook): setup_script.yml
- name: 【検証前準備】リモートノードへのスクリプト配置
hosts: test_servers
gather_facts: false
become: true
tasks:
- name: 配置先のディレクトリを作成
ansible.builtin.file:
path: /opt/myapp/bin
state: directory
mode: '0755'
- name: スクリプトをリモートノードへ転送(実行権限を付与)
ansible.builtin.copy:
src: files/start.sh
dest: /opt/myapp/bin/start.sh
mode: '0755' # 確実に実行できるよう権限を設定
実行コマンド
ansible-playbook -i inventory.ini setup_script.yml
▼ 実行結果
PLAY [【検証前準備】リモートノードへのスクリプト配置] ***************************************************************************************************************************************
TASK [配置先のディレクトリを作成] ***********************************************************************************************************************************************************
ok: [192.168.1.21]
ok: [192.168.1.22]
TASK [スクリプトをリモートノードへ転送(実行権限を付与)] ***********************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.1.22 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
【リモートノード側】
※以下の通りスクリプトが配置されていることを確認済み
- 192.168.1.21(ansibleuser)
[ansibleuser@localhost ~]$ ls -l /opt/myapp/bin
合計 4
-rwxr-xr-x. 1 root root 225 6月 4 17:14 start.sh
- 192.168.1.22(ansibleuser2)
[ansibleuser2@localhost ~]$ ls -l /opt/myapp/bin
合計 4
-rwxr-xr-x. 1 root root 225 6月 4 17:14 start.sh
7-2. 【検証内容】コンテキスト明示Playbookの実行
本題である「正しい設計」を用いた検証用Playbookを実行します。
【コントローラーノード側】
ファイル名: verify_context.yml
- name: コンテキスト明示の検証
hosts: test_servers
gather_facts: false
become: true
tasks:
# -----------------------------------------------------------------
# タスク1: 正確なカレントディレクトリと環境変数を指定し、フルパスでスクリプトを叩く
# -----------------------------------------------------------------
- name: "正確なコンテキストを指定して独自の処理を実行する"
ansible.builtin.shell: /opt/myapp/bin/start.sh # 環境に依存しないようフルパスで指定
args:
chdir: /opt/myapp # スクリプトが動作する「カレントディレクトリ」を固定
environment:
APP_ENV: production # このタスクのプロセスにのみ有効な環境変数を注入
register: shell_result # 実行結果を変数に格納
# -----------------------------------------------------------------
# タスク2: 格納した変数から、スクリプトの出力結果(標準出力)をデバッグ表示する
# -----------------------------------------------------------------
- name: 実行結果を出力
ansible.builtin.debug:
msg:
- "--- タスクの実行結果 ---"
- "{{ shell_result.stdout_lines }}" # 見やすく改行されたログを出力
実行コマンド
ansible-playbook -i inventory.ini verify_context.yml
▼ 実行結果
PLAY [コンテキスト明示の検証] ***************************************************************************************************************************************************************
TASK [正確なコンテキストを指定して独自の処理を実行する] *************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]
TASK [実行結果を出力] ***********************************************************************************************************************************************************************
ok: [192.168.1.21] => {
"msg": [
"--- タスクの実行結果 ---",
[
"--- start.sh が実行されました ---",
"実行ディレクトリ: /opt/myapp",
"適用された環境変数 APP_ENV: production"
]
]
}
ok: [192.168.1.22] => {
"msg": [
"--- タスクの実行結果 ---",
[
"--- start.sh が実行されました ---",
"実行ディレクトリ: /opt/myapp",
"適用された環境変数 APP_ENV: production"
]
]
}
PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.1.22 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
検証結果まとめ
第6セクションで確認した問題が、今回の検証ではすべて解決されていることが確認できます。
-
フルパス指定(
/opt/myapp/bin/start.sh)
リモートノードの$PATHに依存しないため、環境の差異による「コマンドが見つからない」エラーが発生せず、確実にスクリプトを起動できています。 -
chdir(
/opt/myapp)
タスクごとにカレントディレクトリが初期化される仕様に対し、chdirによってスクリプトの作業ディレクトリを確実に固定できています。 -
environment(
APP_ENV: production)
そのタスクのプロセス(およびそこから呼び出されたスクリプト)に対してのみ、環境変数が適切に適用されています。
🔍 裏付けとなる公式ドキュメント
8. まとめ:副作用に頼らないタスク設計を
本記事では、Ansibleにおける実行コンテキストの挙動と、それに応じた正しい設計について解説しました。重要なポイントは以下の3点です。
-
タスクごとにプロセスはリセットされる
Ansibleの各タスクは独立したプロセスとして実行されるため、前のタスクで行ったディレクトリ移動や環境変数の設定は次のタスクに引き継がれません。 -
「フルパス + コンテキスト明示」が原則
やむを得ず shell モジュールを使う場合は、フルパスでコマンドを指定し、作業ディレクトリや環境変数はchdir・environmentでタスクごとに明示します。 -
タスク同士を疎結合に保つ
前のタスクの状態への依存を排除し、各タスクが単独(スタンドアロン)で成立する設計にすることが、環境の差異に左右されない安定した自動化の基本です。
9. 次回予告
第6回では、タスク実行プロセスの分離と、それによる状態リセットの仕組みを解説しました。
実行環境と順序が整理された次に立ちはだかるのが、「エラー」への対応です。タスクが失敗した際、Ansibleはデフォルトで処理を中断しますが、実務では「失敗しても続行したい」「特定の戻り値をエラーとみなしたい」といった柔軟な制御が求められます。
- 「なぜエラーが起きているのに止まらないのか?」
- 「なぜ正常終了したのにエラー扱いされるのか?」
Ansibleのエラーハンドリングと戻り値の評価ロジックを深掘りします。
次回:【Ansible編】第7回:なぜエラー制御が破綻するのか
📑 連載の移動
前の記事:【Ansible編】第5回 | 次の記事:【Ansible編】第7回
📑 【Ansible編】全体のまとめはこちら
「Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ」
🗺️ 初めての方・シリーズの全体像を知りたい方はこちら
本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。
📚10. 連載一覧: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を実務で使いこなすための設計思想を完成させる。 |