Ansibleでhandlerタスクを部分的にシリアル化する


# はじめに

ansible-playbookにおいて、「設定変更が走ったときに `handler` で再起動を掛ける」というコードはよく書くのではないかと思います。でも、冗長構成になっているサーバー群へのPlaybookだとしたら、 **同時に再起動されては困ります** よね。ロードバランス配下のWebサーバー用のPlaybookとか、あるあるなんじゃないかと思います。

一般にそういうときは `serial: 1` などと付けて同時に複数台で更新作業(再起動含む)が走らないようにするものです。

一般的な実装の例:

```yaml:test.yml
- hosts: all
  become: yes
  gather_facts: yes
  serial: 1
  roles:
    - risky_role
```

さて、この実装だと、Playbookが長かったり、対象ホスト数が多いときに、 **非常に時間がかかる** という問題が生じてしまいます。
タスクやblockに対して `serial` が指定できたら良いのですが、2019/02/15現在、まだそのような実装はありません。

本記事は **どうにかhandlerのタスクをパラレル実行できないか** と思って色々試したTipsの（自分用)メモです。

# 解決方法

ネットを探すとタスク毎にシリアル化する実装例は多々出てきます。でもそれをhandlerで適用したいわけです。
・・・というか、この記事をまとめ始めてから再度検索してみたら、Qiita内に近い実装例の記事があることに気付いた(笑)ので、そちらへのリンクもメモしておきます。

* 参考にしたもの: https://shasawas.wordpress.com/2017/01/30/how-to-run-a-particular-task-serially-in-the-middle-of-the-play-in-ansible/
* 似た実装: https://qiita.com/yunano/items/5deb78f0fa008d98227b

これらの記事にあるテクニックを応用して、handlersにもシリアル実行を適用します。

## 実装例(snippet)

handlerの実装例は以下。
複数のタスクからなるhandlerなので、 `import_tasks` をします。

```yaml:roles/risky_role/handlers/main.yml
- name: serial handler
  import_tasks: handlers/serial_task.yml
```

実際の処理は `import_tasks` しているファイル `serial_task.yml` に記載します。
以下の例では、タスク名 `serial task` がシリアルに1台ずつ実行したいタスクです。

```yaml:roles/risky_role/handlers/serial_task.yml
- name: set serial_required
  set_fact:
    serial_required: yes

- block:
  - name: serial task
    shell: |
      sleep 3
      touch /tmp/testfile
    when: 
      - hostvars[item]['serial_required'] is defined 
      - hostvars[item]['serial_required']
    delegate_to: "{{item}}"
    with_items: '{{ ansible_play_hosts }}'
    run_once: yes
    changed_when: no

  - name: reset serial_required
    set_fact:
      serial_required: no
```

## 簡単に説明

引用している記事でも丁寧な解説が書いてありますが、ジャンプするのも面倒なのでメモしておきます。

### シリアル実行の基礎部分

タスク名 `serial task` では、以下の3要素でシリアルに実行することを実現します。

```yaml
    delegate_to: "{{item}}"
    with_items: '{{ ansible_play_hosts }}'
    run_once: yes
```

- `run_once: yes`: 実行対象ホストの内、1台だけで実行する
- `with_items: '{{ ansible_play_hosts }}'`: 実行対象ホストの一覧でループする
- `delegate_to: "{{item}}"`: ループのそれぞれで、処理を委譲する

これらの組み合わせにより、 `A, B, C` 3台のホストに対してPlaybookを実行しているとしたら、以下のようにタスク名 `serial task` は実行されることになります。

- with_itemsのループでは `item` = `A` → `B` → `C` と遷移する
- したがって、 `delegate_to: "{{item}}"` は `delegate_to: A` → `delegate_to: B` → `delegate_to: C` という処理になる

以上により、シリアルに1台ずつ処理が実行されるということになります。

ただし、 `ansible_play_hosts` はhandlerが呼び出されたかどうかを考慮しないリストのため、このままでは **handlerを呼び出していないホストでも処理が実行されてしまいます。**
それを防ぐため、 `serial_required` 変数をsetして利用します。

### 実行対象ホストの制限

`serial_task.yml` 冒頭でset_factしていますが、 `serial_required: yes` となるホストは、handlerを呼び出したホストのみです。これをwhen条件に利用します。
ただし、 `run_once` や `delegate_to` のせいで直接ホストの変数を参照できないため、以下のような回りくどい `when` 条件を書くことで値を参照します。

```yaml
    when: 
      - hostvars[item]['serial_required'] is defined 
      - hostvars[item]['serial_required']
```

### その他

後で実行例をお見せしますが、 `run_once` や `delegate_to` のせいでPLAY RECAPにおいて実行ホスト間で変更数に差異が出てしまいます。 
`changed` の値に差分が出るのが気持ち悪いため、 `changed_when: no` オプションを付けています。(その結果 `ok` に差分が出るのですが)

また、 `serial_required` 変数が再利用されると弊害があるため、 `serial_required: no` に値をリセットしています。


## 実行例

`test task` で一部のホストだけ変更が走った例です。
結果だけではわかりにくいですが、 `RUNNING HANDLER [test : serial task]` は順次処理が実行されています。実行ホストは `test-01` になっていますが、delegateされて矢印の先に実際の実行ホストが書かれています。なお、ホスト `skip-0X` ではhandlerが呼ばれていないため、この処理もskipされています。

```bash:実行例
PLAY [all] ******************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************
ok: [test-01]
ok: [test-02]
ok: [skip-01]
ok: [skip-02]

TASK [test : test task] ***************************************************************************************************
skipping: [skip-01]
skipping: [skip-02]
changed: [test-01]
changed: [test-02]

RUNNING HANDLER [test : set serial_required] *******************************************************************************
ok: [test-01]
ok: [test-02]

RUNNING HANDLER [test : serial task] ********************************************************************************************
ok: [test-01 -> test-01] => (item=test-01)
ok: [test-01 -> test-02] => (item=test-02)
skipping: [test-01] => (item=skip-01)
skipping: [test-01] => (item=skip-02)

RUNNING HANDLER [test : reset serial_required] *****************************************************************************
ok: [test-01]
ok: [test-02]

PLAY RECAP ******************************************************************************************************************
skip-01     : ok=1    changed=0    unreachable=0    failed=0
skip-02     : ok=1    changed=0    unreachable=0    failed=0
test-01     : ok=5    changed=1    unreachable=0    failed=0
test-02     : ok=4    changed=1    unreachable=0    failed=0
```

`PLAY RECAP` を見ると、run_onceで実行ホストに選ばれた `test-01` のみ、 `ok` の値が1つ多くなっています。これはもう、そういうものだと思って諦めましたｗ


# おわりに

handlerの一部タスクを無理やりシリアル実行にする実装例を紹介しました。
特殊な変数を多用しているので、このメモなしには諳で書ける気がしません・・・ｗ