Ansibleのデフォルト動作は「タスクが完了するまでSSH接続を保持し続ける同期実行」です。数秒で終わるタスクなら問題ありませんが、パッケージの大規模更新やソースビルドなど、数分以上かかる処理ではSSHのタイムアウトや長い待ち時間が問題になります。
この記事では、Ansibleの
async と poll を使って長時間タスクを非同期実行に切り替える方法を解説します。基本的な仕組みから async_status による完了確認、複数ホストへの並行実行設計、現場でよくある落とし穴まで体系的にまとめました。この記事のポイント
・asyncでタスクをバックグラウンド実行し、SSHタイムアウトを回避できる
・poll: 0(fire-and-forget)+async_statusで複数ホストの並行実行が可能
・retries×delayがasync値以上になるよう設計することが重要
・Windowsモジュールはasyncに対応していないため注意が必要
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
asyncとpollが必要になる場面——なぜ長時間タスクでSSHが切れるのか
Ansibleは通常、コントロールノード(Ansibleを実行するPC・サーバー)からSSH経由でリモートホストに接続し、タスクを実行します。この接続はタスクが完了するまで維持されます。問題が起きるのは次のような状況です。
・パッケージの大規模更新(dnf update / apt upgrade):依存関係の解決とダウンロードに数分以上かかる
・ソースコードからのビルド(make / cmake):コア数が少ない検証機では十数分以上かかることがある
・データベースのバックアップ・ダンプ:数十GB規模では長時間連続してI/Oが走る
・OSカーネル更新後の再起動待ち:再起動中はSSH接続が必ず切れる
デフォルト設定では、SSH接続がネットワーク経路のタイムアウトやSSHサーバー設定(
ClientAliveInterval 等)によって切断されることがあります。接続が切れると、Ansibleはタスクの成否を判断できないまま失敗扱いになります。これを解決するのが
async と poll です。タスクをバックグラウンドに移すことでSSH接続を解放し、後から結果を確認する仕組みです。asyncとpollの仕組みと基本設定
1. asyncでタスクをバックグラウンド実行する
async パラメーターに秒数を設定すると、タスクはリモートホスト上でバックグラウンドプロセスとして起動されます。指定した秒数がタスクの最大実行時間となり、超過するとKillされます。# async: タスクの最大実行時間(秒)を指定してバックグラウンド実行する - name: パッケージを全件更新する ansible.builtin.dnf: name: "*" state: latest async: 1800 # 最大1800秒(30分)。超過するとKillされる poll: 30 # 30秒ごとにAnsibleが完了を確認する
async の値は「タスクが最長でかかる時間の見積もり」より大きく設定するのが原則です。小さすぎると途中でKillされて失敗します。パッケージ更新なら1800秒(30分)、ビルドなら3600秒(1時間)など、余裕を持った値にしましょう。2. pollで完了確認の間隔を設定する
poll パラメーターはAnsibleがリモートホストへ完了確認(ポーリング)に行く間隔(秒)です。デフォルト値は10です。・
poll: 10(デフォルト):10秒ごとに完了を確認する。確認が終わるまで次のタスクに進まない・
poll: 30:確認頻度を下げる。ネットワーク負荷とSSH接続回数を減らしたい場合・
poll: 0:完了を待たずに次のタスクへ進む(fire-and-forgetモード)pollの値を大きくするほど確認頻度は下がりますが、完了直後の検知が遅れます。バランスを見て設定しましょう。パッケージ更新のように「完了のタイミングが予測しやすい」タスクは30秒程度が目安です。
3. poll: 0でfire-and-forgetにする
poll: 0 を設定すると、Ansibleはタスクをキックした直後に次のタスクへ進みます。ジョブIDが返され、後から async_status モジュールで結果を確認できます。# poll: 0 でfire-and-forget(完了を待たずに次へ進む) - name: パッケージ更新をバックグラウンドで開始する ansible.builtin.dnf: name: "*" state: latest async: 1800 poll: 0 register: dnf_job # ジョブIDをregisterで保持する(後で確認に使う)
register でジョブの情報(ジョブIDを含む辞書)を変数に保存しておき、後のタスクで完了を確認します。この変数を忘れると、ジョブが失敗しても検知できなくなるため必ず記述してください。async_statusで非同期ジョブの完了を確認する
1. ジョブIDを使ってasync_statusで完了を待つ
poll: 0 で投げたジョブは、async_status モジュールを使って完了を待ちます。until と retries / delay を組み合わせた繰り返しループで実装するのが基本パターンです。# ジョブの完了を定期確認する - name: パッケージ更新の完了を待つ ansible.builtin.async_status: jid: "{{ dnf_job.ansible_job_id }}" # ジョブIDを指定 register: job_result until: job_result.finished # finishedがtrueになるまで繰り返す retries: 60 # 最大60回試行 delay: 30 # 30秒ごとに確認する # 設計上の原則: retries * delay >= async の関係を必ず守る # この例: 60 * 30 = 1800秒 = async: 1800 → OK
retries × delay の値が async の値以上になるよう設定してください。これが不足していると、asyncジョブが完了する前にuntilループが終了し、タスクが失敗扱いになります。2. 複数タスクをfireしてまとめて待つ設計パターン
fire-and-forgetの最大の強みは「複数ジョブを同時に起動してから、まとめて結果を待つ」設計が可能な点です。独立した複数処理を順番に実行する必要がなくなります。# 独立した複数ジョブをfireしてバックグラウンドで並行実行する - name: DB1バックアップを開始 ansible.builtin.command: /usr/local/bin/backup-db1.sh async: 3600 poll: 0 register: backup1_job - name: DB2バックアップを開始 ansible.builtin.command: /usr/local/bin/backup-db2.sh async: 3600 poll: 0 register: backup2_job # この時点でDB1とDB2は並行実行中 - name: DB1バックアップの完了を待つ ansible.builtin.async_status: jid: "{{ backup1_job.ansible_job_id }}" register: result1 until: result1.finished retries: 120 delay: 30 - name: DB2バックアップの完了を待つ ansible.builtin.async_status: jid: "{{ backup2_job.ansible_job_id }}" register: result2 until: result2.finished retries: 120 delay: 30
3. 実際の検証サーバーでの出力例
RHEL 9.4 検証サーバー(web01.example.internal、web02.example.internal)でdnf update をasync実行した際の出力例です。TASK [パッケージ更新をバックグラウンドで開始する] ***** changed: [web01.example.internal] changed: [web02.example.internal] TASK [パッケージ更新の完了を待つ] ******************** FAILED - RETRYING: [web01.example.internal]: パッケージ更新の完了を待つ (59 retries left) FAILED - RETRYING: [web02.example.internal]: パッケージ更新の完了を待つ (59 retries left) FAILED - RETRYING: [web01.example.internal]: パッケージ更新の完了を待つ (58 retries left) FAILED - RETRYING: [web02.example.internal]: パッケージ更新の完了を待つ (58 retries left) changed: [web01.example.internal] changed: [web02.example.internal] PLAY RECAP ********************************************* web01.example.internal : ok=2 changed=2 unreachable=0 failed=0 web02.example.internal : ok=2 changed=2 unreachable=0 failed=0
FAILED - RETRYING の表示はエラーではありません。until ループが完了を確認しながら待機中であることを示しています。最終的にジョブが完了すると changed で終了し、PLAY RECAPに failed=0 が表示されます。
Ansible実践ハンズオンの詳細を見る >>
複数ホストへの並行実行設計
1. 各ホストで独立してasyncジョブを走らせる
asyncを使うと、Ansibleが管理する各ホストで独立してバックグラウンドジョブが走ります。インベントリに複数ホストを記載した場合、デフォルトでは全ホストへ同時にジョブをfireするため、並行実行になります。# インベントリの全Webサーバーで並行してパッケージ更新を実行する --- - name: 全Webサーバーのパッケージ更新 hosts: web_servers # web01, web02, web03 の3台 tasks: - name: パッケージ更新をバックグラウンドで開始 ansible.builtin.dnf: name: "*" state: latest async: 1800 poll: 0 register: update_job - name: パッケージ更新の完了を待つ ansible.builtin.async_status: jid: "{{ update_job.ansible_job_id }}" register: job_result until: job_result.finished retries: 60 delay: 30
2. freeストラテジで並行度をさらに上げる
Ansibleのデフォルト実行ストラテジはlinear で、全ホストが同じタスクを終えるまで次のタスクに進みません。free ストラテジに切り替えると、各ホストが独立して次のタスクへ進めるため、ホスト間の処理速度差による待ち時間がなくなります。--- - name: 全Webサーバーのパッケージ更新(freeストラテジ) hosts: web_servers strategy: free # ホストごとに独立して次のタスクへ進む tasks: - name: パッケージ更新をバックグラウンドで開始 ansible.builtin.dnf: name: "*" state: latest async: 1800 poll: 30 # pollあり: このホストの完了を待ってから次へ
strategy: free では処理が速いホストが先に次のタスクへ進みます。ただし strategy: free は各ホストの進行状況が非同期になるため、タスク間に依存関係がある場合は設計に注意が必要です。独立したパッケージ更新やバックアップのような処理に向いています。トラブルシュートと落とし穴
1. async_statusで「Could not find job」が出る場合
async_status 実行時に「Could not find job」エラーが出る主な原因は次の2つです。・ホスト間で変数が混同している:複数ホストで実行した場合、Ansibleは各ホストの変数を自動分離しますが、delegate_toを使う場合などに混同が起きることがある
・別のPlayで変数スコープが切れている:
register した変数は同一Playの範囲でのみ参照できる。別のPlayではjidが引き継がれない# ジョブIDが本当に取れているか確認する - name: ジョブIDのデバッグ出力 ansible.builtin.debug: msg: "Job ID: {{ update_job.ansible_job_id }}"
debug モジュールでジョブIDを出力して確認しましょう。ansible_job_id が空文字や undefined の場合、前のタスクでregisterが正しく機能していません。2. retriesが足りずuntilループがタイムアウトする場合
retries × delay < async の設定は設計ミスです。asyncジョブが完了する前にuntilループが終了して、タスクが失敗扱いになります。# NG例: retries * delay = 10 * 30 = 300秒 だが async: 1800 秒 # → 1800秒のジョブが完了する前にuntilループが300秒で終わる - name: 完了確認(NG設定) ansible.builtin.async_status: jid: "{{ update_job.ansible_job_id }}" register: job_result until: job_result.finished retries: 10 # 少なすぎる delay: 30 # OK例: retries * delay = 60 * 30 = 1800秒 = async: 1800 秒 # → asyncと同じ秒数まで待てる - name: 完了確認(OK設定) ansible.builtin.async_status: jid: "{{ update_job.ansible_job_id }}" register: job_result until: job_result.finished retries: 60 # retries * delay >= async を常に守る delay: 30
3. asyncが使えないケースと注意事項
async はすべての状況で使えるわけではありません。次のケースでは使用できません。・Windowsホストへのタスク: WinRMベースのWindowsモジュールはasyncに対応していない
・include_tasks などの動的インクルード: タスク自体にasyncは設定できない(インクルード先のタスクに個別に設定する必要がある)
・poll: 0のまま完了確認なし: fire-and-forgetでasync_statusを省くと、ジョブが失敗しても検知できずPlaybookが成功扱いで終わる
特に「poll: 0のまま完了確認なし」は最も見つけにくい設計ミスです。エラーが発生しても表面上Playbookが正常終了するため、長期間気づかないことがあります。
poll: 0 を使う場合は必ず対応する async_status タスクを実装してください。本記事のまとめ
| やりたいこと | 設定・方法 |
|---|---|
| SSHタイムアウトを回避して長時間タスクを実行 | async: 秒数 をタスクに追加する |
| 定期的に完了を確認しながら待つ | async: 秒数 と poll: 秒数 を組み合わせる |
| タスクを投げてすぐ次へ進む(並行実行) | async: 秒数 poll: 0 + register でジョブID保持 |
| fire-and-forgetジョブの完了を後から確認 | ansible.builtin.async_status: jid: "{{ 変数.ansible_job_id }}" |
| untilループの設計原則 | retries × delay >= async の関係を常に守る |
| 複数ホストのホスト間待ちをなくす | Playに strategy: free を設定する |
async と poll は、長時間タスクのSSHタイムアウト回避と複数ホストへの並行実行を実現するPlaybook設計の要です。特に poll: 0 によるfire-and-forgetと async_status の組み合わせは、Playbookの総実行時間を大幅に短縮できます。「接続タイムアウトが怖いからsshの設定を変えて対処する」ではなく、asyncとpollで正しく非同期設計することが、現場で信頼されるPlaybook実装の第一歩です。
Ansible実践ハンズオンの詳細を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 次のページへ:Ansibleのimport_tasksとinclude_tasksの違いと使い分け|静的・動的読み込みの設計判断
- 前のページへ:Ansible CollectionsとFQCNの基礎知識|ansible.builtinと外部コレクション導入の実践手順
- この記事の属するカテゴリ:Ansibleへ戻る

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