複数ホストを跨ぐ依存関係のある処理をAnsibleで自動化しようとすると、この壁に当たります。ロードバランサーの操作がその代表例です。
この記事では、delegate_toとrun_onceの仕組みをゼロから整理し、ロードバランサー操作を含む無停止デプロイのオーケストレーション設計を実装例つきで解説します。動作確認環境はRHEL 10 / Rocky Linux 9(Ansible 2.17以降)です。
この記事のポイント
・delegate_toでタスクを別ホスト(LBなど)に移譲できる
・run_once: trueで全ホスト中1回だけ実行を保証できる
・delegate_to+run_onceで実行場所と回数を同時に制御できる
・serial: 1と組み合わせて無停止ローリングデプロイを設計できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜdelegate_toとrun_onceが必要なのか
Ansibleのplaybookは、デフォルトで「hostsに指定した全ホストで各タスクを実行」します。この動作でほとんどのケースは問題ないのですが、オーケストレーションが絡む場面では不十分です。具体的な例で考えてみましょう。NginxとアプリサーバーをまとめたWebサーバー群(web01、web02、web03)をHAProxy(lb01)の背後に並べた環境で、ローリングアップデートを自動化したいとします。
必要な処理順序は次のとおりです。
・lb01でweb01をバックエンドプールから解除する
・web01でアプリを更新し、ヘルスチェック通過を確認する
・lb01でweb01をプールに復帰させる
・同様の処理をweb02 → web03の順に繰り返す
この「lb01上でweb01に関する操作をする」という処理は、Ansibleの標準の動作(web01のコンテキストでタスクを実行)だけでは表現できません。そこで登場するのが
delegate_toとrun_onceです。delegate_toの仕組みと基本的な使い方
1. delegate_toの基本構文
delegate_toは、あるホストのループ中に「別のホスト上でこのタスクを実行する」ための指令です。# web01の処理ループ中にlb01上でコマンドを実行する例 - name: HAProxyでweb01をバックエンドから解除する shell: > echo "disable server myapp/{{ inventory_hostname }}" | socat stdio /var/run/haproxy/admin.sock delegate_to: lb01
{{ inventory_hostname }}の値は「web01」のまま保持されているため、lb01上でweb01を対象にした操作が自然に書けます。2. delegate_to実行時の変数スコープ
delegate_to使用時の変数挙動を理解しておくことが重要です。・{{ inventory_hostname }}:ループ中の元ホスト名(web01、web02…)のまま
・{{ ansible_host }}:delegate先のホスト(lb01)のIPアドレスに切り替わる
・{{ hostvars[inventory_hostname] }}:元ホストの変数を明示的に参照できる
この仕様を理解していないと、意図しないIPアドレスが使われてはまるケースがあります。
3. hostvarsで現在のホスト情報をdelegate先に渡す
lb01上で元ホスト(web01)のIPアドレスを参照したい場合はhostvarsを使います。# lb01の上でweb01のIPアドレスを使った操作の例 - name: HAProxy統計ページで対象バックエンドの状態を確認 shell: > echo "show stat" | socat stdio /var/run/haproxy/admin.sock | grep {{ hostvars[inventory_hostname]['ansible_host'] }} delegate_to: lb01 register: backend_stat
run_onceの仕組みと使いどころ
1. run_onceの基本構文と動作
run_once: trueは、「hostsに複数のホストが指定されていても、このタスクをplay全体で1回だけ実行する」指令です。# 全Webサーバーのうち1台だけでDBマイグレーションを実行する - name: DBスキーママイグレーション(1回だけ実行) shell: /app/bin/migrate.sh run_once: true
2. delegate_toとrun_onceを組み合わせる
「特定のホストで1回だけ実行する」という要件を確実に満たすには、両者を組み合わせます。# Ansible実行PCからSlackへ1回だけ通知する - name: デプロイ開始をSlackに通知(1回だけ) uri: url: "{{ slack_webhook_url }}" method: POST body_format: json body: text: "Rolling deploy started: {{ ansible_date_time.iso8601 }}" delegate_to: localhost run_once: true
delegate_to: localhostはplaybookを実行しているPC自身を指します。run_onceと組み合わせることで、「Ansible実行PCからSlackへ1回だけ通知する」という処理が正確に実現できます。よくある誤解として「delegate_toだけで1回になるんじゃないか」というものがあります。答えはNOです。delegate_toは「どこで実行するか」を変えるだけで、hostsに指定した全ホスト分ループします。run_onceを加えて初めて「1回だけ」になります。
Ansibleをより体系的に学びたい方には、Ansible実践講座でdelegate_to・run_once・serialを含む実務オーケストレーション設計を包括的に扱っています。
ロードバランサー操作を含む無停止デプロイ設計
1. 設計の全体像
構成は次のとおりです。・インベントリ:lb01(HAProxy)+ web01、web02、web03(Nginx+アプリ)
・serial: 1:Webサーバーを1台ずつ順番に処理(全台同時更新によるサービス断を防止)
・delegate_to: lb01:切り離し・復帰タスクをlb01上で実行
・run_once:デプロイ開始・終了通知を全体で1回だけ実行
インベントリの定義は次のとおりです。
# inventory/hosts.ini [lb] lb01 ansible_host=192.168.1.10 [web] web01 ansible_host=192.168.1.21 web02 ansible_host=192.168.1.22 web03 ansible_host=192.168.1.23
2. Playbookの実装例
# deploy.yml --- ローリングアップデート with LB操作 - name: WebサーバーのローリングアップデートPlaybook hosts: web serial: 1 become: true tasks: # 1. デプロイ開始通知(全体で1回だけ) - name: デプロイ開始をSlackに通知 uri: url: "{{ slack_webhook_url }}" method: POST body_format: json body: text: "Rolling deploy started on {{ inventory_hostname }}" delegate_to: localhost run_once: true # 2. LBからこのホストを切り離す - name: HAProxyでバックエンドを無効化 shell: > echo "disable server myapp/{{ inventory_hostname }}" | socat stdio /var/run/haproxy/admin.sock delegate_to: lb01 # 3. コネクションドレインを待機する - name: ドレイン待機(30秒) pause: seconds: 30 # 4. アプリを更新する - name: アプリパッケージを最新化 dnf: name: myapp state: latest - name: アプリサービスを再起動 systemd: name: myapp state: restarted # 5. ヘルスチェックが通るまで待機する - name: ヘルスチェックエンドポイントをポーリング uri: url: "http://{{ hostvars[inventory_hostname]['ansible_host'] }}:8080/health" status_code: 200 delegate_to: localhost retries: 12 delay: 5 register: health_check until: health_check.status == 200 # 6. LBにホストを復帰させる - name: HAProxyでバックエンドを再有効化 shell: > echo "enable server myapp/{{ inventory_hostname }}" | socat stdio /var/run/haproxy/admin.sock delegate_to: lb01
3. 実行時の動作確認
このplaybookを実行すると、次のような出力が得られます。PLAY [WebサーバーのローリングアップデートPlaybook] **** TASK [デプロイ開始をSlackに通知] **** ok: [web01 -> localhost] TASK [HAProxyでバックエンドを無効化] **** changed: [web01 -> lb01] TASK [ドレイン待機(30秒)] **** Pausing for 30 seconds ok: [web01] TASK [アプリパッケージを最新化] **** changed: [web01] TASK [アプリサービスを再起動] **** changed: [web01] TASK [ヘルスチェックエンドポイントをポーリング] **** ok: [web01 -> localhost] TASK [HAProxyでバックエンドを再有効化] **** changed: [web01 -> lb01] (以降、web02、web03と同様の処理が順次実行される) PLAY RECAP **** web01 : ok=7 changed=4 web02 : ok=7 changed=4 web03 : ok=7 changed=4
設計上の注意点とトラブルシュート
1. run_onceの実行ホストが不定になる問題
run_once: trueだけを指定した場合、タスクを実行するホストは「hostsリストの先頭ホスト」になります。ただし、--limitオプションやタグで実行対象を絞った際に「先頭ホスト」が変わることがあるため、実行ホストを固定したい場合は必ずdelegate_toを組み合わせてください。・実行ホストを固定したい場合:
run_once: true + delegate_to: 特定ホスト名・Ansible実行PC上で1回だけ処理したい場合:
run_once: true + delegate_to: localhost2. delegate_toとloopの組み合わせ時の注意
delegate_toにloopを組み合わせると、ループの各アイテムについてdelegate先でタスクが実行されます。「同じdelegate先に対して複数回接続が発生する」ことを理解した上で、SSH接続コストが問題になる場合はControlPersistの設定を検討してください。# ansible.cfg にControlPersistを設定する例 [ssh_connection] ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=/tmp/.ansible-ssh-%h-%p-%r
3. よくあるエラーと対処法
「The task includes an option with an undefined variable」がdelegate先で出る場合delegate先でhostvarsを参照しようとしている場合、gather_factsが無効になっているとfactが収集されずにエラーになります。playbook冒頭の
gather_facts: true(デフォルト)が明示的にfalseにされていないか確認してください。「unreachable: true」がdelegate先で出る場合
delegate_toに指定したホストがインベントリに存在しないか、SSH接続できない状態です。次のコマンドでdelegate先への疎通を確認してください。
# lb01へのSSH疎通確認 ansible lb01 -m ping -i inventory/hosts.ini
serial指定時、Ansibleのバージョンによっては
run_onceが「play全体で1回」ではなく「serialのバッチ単位で1回」として動作するケースがあります(Ansible 2.9系の既知の挙動)。run_once: true + delegate_to: localhostの組み合わせにすることで、実行環境に依存しない安定した動作が得られます。本記事のまとめ
| 機能 | 役割 | 典型的なユースケース |
|---|---|---|
delegate_to: ホスト名 |
タスクを別ホストで実行する | LBの切り離し・復帰操作、監視ツールへの通知 |
delegate_to: localhost |
Ansible実行PC上でタスクを実行 | Slack通知、ヘルスチェックのcurl実行 |
run_once: true |
全ホストで1回だけ実行を保証 | DBマイグレーション、デプロイ開始・終了通知 |
| 両者の組み合わせ | 特定ホストで厳密に1回だけ実行 | スキーマ変更、リリースタグ付け |
serial: 1との併用 |
1台ずつLBを制御しながら順次更新 | 無停止ローリングアップデート |
Ansible実践ハンズオンの詳細を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 前のページへ:AnsibleでLinuxサーバーのパッチ適用を自動化する方法|dnf・aptモジュールとserialによるローリング更新
- この記事の属するカテゴリ:Ansibleへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます