てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] 所属するグループ内に失敗したホストがあればPlayをとめる

この記事は Ansible Advent Calendar 2023 の 11日目の記事です。

はじめに

今回の Advent Calendar 8日目の記事で @usagi_automate さんが、「所属するグループ内に失敗したホストがあれば処理スキップしたい」という記事(以下、元記事)を書かれていました。

usage-automate.hatenablog.com

興味深く拝見して、ちょっと別解を考えたくなりました。

微妙に要件が異なりますが、スキップではなく該当ホストの Play をとめるパターンの別解を考えてみました。

  • 検証環境
    • ansible-core 2.15.7

インベントリー

元記事と全く同じものを利用します。GroupA、GroupB、GroupC にそれぞれ 3台ずつ所属しています。

---
all:
  children:
    targets:
      children:
        groupA:
          hosts:
            host1:
              ansible_connection: local
            host2:
              ansible_connection: local
            host3:
              ansible_connection: local
          vars:
            group_id: groupA
        groupB:
          hosts:
            host4:
              ansible_connection: local
            host5:
              ansible_connection: local
            host6:
              ansible_connection: local
          vars:
            group_id: groupB
        groupC:
          hosts:
            host7:
              ansible_connection: local
            host8:
              ansible_connection: local
            host9:
              ansible_connection: local
          vars:
            group_id: groupC

Playbook

実装上の違い

failed グループに所属させる方法の違い

元記事では rescue 内(失敗したホストでも処理を続行)で group_by モジュールを使っていました。これにより「自分が失敗したら自分を failed グループに所属させる」を実現していました。

一方、本記事では blockrescue は利用しません。これらを使わずに似たようなことをできるかなぁ、と考えたのが発端のためです。rescue を利用しない場合、失敗したホストはその時点で処理が止まります(デフォルトでは)。そのため、自分が失敗したら自分で何かするということができません。本記事では代わりに、続行するほかのホストのタスクとして ansible.builtin.add_host モジュールで、failed に失敗ホストを所属させています(イマイチかもです)。

失敗ホストの求め方の違い

元記事では、前述どおり「自分が失敗したら自分を failed グループに所属させる」というアプローチのため、(failedグループに内に)自然と失敗ホストのリストができる仕組みです。

一方、本記事では同じアプローチができず、他の方法で失敗ホストのリスト求める必要があります。これを求めるには以下の2つの特別な変数を利用します。

