0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ansibleが理解できない理由はLinuxにあった【Ansible編】第8回:なぜ非同期・待機で失敗するのか

0
Last updated at Posted at 2026-07-27

📚 Ansibleが理解できない理由はLinuxにあった【Ansible編】第8回:なぜ非同期・待機で失敗するのか

⓪ なぜShellの知識がないとAnsibleは壊れるのか
① なぜshellモジュールは危険なのか
② なぜmoduleを使うと安全になるのか
③ なぜ毎回changedになるのか
④ なぜ条件分岐が壊れるのか(Ansible編)
⑤ なぜ変数が意図通りに扱えないのか
⑥ なぜタスクの順序で壊れるのか
⑦ なぜエラー制御が破綻するのか
⑧ なぜ非同期・待機で失敗するのか
⑨ なぜPlaybookが読めないのか
⑩ なぜAnsibleを正しく設計できるようになるのか


🗺️ 初めての方・シリーズの全体像を知りたい方はこちら

本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。

→ Ansibleが理解できない理由はLinuxにあった|統合ガイド


📑 【Ansible編】全体のまとめはこちら
「Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ」


📑 連載の移動
前の記事:【Ansible編】第7回 | 次の記事:【Ansible編】第9回


📋 目次

  1. 前回の振り返り(第7回)
  2. 逆引き辞典との連動
  3. はじめに(この記事のスタンス)
  4. 問題定義:長時間タスクにおける「接続断」と「タイミング不整合」
  5. 非同期実行(async)とポーリングの仕組み
  6. 実証:サービス起動後の疎通確認における予期しない失敗
  7. 改善:wait_forモジュールによる疎通確認の安定化
  8. 実証:poll: 0で起動後に結果を回収しない場合の挙動
  9. 改善:async_statusモジュールによる非同期タスクの結果回収
  10. まとめ
  11. 次回予告
  12. 連載一覧:Ansibleが理解できない理由はLinuxにあった【Ansible編】

※本記事の位置とシリーズ全体の関係を先に確認します。


:map: シリーズ全体構造(学習 × 問題解決)


本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。

この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。


📍 現在の位置


現在はこの図の「Ansible編の第8回」になります。


↑ 目次に戻る


1. 前回の振り返り(第7回)

前回 は、Ansibleのエラー判定の仕組みを取り上げました。デフォルトのrc != 0による成否判定に加え、failed_whenを使った判定条件のカスタマイズと、block/rescueによる例外処理の書き方を確認しました。


↑ 目次に戻る


2. 逆引き辞典との連動

「設計上の原因」を特定したい場合は、以下の逆引き辞典を活用してください。

Ansibleが理解できない理由はLinuxにあった【Ansible編】:トラブル逆引き辞典(設計パターン版)

  • まず「逆引き辞典」で該当ステップを特定する
  • 次に「本編(各連載記事)」で仕組みを理解する

↑ 目次に戻る


3. はじめに(この記事のスタンス)

Ansibleのタスク実行は基本的に同期処理です。つまり、1つのタスクが完了するまで次のタスクには進みません。
しかしインフラ構築では、OSの再起動やミドルウェアのインストールなど、完了までに長時間かかる処理が避けられません。こうした処理を同期的に実行し続けると、SSHタイムアウトによる切断や、サービス起動後の疎通確認失敗といった問題が発生します。
本記事では、こうした「時間のズレ」に起因するトラブルを整理した上で、async/pollによる非同期実行とwait_forによる待機制御という2つのアプローチを解説します。


↑ 目次に戻る


4. 問題定義:長時間タスクにおける「接続断」と「タイミング不整合」

実務において、以下のような時間制御に関連する不具合が発生します。

  • サービス起動後の疎通確認失敗
    HTTPサーバを起動した直後に疎通確認を行うと、サービスは起動済みであるにもかかわらず、期待したレスポンスが返らずタスクが失敗することがあります。これはAnsibleが「起動コマンドの完了」を「サービスが正常に応答できる状態」と同一視してしまうことに起因します。

また、環境によっては以下の問題が発生することもあります。

  • SSHタイムアウトによる中断
    数十分を要するバッチ処理を実行中、SSH接続のタイムアウトによりAnsibleが接続断と判断し、タスクが途中で失敗することがあります。

  • バックグラウンド実行の不安定性
    shellモジュールで&を使ったバックグラウンド実行は、SSHセッションの終了時に標準入出力の切断やシグナルの影響を受けるため、安定して動作しない場合があります。

