T-CREATOR

Ansible 実行モデル解体新書:コントローラからターゲットまでの裏側

Ansible 実行モデル解体新書:コントローラからターゲットまでの裏側

Ansible を使って日々のインフラ運用を自動化している方は多いでしょう。playbook を書いて ansible-playbook コマンドを実行すれば、魔法のように複数のサーバーに設定が適用される。便利ですよね。

しかし、コマンドを実行してから実際にターゲットノードで処理が完了するまで、内部では一体何が起きているのでしょうか。SSH で接続して、何か Python スクリプトを転送して実行しているらしいことは知っていても、詳しいプロセスまでは把握していない方も多いかもしれません。

本記事では、Ansible コントローラからターゲットノードまでの実行プロセスを徹底的に解剖します。内部動作を理解することで、トラブルシューティングやパフォーマンスチューニングの精度が格段に向上するはずです。

背景

Ansible のアーキテクチャ概要

Ansible はエージェントレスな IT 自動化ツールとして設計されています。Chef や Puppet などの従来の構成管理ツールとは異なり、ターゲットノードに常駐エージェントをインストールする必要がありません。

この設計思想により、以下のメリットが生まれました。

  • セットアップの簡素化(SSH 接続さえあれば利用可能)
  • 管理対象サーバーのリソース消費削減
  • セキュリティリスクの低減(常駐プロセスなし)

では、エージェントなしでどのように遠隔地のサーバーを制御しているのでしょうか。

コントローラとターゲットノードの関係

Ansible の実行環境は、大きく 2 つの要素で構成されます。

以下の図で、基本的な関係性を確認しましょう。

mermaidflowchart LR
    controller["Ansibleコントローラ<br/>(実行元)"]
    target1["ターゲットノード1<br/>(管理対象)"]
    target2["ターゲットノード2<br/>(管理対象)"]
    target3["ターゲットノード3<br/>(管理対象)"]

    controller -->|"SSH接続"| target1
    controller -->|"SSH接続"| target2
    controller -->|"SSH接続"| target3

Ansible コントローラは playbook を解析し、各タスクを実行するための指示をターゲットノードに送信します。この通信は主に SSH プロトコルを使用しており、追加のポート開放や特別なネットワーク設定は不要です。

Python への依存関係

Ansible の実行モデルを語る上で、Python への依存関係を理解することは欠かせません。

#コンポーネントPython 要件備考
1Ansible コントローラPython 3.9 以上推奨ansible-core パッケージが必要
2ターゲットノードPython 2.7 または 3.5 以上raw/script モジュール以外
3モジュール実行環境ターゲットの Python自動検出される

ターゲットノードに Python がインストールされていれば、Ansible は自動的にそれを検出して利用します。このシンプルな前提条件が、Ansible の手軽さを支えているのですね。

課題

ブラックボックス化した実行プロセス

Ansible は使いやすさを追求した結果、内部の複雑な処理が抽象化されています。これは初心者にとってはメリットですが、以下のような課題も生み出しました。

トラブルシューティングの困難さ

エラーが発生した際、どの段階で問題が起きたのか判断できないケースが頻発します。SSH 接続の失敗なのか、モジュール転送の問題なのか、実行時のエラーなのか。

パフォーマンス最適化の難しさ

大量のホストに対して実行する際、どこがボトルネックになっているのか特定するのは容易ではありません。並列実行数の調整も、内部動作を理解していないと効果的に設定できないでしょう。

実行モデルの理解不足による問題事例

実際の運用現場では、実行モデルへの理解不足が原因で以下のような問題が報告されています。

以下の図で、よくある問題発生パターンを整理します。

mermaidflowchart TD
    start["Playbookを実行"] --> check1{"SSH接続<br/>可能?"}
    check1 -->|"No"| error1["エラー: 接続失敗"]
    check1 -->|"Yes"| check2{"Python<br/>インストール済み?"}
    check2 -->|"No"| error2["エラー: Python未検出"]
    check2 -->|"Yes"| check3{"モジュール転送<br/>成功?"}
    check3 -->|"No"| error3["エラー: 権限不足/<br/>ディスク容量不足"]
    check3 -->|"Yes"| check4{"モジュール実行<br/>成功?"}
    check4 -->|"No"| error4["エラー: 実行時エラー"]
    check4 -->|"Yes"| success["タスク完了"]

各段階で異なるエラーが発生する可能性がありますが、エラーメッセージだけでは原因特定が難しい場合も多いのです。