変数名 説明 今回の Playbook の場合の値
ansible_play_hosts_all Play内のすべてのホストが入る ["host1", "host2", "host3", "host4, "host5", "host6", "host7", "host8", "host9"]
ansible_play_hosts Play内で現在正常なホストのリストが入る。エラーのホストは除外される ["host2", "host3", "host4, "host5", "host6", "host7", "host9"]

ansible_play_hosts_all にあって、ansible_play_hosts にないホストを求めることにより、失敗ホストのリストを求めます。

今回の場合 "{{ ansible_play_hosts_all | difference(ansible_play_hosts) }}" で、失敗ホストのリスト ["host1", "host8"] になります。

要件の違いによる実装の違い

元記事では、タイトルの「所属するグループ内に失敗したホストがあれば処理スキップしたい」どおり、スキップするのが要件です。

一方、本記事では、スキップではなくそのホストの Play を止めます。そのために、meta: end_host を利用します。Play を止めるホストの条件の指定として、 when の条件に「自分が所属するグループ内のホストに、失敗したホストが所属していること」を指定します。

処理の継続のさせ方の違い

元記事では、失敗したホストを rescue で扱うことで、

タスクの失敗ステータスが「取り消され」、成功したかのように続行していきます。

を実現し、2Play目の hosts: failed の処理を実行していました。

一方、本記事では rescue を使用していないため、これもやはり別の方法が必要です。そのために、meta: clear_host_errors を利用します。この指定をしないと 2Play目の hosts: failed 内に正常ホストがない扱いになって何も起こりません。

Playbook 内容

Playbook は以下の通りです。

---
- name: グループ内の1hostが失敗していたら、ほかの処理も止めたい
  hosts: targets
  gather_facts: false
  connection: local

  tasks:
    - name: なにかしらの失敗
      fail:
        msg: "Fail"
      when: "inventory_hostname == 'host1' or inventory_hostname == 'host8'"

    # (*1)
    - name: 失敗ホストをfailedグループに追加
      ansible.builtin.add_host:
        name: "{{ item }}"
        groups:
          - failed
      # 失敗したホストのリスト(今回は ["host1", "host8"] )のループ
      loop: "{{ ansible_play_hosts_all | difference(ansible_play_hosts) }}"

    - name: 自分が所属するグループ内のホストに、失敗したホストが所属していたら Play 停止
      meta: end_host
      when:
        - (groups[group_id] | intersect(failed_hosts) | length) >= 1
      vars:
        # 失敗したホストのリスト(今回は ["host1", "host8"] )
        failed_hosts: "{{ ansible_play_hosts_all | difference(ansible_play_hosts) }}"

    - name: 失敗無いグループのホストのみ処理
      debug:
        msg: "Errorないよ"

    # (*1)
    - name: エラー状態のクリア
      ansible.builtin.meta: clear_host_errors

- name: 失敗したホストの所属するグループを表示
  hosts: failed
  gather_facts: false
  connection: local

  tasks:
    - name: dev
      debug:
        msg: "{{ group_id }}"
    - name: fail
      fail:
        msg: "Error"

なお、2Play目の「失敗したホストの所属するグループを表示」が不要であれば、1Play目で (*1) で示した2つのタスクは不要です。

実行結果

タスク「失敗無いグループのホストのみ処理」では、どのホストの失敗しなかったグループ GroupB に所属するホスト host4host5host6 の分だけが処理されています。

その他のホストは skippedにもなりません。その他のホストは meta: end_host によって Play が止まっていたり、そもそも意図的にエラー(host1host8)を起こしているホストであるためです。ここが元記事と要件的に異なる点です。

実行結果は以下の通りです。

PLAY [グループ内の1hostが失敗していたら、ほかの処理も止めたい] ****************************************************

TASK [なにかしらの失敗] *******************************************************************************************
fatal: [host1]: FAILED! => {"changed": false, "msg": "Fail"}
skipping: [host2]
skipping: [host3]
skipping: [host4]
skipping: [host5]
skipping: [host6]
skipping: [host7]
fatal: [host8]: FAILED! => {"changed": false, "msg": "Fail"}
skipping: [host9]

TASK [失敗ホストをfailedグループに追加] ***************************************************************************
changed: [host2] => (item=host1)
changed: [host2] => (item=host8)

TASK [自分が所属するグループ内のホストに、失敗したホストが所属していたら Play 停止] *******************************

TASK [自分が所属するグループ内のホストに、失敗したホストが所属していたら Play 停止] *******************************

TASK [自分が所属するグループ内のホストに、失敗したホストが所属していたら Play 停止] *******************************
skipping: [host4]

TASK [自分が所属するグループ内のホストに、失敗したホストが所属していたら Play 停止] *******************************
skipping: [host5]

TASK [自分が所属するグループ内のホストに、失敗したホストが所属していたら Play 停止] *******************************
skipping: [host6]

TASK [自分が所属するグループ内のホストに、失敗したホストが所属していたら Play 停止] *******************************

TASK [自分が所属するグループ内のホストに、失敗したホストが所属していたら Play 停止] *******************************

TASK [失敗無いグループのホストのみ処理] ***************************************************************************
ok: [host4] => {
    "msg": "Errorないよ"
}
ok: [host5] => {
    "msg": "Errorないよ"
}
ok: [host6] => {
    "msg": "Errorないよ"
}

TASK [エラー状態のクリア] *****************************************************************************************

PLAY [失敗したホストの所属するグループを表示] *********************************************************************

TASK [dev] ********************************************************************************************************
ok: [host1] => {
    "msg": "groupA"
}
ok: [host8] => {
    "msg": "groupC"
}

TASK [fail] *******************************************************************************************************
fatal: [host1]: FAILED! => {"changed": false, "msg": "Error"}
fatal: [host8]: FAILED! => {"changed": false, "msg": "Error"}

PLAY RECAP ********************************************************************************************************
host1                      : ok=1    changed=0    unreachable=0    failed=2    skipped=0    rescued=0    ignored=0 
host2                      : ok=1    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0 
host3                      : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0 
host4                      : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0 
host5                      : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0 
host6                      : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0 
host7                      : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0 
host8                      : ok=1    changed=0    unreachable=0    failed=2    skipped=0    rescued=0    ignored=0 
host9                      : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0 

meta のタスクのログの出方が少し特徴的ですかね。

おわりに

blockrescue を避けるアプローチを考えたらこうなりました。結果的に要件が元記事とは異なるので単純な比較はできませんが。

@usagi_automate さん、ネタ提供ありがとうございました!