これらの問題は、Ansibleの同期的なタスク実行モデルと、リモートホスト上で動作する実プロセスの非同期的な振る舞いとのズレに起因します。

第6セクションでは「サービス起動後の疎通確認失敗」を、第8セクションではpoll: 0による結果未回収の挙動をそれぞれ再現します。第7セクションではwait_forモジュールによる解決策を、第9セクションではasync_statusモジュールによる正しい結果回収の方法を示します。


🔍 裏付けとなる公式ドキュメント


↑ 目次に戻る


5. 非同期実行(async)とポーリングの仕組み

Ansibleではasyncキーワードを使うことで、タスクをバックグラウンドで実行させることができます。SSHセッションを長時間維持することなく、処理を継続できるのが特徴です。

🔄 非同期実行のプロセスフロー

📌 各パラメータの役割

  1. async: タスクの最大実行時間を秒単位で指定します。この時間を超えるとジョブは失敗とみなされます。

  2. poll: 制御ノードが進捗を確認する間隔を秒単位で指定します。0を指定すると結果を待たずに次のタスクへ進みます。

  3. ジョブの管理: 非同期タスクはリモートホスト上でバックグラウンド実行されるため、SSHセッションが切れても処理は継続します。ただし、ホストの再起動やプロセス強制終了の影響は受けます。


🔍 裏付けとなる公式ドキュメント


↑ 目次に戻る


6. 実証:サービス起動後の疎通確認における予期しない失敗

Ansibleでサービスを起動した直後に疎通確認を行うと、サービスは起動済みであるにもかかわらず、期待したレスポンスが返らずタスクが失敗することがあります。ここでは、この現象を実際に再現します。

【検証】サービス起動後の疎通確認における予期しない失敗

httpdを起動した直後にuriモジュールで疎通確認を行い、サービスが起動済みであっても疎通確認が失敗するケースを確認します。

■事前準備:httpdのインストールとポート80番の開放

リモートノードにhttpdがインストールされていないため、コントローラーノード側からPlaybookを使ってインストールし、ポート80番を開放します。

【コントローラーノード側】
ファイル名: setup_httpd.yml

---
- name: Setup httpd
  hosts: test_servers
  gather_facts: false
  become: true
  tasks:
    - name: httpdをインストールする
      ansible.builtin.dnf:
        name: httpd
        state: present

    - name: ポート80を開放する
      ansible.builtin.firewalld:
        service: http
        permanent: true
        state: enabled
        immediate: true
      register: firewalld_result

    - name: ファイアウォールの設定結果を表示する
      ansible.builtin.debug:
        var: firewalld_result

実行コマンド

ansible-playbook -i inventory.ini setup_httpd.yml

▼ 実行結果

PLAY [Setup httpd] **************************************************************************************************************************************************************************

TASK [httpdをインストールする] **************************************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]

TASK [ポート80を開放する] *******************************************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]

TASK [ファイアウォールの設定結果を表示する] *************************************************************************************************************************************************
ok: [192.168.1.21] => {
    "firewalld_result": {
        "changed": true,
        "failed": false,
        "msg": "Permanent and Non-Permanent(immediate) operation, Changed service http to enabled"
    }
}
ok: [192.168.1.22] => {
    "firewalld_result": {
        "changed": true,
        "failed": false,
        "msg": "Permanent and Non-Permanent(immediate) operation, Changed service http to enabled"
    }
}

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.1.22               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

【リモートノード側の確認】

  • リモートノード1
[root@localhost ~]# dnf list --installed | grep httpd
httpd.x86_64                            2.4.62-13.el9_8.1             @appstream
httpd-core.x86_64                       2.4.62-13.el9_8.1             @appstream
httpd-filesystem.noarch                 2.4.62-13.el9_8.1             @appstream
httpd-tools.x86_64                      2.4.62-13.el9_8.1             @appstream
rocky-logos-httpd.noarch                90.17-1.el9                   @appstream
[root@localhost ~]# sudo systemctl status httpd
○ httpd.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; preset: disabled)
     Active: inactive (dead)
       Docs: man:httpd.service(8)
[root@localhost ~]# sudo firewall-cmd --list-services
cockpit dhcpv6-client http ssh
  • リモートノード2