一時ファイルとセキュリティ懸念

Ansible は実行時にターゲットノード上に一時ファイルを作成しますが、この動作が以下のセキュリティ懸念を生むことがあります。

  • 一時ファイルの残留リスク
  • ファイル権限の不適切な設定
  • 機密情報の一時的な露出

これらの問題は、実行モデルを正確に理解していれば回避可能です。次のセクションで、詳しく見ていきましょう。

解決策

Ansible 実行フローの全体像

Ansible コマンド実行から結果取得までのプロセスを、段階的に解説します。全体フローは以下の 5 つのステージに分類できます。

mermaidflowchart TD
    stage1["Stage 1:<br/>Playbook解析"] --> stage2["Stage 2:<br/>接続確立"]
    stage2 --> stage3["Stage 3:<br/>モジュール転送"]
    stage3 --> stage4["Stage 4:<br/>リモート実行"]
    stage4 --> stage5["Stage 5:<br/>結果取得と<br/>クリーンアップ"]

    stage5 --> decision{"次のタスク<br/>存在?"}
    decision -->|"Yes"| stage2
    decision -->|"No"| finish["実行完了"]

この 5 段階のステージを理解することで、どの時点で何が行われているのか正確に把握できるようになります。

Stage 1: Playbook 解析とタスク準備

最初のステージでは、Ansible コントローラ上で playbook と inventory ファイルが解析されます。

Playbook の読み込み

Ansible コントローラは、YAML ファイルとして記述された playbook をパースして内部データ構造に変換します。

yaml# サンプルplaybook
---
- name: Webサーバーのセットアップ
  hosts: webservers
  become: yes
  tasks:
    - name: Nginxのインストール
      apt:
        name: nginx
        state: present

この YAML ファイルが以下の処理を経て、実行可能な形式に変換されます。

python# 内部的なPlaybookオブジェクトの生成(簡略化)
from ansible.playbook import Playbook
from ansible.inventory.manager import InventoryManager

# Playbookをロード
playbook = Playbook.load(
    playbook_path,
    variable_manager=variable_manager,
    loader=loader
)

Inventory の解析

同時に、inventory ファイルから対象ホストのリストが読み込まれます。

ini# inventory例
[webservers]
web01.example.com ansible_host=192.168.1.10
web02.example.com ansible_host=192.168.1.11

