serialが使えれば良かったのだけど

Ansibleでは特に指定がないと処理対象のホストに同時にモジュールが実行される。
そのため例えばサービスの再起動を全台同時に行ってしまうと、せっかくロードバランス等していても外から見て一時的にサービス停止となってしまう。

全台同時に実行されないためにはserial: (最大同時実行台数)を指定しておけば良いのだが、残念ながらserialはplay単位でしか指定できない。
play単位でしか指定できないということはroleも再起動するためのtaskだけ分けないといけないか、role丸ごとserialの影響下に置かないといけないということであり、前処理の関係上現実的にそれが困難であることもあり得る。

もちろんこれまでもtaskごとのserial機能の追加要望などが挙がっていたりもしたのだが、どうやら実装が難しいらしく今のところ追加される気配はない。
https://github.com/ansible/ansible/issues/12170

ではどうするか

さて、上記URLの最後にワークアラウンドが書かれている。引用すると以下の通り。

- mytask: ..
  delegate_to: "{{item}}"
  run_once: true
  # many diff ways to make the loop
  with_inventory_hostnames: all

delegate_toとrun_onceを使うのは固定で、あとはループを何で回すかであるようだ。
さあ、何で回そう。

想定される使い道として、大抵はhandlerとして呼ばれるtaskで行うことになるだろう。
とするとnotify側のtaskがchangedになったホストだけに適用されるのが要件になるだろう。

handler側にrun_onceがあるということは、notify側がchangedになったうちのどれか1台だけでhandler側が実行されるということだ。(だからわざわざ台数分ループで回してdelegate_toでホストごとに実行させている)

ならばまずどのホストでchangedであったのかをhandler側に伝える必要がある。これがないと実行しているホストは自分がnotify側でchangedであったことを知っているが、他のどのホストがchangedであったかを知りようがないからだ。
というわけでnotify側ではregisterを使う。

- name: task1
  mytask1module: ..
  notify: handler1
  register: task1result

handler側ではホストごとのregisterされた情報をhostvarsから引き出して、そこから実行判断をすれば良い。play_hostsはplayの対象となっているホストのリストである。

- name: handler1
  myhandler1module: ..
  delegate_to: '{{ item }}'
  run_once: yes
  when: hostvars[item].task1result | changed
  with_items: '{{ play_hosts }}'

欠点

この方法ではserialのように好きな台数を指定できるわけではなく、必ず1台ごとに実行されることになる。

また、notify側がchangedになったうちのどれか1台だけでhandler側が実行されるため、PLAY RECAPとして表示される実行の最終結果も、その1台だけに加算されるのが残念である。
以下はnode1ではchangedにならず、またnode2がhandler側になった場合のPLAY RECAPの例である。

PLAY RECAP *********************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0
node2                      : ok=4    changed=2    unreachable=0    failed=0
node3                      : ok=3    changed=1    unreachable=0    failed=0

だめだった例

なお、途中以下のようなhandler側も考えたのだが、skipはモジュールの処理時間より圧倒的に短く、またplay_hostの順番は処理ホストごとに一定しないのでこれだと同時に実行される可能性が高い。

- name: handler1
  myhandler1module: ..
  with_items: '{{ play_hosts }}'
  when: inventory_hostname == item

やはり1台ごとに実行させるためには実行主体をrun_onceで1台にしてから、並列処理されないようループを使うしかなかった。

TOP