[root@localhost ~]# dnf list --installed | grep httpd
httpd.x86_64                            2.4.62-13.el9_8.1             @appstream
httpd-core.x86_64                       2.4.62-13.el9_8.1             @appstream
httpd-filesystem.noarch                 2.4.62-13.el9_8.1             @appstream
httpd-tools.x86_64                      2.4.62-13.el9_8.1             @appstream
rocky-logos-httpd.noarch                90.17-1.el9                   @appstream
[root@localhost ~]# sudo systemctl status httpd
○ httpd.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; preset: disabled)
     Active: inactive (dead)
       Docs: man:httpd.service(8)
[root@localhost ~]# sudo firewall-cmd --list-services
cockpit dhcpv6-client http ssh

※上記の通り、事前にhttpdのインストールとポート80番を開放しておきます。またサービスは停止の状態です

■検証内容

【コントローラーノード側】

ファイル名: verify_service_lag.yml

---
- name: Verify Service Startup Lag
  hosts: test_servers
  gather_facts: false
  become: true
  tasks:
    - name: 【再現実験】httpdを起動する
      ansible.builtin.service:
        name: httpd
        state: started

    - name: 【再現実験】起動直後に疎通確認を行う(待機なし)
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}:80"
        status_code: 200

実行コマンド

ansible-playbook -i inventory.ini verify_service_lag.yml

▼ 実行結果


(ansible) [ansible@localhost workspace]$ ansible-playbook -i inventory.ini verify_service_lag.yml

PLAY [Verify Service Startup Lag] ***********************************************************************************************************************************************************

TASK [【再現実験】httpdを起動する] **********************************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]

TASK [【再現実験】起動直後に疎通確認を行う(待機なし)] *************************************************************************************************************************************
fatal: [192.168.1.21]: FAILED! => {"accept_ranges": "bytes", "changed": false, "connection": "close", "content_length": "7620", "content_type": "text/html; charset=UTF-8", "date": "Tue, 09 Jun 2026 04:52:31 GMT", "elapsed": 0, "etag": "\"1dc4-645c65b4edc80\"", "last_modified": "Fri, 12 Dec 2025 19:34:58 GMT", "msg": "Status code was 403 and not [200]: HTTP Error 403: Forbidden", "redirected": false, "server": "Apache/2.4.62 (Rocky Linux)", "status": 403, "url": "http://192.168.1.21:80"}
fatal: [192.168.1.22]: FAILED! => {"accept_ranges": "bytes", "changed": false, "connection": "close", "content_length": "7620", "content_type": "text/html; charset=UTF-8", "date": "Tue, 09 Jun 2026 04:52:31 GMT", "elapsed": 0, "etag": "\"1dc4-645c65b4edc80\"", "last_modified": "Fri, 12 Dec 2025 19:34:58 GMT", "msg": "Status code was 403 and not [200]: HTTP Error 403: Forbidden", "redirected": false, "server": "Apache/2.4.62 (Rocky Linux)", "status": 403, "url": "http://192.168.1.22:80"}

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=1    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
192.168.1.22               : ok=1    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

リモートノード側の確認

疎通確認が失敗した時点でのhttpdプロセスの状態を確認します。

  • 192.168.1.21
[root@localhost ~]# systemctl status httpd
● httpd.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; preset: disabled)
     Active: active (running) since Tue 2026-06-09 13:52:28 JST; 46s ago
       Docs: man:httpd.service(8)
   Main PID: 18985 (httpd)
     Status: "Total requests: 1; Idle/Busy workers 100/0;Requests/sec: 0.0256; Bytes served/sec: 205 B/sec"
      Tasks: 177 (limit: 10627)
     Memory: 14.4M (peak: 14.7M)
        CPU: 169ms
     CGroup: /system.slice/httpd.service
             ├─18985 /usr/sbin/httpd -DFOREGROUND
             ├─18986 /usr/sbin/httpd -DFOREGROUND
             ├─18987 /usr/sbin/httpd -DFOREGROUND
             ├─18988 /usr/sbin/httpd -DFOREGROUND
             └─18989 /usr/sbin/httpd -DFOREGROUND

 6月 09 13:52:28 localhost.localdomain systemd[1]: Starting The Apache HTTP Server...
 6月 09 13:52:28 localhost.localdomain httpd[18985]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using localhost.localdomain. Set the 'ServerName>
 6月 09 13:52:28 localhost.localdomain httpd[18985]: Server configured, listening on: port 80
 6月 09 13:52:28 localhost.localdomain systemd[1]: Started The Apache HTTP Server.