[webservers:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3

これらの情報が組み合わされ、どのホストに対してどのタスクを実行するかが決定されます。

Stage 2: SSH 接続の確立

次に、コントローラからターゲットノードへの SSH 接続が確立されます。

接続プラグインの選択

Ansible は複数の接続方式をサポートしており、デフォルトではsshプラグインが使用されます。

#接続プラグイン用途特徴
1ssh一般的なリモート接続OpenSSH を利用
2paramikoPython ベース接続依存性が少ない
3localローカル実行SSH 不要
4winrmWindows 管理WinRM 経由

SSH 接続の詳細プロセス

SSH 接続時には、以下の順序で認証が試行されます。

python# SSH接続時の認証フロー(概念的な表現)
connection_methods = [
    'ssh_key_authentication',      # 1. SSH鍵認証
    'ssh_agent_authentication',    # 2. SSHエージェント
    'password_authentication'      # 3. パスワード認証
]

接続が確立されると、Ansible はControlPersist機能を活用して SSH 接続を再利用します。これにより、複数タスク実行時の接続オーバーヘッドが大幅に削減されるのです。

接続の永続化設定

SSH 設定ファイルまたは ansible.cfg で接続永続化を制御できます。

ini# ansible.cfg での設定例
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True

pipelining オプションを有効にすると、さらなる高速化が実現します。

Stage 3: モジュール転送とラッピング

SSH 接続確立後、実行するモジュールがターゲットノードに転送されます。このプロセスが、Ansible の実行モデルで最も複雑な部分です。

モジュールのラッピング処理

Ansible モジュールは、単体では動作しません。実行前に「ラッパーコード」で包まれる必要があります。

以下のコードで、モジュールラッピングの概念を示します。

python# モジュールのラッピング処理(簡略化)
def wrap_module(module_name, module_args):
    """
    モジュールを実行可能な形式にラップする
    """
    # 1. モジュール本体を読み込み
    module_code = load_module_source(module_name)

    # 2. 引数をJSON化
    args_json = json.dumps(module_args)

    # 3. ラッパーテンプレートに埋め込み
    wrapper_template = get_wrapper_template()

    return wrapper_template.format(
        module_code=module_code,
        args=args_json
    )

このラッピングにより、モジュールは独立した Python スクリプトとして実行可能になります。

一時ファイルの作成

ラップされたモジュールコードは、ターゲットノード上の一時ディレクトリに配置されます。

python# 一時ディレクトリの決定ロジック
import os
import tempfile

def get_remote_tmp_dir(remote_user):
    """
    リモート環境での一時ディレクトリパスを決定
    """
    # 環境変数から取得を試行
    tmp_candidates = [
        os.environ.get('ANSIBLE_REMOTE_TMP'),
        f'/home/{remote_user}/.ansible/tmp',
        '/tmp/.ansible'
    ]

    for tmp_path in tmp_candidates:
        if tmp_path and can_write(tmp_path):
            return tmp_path

    # フォールバック
    return tempfile.gettempdir()

デフォルトでは、~​/​.ansible​/​tmp​/​ansible-tmp-<timestamp>-<random>​/​ といったパスが使用されます。

モジュールファイルの転送

作成された一時ディレクトリに、モジュールスクリプトが SCP/SFTP 経由で転送されます。

bash# 実際の転送コマンド例(内部的に実行される)
# ファイル名: AnsiballZ_<module_name>.py

scp /tmp/local-ansible-tmp/AnsiballZ_apt.py \
    deploy@192.168.1.10:~/.ansible/tmp/ansible-tmp-1234567890.12-3456/AnsiballZ_apt.py

ファイル名にAnsiballZというプレフィックスが付くのは、ラップされたモジュールであることを示す命名規則です。

Stage 4: リモート実行とモジュール動作

モジュール転送後、ターゲットノード上で Python インタプリタを使って実行されます。

Python インタプリタの検出

Ansible は、ターゲットノード上で利用可能な Python インタプリタを自動検出します。

python# Python検出の優先順位
python_interpreters = [
    '/usr/bin/python3',
    '/usr/bin/python',
    '/usr/bin/python2.7',
    '/usr/local/bin/python3',
]

for interpreter in python_interpreters:
    if os.path.exists(interpreter):
        return interpreter

inventory 内でansible_python_interpreter変数が指定されている場合は、その値が優先されます。

モジュールの実行コマンド

検出された Python インタプリタで、転送されたモジュールスクリプトが実行されます。

bash# リモートノード上で実行されるコマンド
/usr/bin/python3 \
    ~/.ansible/tmp/ansible-tmp-1234567890.12-3456/AnsiballZ_apt.py && \
    sleep 0

このコマンドが SSH 経由で送信され、リモートで実行されるのです。

モジュール内部の処理フロー

モジュール実行時、以下の処理が順次行われます。

mermaidsequenceDiagram
    participant Controller as Ansibleコントローラ
    participant SSH as SSH接続
    participant Target as ターゲットノード
    participant Module as モジュール
    participant System as システム

    Controller->>SSH: モジュール実行コマンド送信
    SSH->>Target: コマンド転送
    Target->>Module: Pythonでモジュール起動
    Module->>Module: 引数を解析
    Module->>Module: 事前チェック実行
    Module->>System: システム操作(例:apt install)
    System-->>Module: 操作結果
    Module->>Module: 結果をJSON化
    Module-->>Target: 標準出力にJSON出力
    Target-->>SSH: 実行結果
    SSH-->>Controller: 結果を返送

モジュールは実行結果をJSON 形式で標準出力に出力します。これが Ansible コントローラに返送される仕組みです。

実行結果の JSON 構造

モジュールが出力する JSON 構造は、以下のような形式になっています。

json{
  "changed": true,
  "msg": "パッケージがインストールされました",
  "stdout": "nginx is already installed",
  "stderr": "",
  "rc": 0,
  "invocation": {
    "module_args": {
      "name": "nginx",
      "state": "present"
    }
  }
}

この構造化されたデータにより、Ansible は実行結果を正確に判断できます。

Stage 5: 結果取得とクリーンアップ

最終ステージでは、実行結果の取得と一時ファイルの削除が行われます。

標準出力のキャプチャ

モジュールの標準出力が SSH 経由でコントローラに返送されます。

python# 実行結果の取得処理(簡略化)
def get_module_result(ssh_connection):
    """
    SSH経由でモジュール実行結果を取得
    """
    # 標準出力を取得
    stdout, stderr, return_code = ssh_connection.exec_command_result()

    # JSON文字列をパース
    try:
        result = json.loads(stdout)
    except json.JSONDecodeError:
        result = {
            'failed': True,
            'msg': 'モジュール出力のパースに失敗',
            'stdout': stdout,
            'stderr': stderr
        }

    return result

正常に JSON パースできれば、結果が構造化データとして扱われます。

一時ファイルの削除

セキュリティとディスク容量の観点から、使用済みの一時ファイルは即座に削除されます。

bash# クリーンアップコマンド例
rm -rf ~/.ansible/tmp/ansible-tmp-1234567890.12-3456/

この削除処理は、モジュール実行直後に自動的に行われます。ただし、デバッグ目的で一時ファイルを残したい場合は、環境変数で制御可能です。

bash# 一時ファイルを残す設定
export ANSIBLE_KEEP_REMOTE_FILES=1
ansible-playbook site.yml

この設定により、トラブルシューティング時に転送されたモジュールの内容を確認できます。

具体例

実際のコマンド実行をトレースする

理論だけでは理解しづらいので、実際の ansible-playbook コマンド実行をトレースしてみましょう。

サンプル Playbook の準備

以下のシンプルな playbook を使用します。

yaml# site.yml
---
- name: ファイル作成タスク
  hosts: target
  tasks:
    - name: テストファイルを作成
      copy:
        content: 'Hello from Ansible'
        dest: /tmp/ansible-test.txt
        mode: '0644'
ini# inventory
[target]
testserver ansible_host=192.168.1.100 ansible_user=ansible

詳細ログの有効化

実行プロセスを可視化するため、最大レベルの詳細ログを有効にします。

bash# 詳細ログレベルでPlaybook実行
ansible-playbook -i inventory site.yml -vvvv

-vvvvオプションにより、内部処理が詳細に出力されます。

ログ出力の解析

実行時のログ出力から、各ステージを確認できます。

接続確立のログ例

ini<192.168.1.100> ESTABLISH SSH CONNECTION FOR USER: ansible
<192.168.1.100> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s
  -o StrictHostKeyChecking=no -o Port=22
  -o 'IdentityFile="/home/user/.ssh/id_rsa"'
  -o KbdInteractiveAuthentication=no
  -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey
  -o PasswordAuthentication=no
  -o 'User="ansible"'
  -o ConnectTimeout=10 192.168.1.100 '/bin/sh -c ...'

このログから、SSH コマンドのオプションや認証方式が確認できますね。

モジュール転送のログ例

swift<192.168.1.100> PUT /tmp/ansible-local-12345/tmpAbCdEf TO
  /home/ansible/.ansible/tmp/ansible-tmp-1701234567.89-67890/AnsiballZ_copy.py

ローカルの一時ファイルが、リモートの一時ディレクトリに転送されています。

モジュール実行のログ例

swift<192.168.1.100> EXEC /bin/sh -c '/usr/bin/python3
  /home/ansible/.ansible/tmp/ansible-tmp-1701234567.89-67890/AnsiballZ_copy.py
  && sleep 0'

Python インタプリタで、AnsiballZ_copy.py が実行されていることが分かります。

クリーンアップのログ例

swift<192.168.1.100> EXEC /bin/sh -c 'rm -f -r
  /home/ansible/.ansible/tmp/ansible-tmp-1701234567.89-67890/ > /dev/null 2>&1
  && sleep 0'

実行後、一時ディレクトリが削除されています。

パイプライニングによる最適化

デフォルトの実行フローでは、モジュール転送と実行が別々の SSH コマンドで行われます。しかし、pipeliningを有効にすると、このプロセスが最適化されるのです。

デフォルト動作との比較

以下の表で、両者の違いを整理しました。

#項目デフォルト(pipelining=False)pipelining=True
1SSH 接続数タスクごとに複数回タスクごとに 1 回
2ファイル転送SCP で転送標準入力経由
3一時ファイル作成される作成されない
4実行速度標準高速(最大 5 倍)
5requiretty 制約影響なし無効化が必要

pipelining の有効化

ansible.cfg で設定を変更します。

ini# ansible.cfg
[defaults]
host_key_checking = False

[ssh_connection]
pipelining = True

sudoers の設定調整

pipelining を使用する場合、ターゲットノードの sudoers 設定を調整する必要があります。

bash# /etc/sudoers の編集(visudo コマンド使用)
# requiretty を無効化
Defaults !requiretty

# または、特定ユーザーのみ例外設定
Defaults:ansible !requiretty

この設定により、擬似端末なしで sudo コマンドが実行可能になります。

パフォーマンス測定

実際に pipelining の効果を測定してみましょう。

bash# pipelining無効時の実行時間測定
time ansible-playbook -i inventory site.yml

# 出力例
# real    0m12.345s
bash# pipelining有効時の実行時間測定
time ansible-playbook -i inventory site.yml

# 出力例(大幅に短縮)
# real    0m2.891s

タスク数が増えるほど、pipelining の効果は顕著になります。

become(権限昇格)時の実行フロー

多くの管理タスクでは、root 権限が必要です。become: yesを指定した場合の実行フローを見てみましょう。

become の基本設定

yaml# playbook with become
---
- name: システム設定変更
  hosts: target
  become: yes
  become_method: sudo
  become_user: root
  tasks:
    - name: システムパッケージ更新
      apt:
        update_cache: yes

権限昇格のプロセスフロー

become 使用時は、モジュール実行コマンドが sudo でラップされます。

mermaidflowchart TD
    start["SSH接続"] --> transfer["モジュール転送"]
    transfer --> wrap["sudoコマンドで<br/>ラップ"]
    wrap --> execute["権限昇格して実行"]
    execute --> result["結果取得"]
    result --> cleanup["クリーンアップ<br/>(sudo権限で削除)"]

実際の実行コマンドは以下のようになります。

bash# become使用時の実行コマンド例
sudo -H -S -n -u root /bin/sh -c 'echo BECOME-SUCCESS-<random> ;
  /usr/bin/python3 /home/ansible/.ansible/tmp/ansible-tmp-1234567890/AnsiballZ_apt.py'

BECOME-SUCCESS-<random>は、権限昇格が成功したことを示すマーカーです。

パスワード認証の処理

sudo 実行時にパスワードが必要な場合、以下の方法で指定できます。

bash# 対話的にパスワード入力
ansible-playbook -i inventory site.yml --ask-become-pass

# または環境変数で設定
export ANSIBLE_BECOME_PASSWORD='your_sudo_password'
ansible-playbook -i inventory site.yml

vault 機能を使えば、パスワードを暗号化して保存することも可能です。

並列実行のメカニズム

Ansible は複数ホストに対して並列でタスクを実行します。この並列度を制御することで、パフォーマンスを最適化できるのです。

forks 設定による並列度制御

デフォルトでは 5 ホストまで並列実行されますが、これは変更可能です。

ini# ansible.cfg
[defaults]
forks = 20
bash# コマンドラインから指定
ansible-playbook -i inventory site.yml -f 20

並列実行の可視化

以下の図で、forks 設定と実行パターンの関係を示します。

mermaidflowchart TD
    subgraph "forks=5の場合"
        batch1["ホスト1-5<br/>並列実行"]
        batch2["ホスト6-10<br/>並列実行"]
        batch1 --> batch2
    end

    subgraph "forks=10の場合"
        batch_a["ホスト1-10<br/>並列実行"]
    end

ホスト数が多い場合、適切な forks 値を設定することで実行時間を大幅に短縮できます。

並列実行の注意点

並列度を上げすぎると、以下の問題が発生する可能性があります。

  • Ansible コントローラのメモリ不足
  • ネットワーク帯域の圧迫
  • ターゲットシステムへの負荷集中

環境に応じて、最適な値を実験的に見つけることをお勧めします。

まとめ

本記事では、Ansible の実行モデルを 5 つのステージに分けて詳しく解説しました。

Ansible コマンド実行時には、Playbook 解析、SSH 接続確立、モジュール転送とラッピング、リモート実行、結果取得とクリーンアップという一連のプロセスが自動的に行われます。各段階を理解することで、エラー発生時の原因特定やパフォーマンスチューニングが的確に行えるようになるでしょう。

特に重要なポイントは以下の通りです。

モジュールのラッピング機構を理解すれば、カスタムモジュール開発時の設計指針が明確になります。AnsiballZ という名前の一時ファイルが作られる理由も、納得できたのではないでしょうか。

pipelining 機能を活用することで、実行速度を大幅に向上させられます。ただし、sudoers 設定の調整が必要な点には注意が必要ですね。

詳細ログ出力(-vvvv)を活用すれば、内部で実行される SSH コマンドや Python インタプリタの選択ロジックを確認できます。トラブルシューティング時の強力な武器になるはずです。

Ansible は表面上シンプルに見えますが、その内部には洗練された実行モデルが隠されています。この知識を武器に、より効率的で堅牢な自動化環境を構築していってください。

関連リンク