lines 1-20/20 (END)

[root@localhost ~]# ss -tlnp | grep :80
LISTEN 0      511                *:80              *:*    users:(("httpd",pid=18989,fd=4),("httpd",pid=18988,fd=4),("httpd",pid=18987,fd=4),("httpd",pid=18985,fd=4))
  • 192.168.1.22
[root@localhost ~]# systemctl status httpd
● httpd.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; preset: disabled)
     Active: active (running) since Tue 2026-06-09 13:52:29 JST; 2min 1s ago
       Docs: man:httpd.service(8)
   Main PID: 16920 (httpd)
     Status: "Total requests: 1; Idle/Busy workers 100/0;Requests/sec: 0.0084; Bytes served/sec:  67 B/sec"
      Tasks: 177 (limit: 10635)
     Memory: 14.5M (peak: 15.0M)
        CPU: 269ms
     CGroup: /system.slice/httpd.service
             ├─16920 /usr/sbin/httpd -DFOREGROUND
             ├─16921 /usr/sbin/httpd -DFOREGROUND
             ├─16922 /usr/sbin/httpd -DFOREGROUND
             ├─16923 /usr/sbin/httpd -DFOREGROUND
             └─16924 /usr/sbin/httpd -DFOREGROUND

 6月 09 13:52:29 localhost.localdomain systemd[1]: Starting The Apache HTTP Server...
 6月 09 13:52:29 localhost.localdomain httpd[16920]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using localhost.localdomain. Set the 'ServerName>
 6月 09 13:52:29 localhost.localdomain httpd[16920]: Server configured, listening on: port 80
 6月 09 13:52:29 localhost.localdomain systemd[1]: Started The Apache HTTP Server.
lines 1-20/20 (END)

[root@localhost ~]# ss -tlnp | grep :80
LISTEN 0      511                *:80              *:*    users:(("httpd",pid=16924,fd=4),("httpd",pid=16923,fd=4),("httpd",pid=16922,fd=4),("httpd",pid=16920,fd=4))

■結果

httpdの起動直後に疎通確認を実行したところ、ステータスコード403 Forbiddenが返りタスクは失敗しました。リモートノード側の確認でもhttpdはすでに起動済みで、ポート80もリッスン状態です。
つまり「サービスの起動」と「サービスが正常に応答できる状態」は必ずしも一致しません。serviceモジュールがchangedを返した時点では、Ansibleは起動コマンドの完了を検知しているに過ぎず、サービスが期待通りのレスポンスを返せる状態かどうかは関知しません。この認識のズレが、実務における疎通確認失敗の原因となります。
第7セクションではwait_forモジュールを使ってこの問題を解決します。


↑ 目次に戻る


7. 改善:wait_forモジュールによる疎通確認の安定化

第6セクションでは、httpd起動直後の疎通確認が403 Forbiddenで失敗することを確認しました。ここではwait_forモジュールを使い、サービスが正常に応答できる状態になってから疎通確認を行う方法を示します。

【検証】wait_forによる疎通確認の安定化

httpd起動後にwait_forモジュールでポート80の待機を挟み、疎通確認が成功することを確認します。

■事前準備

第6セクションの検証後、httpdが起動したままになっているため、停止しておきます。

【コントローラーノード側】

ファイル名: prepare_section7.yml

---
- name: Prepare Section7
  hosts: test_servers
  gather_facts: false
  become: true
  tasks:
    - name: httpdを停止する
      ansible.builtin.service:
        name: httpd
        state: stopped

    - name: httpdの停止を確認する
      ansible.builtin.service_facts:

    - name: httpdのサービス状態を表示する
      ansible.builtin.debug:
        msg: "httpd status: {{ ansible_facts.services['httpd.service'].state }}"

実行コマンド

ansible-playbook -i inventory.ini prepare_section7.yml

▼ 実行結果

PLAY [Prepare Section7] *********************************************************************************************************************************************************************

TASK [httpdを停止する] **********************************************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]

TASK [httpdの停止を確認する] ****************************************************************************************************************************************************************
ok: [192.168.1.21]
ok: [192.168.1.22]

TASK [httpdのサービス状態を表示する] ********************************************************************************************************************************************************
ok: [192.168.1.21] => {
    "msg": "httpd status: inactive"
}
ok: [192.168.1.22] => {
    "msg": "httpd status: inactive"
}

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.1.22               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

■検証内容

【コントローラーノード側】
ファイル名: fix_service_lag.yml

---
- name: Fix Service Startup Lag
  hosts: test_servers
  gather_facts: false
  become: true
  tasks:
    - name: httpdを起動する
      ansible.builtin.service:
        name: httpd
        state: started

    - name: ポート80が応答するまで待機する
      ansible.builtin.wait_for:
        host: "{{ inventory_hostname }}"
        port: 80
        timeout: 30

    - name: 疎通確認を行う
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}:80"
        status_code: 200, 403

※uriモジュールのstatus_codeに200, 403を指定しています。今回の環境ではドキュメントルートにコンテンツが存在しないため、Apacheは403を返します。wait_forでポートの応答を確認した上で疎通自体が成功していることを示すため、両方を許容しています。

実行コマンド

ansible-playbook -i inventory.ini fix_service_lag.yml

▼ 実行結果

PLAY [Fix Service Startup Lag] **************************************************************************************************************************************************************

TASK [httpdを起動する] **********************************************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]

TASK [ポート80が応答するまで待機する] *******************************************************************************************************************************************************
ok: [192.168.1.21]
ok: [192.168.1.22]

TASK [疎通確認を行う] ***********************************************************************************************************************************************************************
ok: [192.168.1.21]
ok: [192.168.1.22]

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.1.22               : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

■結果

wait_forモジュールでポート80の応答を確認してから疎通確認を行ったところ、第6セクションで失敗していたタスクが正常に完了しました。httpdの起動完了後にポートの応答を待機することで、タイミングのズレによる失敗を回避できています。
serviceモジュールで「起動コマンドが完了した」だけでは、サービスが実際に応答できる状態かどうかは保証されません。wait_forを挟むことで、次のタスクに進む前にサービスの準備完了を確認するという明示的な制御が可能になります。


🔍 裏付けとなる公式ドキュメント


↑ 目次に戻る


8. 実証:poll: 0で起動後に結果を回収しない場合の挙動

第5セクションで触れたpoll: 0の即時復帰について、結果を回収しないまま次のタスクへ進んだ場合の具体的な挙動を確認します。

【検証】結果未回収のまま次のタスクへ進む

poll: 0で起動したバックグラウンドタスクの結果を回収せずに次のタスクへ進み、タスクの完了を待たずに処理が続いてしまうことを確認します。

■検証内容

【コントローラーノード側】
ファイル名: verify_async_no_wait.yml

---
- name: Verify Async Without Waiting
  hosts: test_servers
  gather_facts: false
  tasks:
    - name: 【再現実験】長時間タスクをpoll:0で起動する
      ansible.builtin.command: sleep 60
      async: 120
      poll: 0
      register: async_result

    - name: 【再現実験】結果を回収せずに次のタスクへ進む
      ansible.builtin.debug:
        msg: "ジョブID: {{ async_result.ansible_job_id }} のタスクが完了したか確認しない"

    - name: 【再現実験】バックグラウンドタスクの完了前提で処理を続ける
      ansible.builtin.command: echo "バックグラウンドタスクは完了しているはず"

実行コマンド

ansible-playbook -i inventory.ini verify_async_no_wait.yml

▼ 実行結果

PLAY [Verify Async Without Waiting] *********************************************************************************************************************************************************

TASK [【再現実験】長時間タスクをpoll:0で起動する] *******************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]

TASK [【再現実験】結果を回収せずに次のタスクへ進む] *****************************************************************************************************************************************
ok: [192.168.1.21] => {
    "msg": "ジョブID: j440776784560.4828 のタスクが完了したか確認しない"
}
ok: [192.168.1.22] => {
    "msg": "ジョブID: j125824261027.4867 のタスクが完了したか確認しない"
}

TASK [【再現実験】バックグラウンドタスクの完了前提で処理を続ける] ***************************************************************************************************************************
changed: [192.168.1.22]
changed: [192.168.1.21]

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.1.22               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

リモートノード側の確認
Playbookが完了した直後にバックグラウンドでsleepが残っているか確認します。

  • 192.168.1.21
[root@localhost ~]# ps aux | grep sleep
ansible+    4834  0.0  0.1 220964  2104 ?        S    10:42   0:00 sleep 60
root        4987  0.0  0.1 221684  2600 pts/0    R+   10:42   0:00 grep --color=auto sleep
  • 192.168.1.22
[root@localhost ~]# ps aux | grep sleep
ansible+    4873  0.0  0.1 220968  2100 ?        S    10:42   0:00 sleep 60
root        5026  0.0  0.1 221692  2604 pts/0    R+   10:42   0:00 grep --color=auto sleep

■結果

Playbookはsleep 60の完了を待たずに正常終了しました。しかしリモートノード側ではsleep 60のプロセスがバックグラウンドで動き続けていることが確認できます。

ansible+    4834  0.0  0.1 220964  2104 ?        S    10:42   0:00 sleep 60

ステータスが?になっている点も重要です。これはSSHセッションから切り離されたバックグラウンドプロセスであることを示しています。

つまりpoll: 0で即時復帰した場合、Ansibleはバックグラウンドタスクの完了を関知しないまま次のタスクへ進みます。今回はechoコマンドのような軽量な処理であれば問題になりませんが、バックグラウンドタスクの完了を前提とした処理が続く場合は意図しない結果を招きます。

第9セクションではasync_statusモジュールを使い、バックグラウンドタスクの完了を正しく回収する方法を示します。


↑ 目次に戻る


9. 改善:async_statusモジュールによる非同期タスクの結果回収

第8セクションではpoll: 0で起動したバックグラウンドタスクの完了を確認しないまま次のタスクへ進む挙動を確認しました。
ここではasync_statusモジュールを使い、バックグラウンドタスクの完了を正しく回収する方法を示します。

【検証】async_statusによる結果の回収
poll: 0で起動したバックグラウンドタスクのジョブIDをasync_statusモジュールで追跡し、完了を確認してから次のタスクへ進むことを確認します。

■検証内容

【コントローラーノード側】

ファイル名: fix_async_wait.yml

---
- name: Fix Async Without Waiting
  hosts: test_servers
  gather_facts: false
  tasks:
    - name: 長時間タスクをpoll:0で起動する
      ansible.builtin.command: sleep 60
      async: 120
      poll: 0
      register: async_result

    - name: async_statusでタスクの完了を待機する
      ansible.builtin.async_status:
        jid: "{{ async_result.ansible_job_id }}"
      register: job_result
      until: job_result.finished
      retries: 15
      delay: 10

    - name: バックグラウンドタスクの完了後に次のタスクへ進む
      ansible.builtin.command: echo "バックグラウンドタスクが完了した"

実行コマンド

ansible-playbook -i inventory.ini fix_async_wait.yml

▼ 実行結果

PLAY [Fix Async Without Waiting] ************************************************************************************************************************************************************

TASK [長時間タスクをpoll:0で起動する] *******************************************************************************************************************************************************
changed: [192.168.1.22]
changed: [192.168.1.21]

TASK [async_statusでタスクの完了を待機する] *************************************************************************************************************************************************
FAILED - RETRYING: [192.168.1.22]: async_statusでタスクの完了を待機する (15 retries left).
FAILED - RETRYING: [192.168.1.21]: async_statusでタスクの完了を待機する (15 retries left).
FAILED - RETRYING: [192.168.1.22]: async_statusでタスクの完了を待機する (14 retries left).
FAILED - RETRYING: [192.168.1.21]: async_statusでタスクの完了を待機する (14 retries left).
FAILED - RETRYING: [192.168.1.22]: async_statusでタスクの完了を待機する (13 retries left).
FAILED - RETRYING: [192.168.1.21]: async_statusでタスクの完了を待機する (13 retries left).
FAILED - RETRYING: [192.168.1.22]: async_statusでタスクの完了を待機する (12 retries left).
FAILED - RETRYING: [192.168.1.21]: async_statusでタスクの完了を待機する (12 retries left).
FAILED - RETRYING: [192.168.1.21]: async_statusでタスクの完了を待機する (11 retries left).
FAILED - RETRYING: [192.168.1.22]: async_statusでタスクの完了を待機する (11 retries left).
changed: [192.168.1.21]
changed: [192.168.1.22]

TASK [バックグラウンドタスクの完了後に次のタスクへ進む] *************************************************************************************************************************************
changed: [192.168.1.21]
changed: [192.168.1.22]

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
192.168.1.22               : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

リモートノード側の確認

async_statusによる待機完了後、sleepプロセスが残っていないことを確認します。

  • 192.168.1.21
[root@localhost ~]# ps aux | grep sleep
root        6643  0.0  0.1 221684  2600 pts/0    S+   11:14   0:00 grep --color=auto sleep
  • 192.168.1.22
[root@localhost ~]# ps aux | grep sleep
root        6456  0.0  0.1 221692  2592 pts/0    S+   11:15   0:00 grep --color=auto sleep

■結果

async_statusモジュールがジョブIDをもとにバックグラウンドタスクの完了を追跡し、sleep 60が終了するまでポーリングを繰り返しました。完了確認後に次のタスクへ進んでいることがログから確認できます。

FAILED - RETRYING: [192.168.1.21]: async_statusでタスクの完了を待機する (15 retries left).
...
changed: [192.168.1.21]  ← 完了を確認してchangedに変わった

FAILED - RETRYINGはエラーではなく「まだ完了していないため再試行中」という状態を示しています。retries: 15・delay: 10の設定で最大150秒間ポーリングを続け、sleep 60の完了を検知した時点で次のタスクへ進んでいます。

リモートノード側の確認でもsleepプロセスが残っておらず、バックグラウンドタスクが正常に完了した上で次のタスクへ進んだことが確認できます。

第8セクションと比較すると以下の通りです。

セクション 挙動 sleepプロセスの状態
第8セクション 完了を待たずに次のタスクへ進む Playbook完了後も残っていた
第9セクション 完了を確認してから次のタスクへ進む Playbook完了時には終了済み

🔍 裏付けとなる公式ドキュメント


↑ 目次に戻る


10. まとめ

本記事では、Ansibleの時間制御に関する問題を実際に再現しながら、wait_forモジュールとasync/pollによる解決策を確認しました。

各セクションの振り返り

  • サービス起動後の疎通確認失敗(第6・第7セクション)
    serviceモジュールでサービスを起動しても、Ansibleは「起動コマンドの完了」を検知しているに過ぎず、サービスが正常に応答できる状態かどうかは関知しません。wait_forモジュールでポートの応答を確認してから次のタスクへ進むことで、タイミングのズレによる失敗を回避できます。

  • poll: 0による即時復帰と結果未回収(第8・第9セクション)
    poll: 0を指定するとAnsibleはバックグラウンドタスクの完了を待たずに次のタスクへ進みます。バックグラウンドタスクの完了を前提とした処理が続く場合は、async_statusモジュールでジョブIDを追跡し、完了を確認してから次のタスクへ進む必要があります。

設計指針

本記事の検証を通じて、以下の指針が導けます。

状況 対策
サービス起動後に疎通確認を行う wait_forでポートの応答を確認してから進む
長時間タスクをバックグラウンドで実行する async/pollでバックグラウンド化する
poll: 0で即時復帰した後に結果が必要な場合 async_statusで完了を確認してから進む

おわりに

Ansibleのタスク実行は基本的に同期処理ですが、wait_forやasync/pollを適切に組み合わせることで、時間という不確定要素を制御した安定したPlaybookを書くことができます。

本記事が実務でのPlaybook設計の参考になれば幸いです。


🔍 裏付けとなる公式ドキュメント


↑ 目次に戻る


11. 次回予告

第8回では、非同期実行の仕組みと、時間差による失敗を防ぐための待機制御について整理しました。

これまでの連載で、変数、順序、エラー、時間といったPlaybookの「挙動」に関わる要素を一通り扱いました。しかしこれらを1つのPlaybookに詰め込むと、今度は「可読性」と「保守性」が犠牲になります。

  • 「どこに何が書いてあるか分からない」

  • 「一度作ったPlaybookが再利用できない」

次回は、Ansibleを実務でスケールさせるために欠かせないRole構成とタスク分割の考え方を取り上げます。

次回:【Ansible編】第9回:なぜPlaybookが読めないのか


↑ 目次に戻る


📑 連載の移動
前の記事:【Ansible編】第7回 | 次の記事:【Ansible編】第9回


📑 【Ansible編】全体のまとめはこちら
「Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ」


🗺️ 初めての方・シリーズの全体像を知りたい方はこちら

本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。

→ Ansibleが理解できない理由はLinuxにあった|統合ガイド


📚12. 連載一覧: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を実務で使いこなすための設計思想を完成させる。

↑ 目次に戻る


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?