AnsibleでHandlerをいい感じに使ってplaybookをきれいにしよう

#はじめに
Ansibleのhandlerを使っていますか？
私はPlaybookに冪等性を持たせる為にも、積極的に使っていきたいと思っています。
ですが、Roleの粒度を分割していくと
Handlerの設定をどう書くか悩む事も出てきます。

そんな時、AnsibleのHandlerはグローバルに設定される事を意識してみると
いい感じのPlaybookにできるので紹介します。


# Handlerのおさらい

AnsibleでのHandlerとは、Task上で状態が変更されていた時に
1回だけ実行されるジョブの仕組みです。

具体的には、Taskの実行時にcahngedが出て変更が走った場合
Handlerに記載されている名前のジョブが実行されます。

なお、この説明ではcopyされるFilesはすべて記載を省略しています。

```yaml:sample_role
tasks:
  - name: sample_application config copy job
    copy:
      src: foo/bar/
      dest: /foo/bar/
    notify: sample_application_restart

handlers:
  - name: sample_application_restart
    systemd:
      name: sample_application.service
      state: restarted
    become: yes
```

## Ansible Best Practicesにのっとった構成

http://docs.ansible.com/ansible/latest/playbooks_best_practices.html#directory-layout

Best Practicesでは各Roleの下に
役割ごとの階層をもうける事が期待されてます。  

```:上記一部抜粋
roles/
    common/               # this hierarchy represents a "role"
        tasks/            #
            main.yml      #  <-- tasks file can include smaller files if warranted
        handlers/         #
            main.yml      #  <-- handlers file
        templates/        #  <-- files for use with the template resource
            ntp.conf.j2   #  <------- templates end in .j2
        files/            #
            bar.txt       #  <-- files for use with the copy resource
            foo.sh        #  <-- script files for use with the script resource
```

上記サンプルRoleをBest Practicesにあわせると、このような形になりますね。

``` yaml:playbook
- hosts: all
  roles:
    - sample_role
```

``` yaml:roles/sample_role/tasks/main.yml
- name: sample_application config copy job
  copy:
    src: foo/bar/
    dest: /foo/bar/
  notify: sample_application_restart
```

``` yaml:roles/sample_role/handlers/main.yml
- name: sample_application_restart
  systemd:
    name: sample_application.service
    state: restarted
  become: yes
```

## Notifyについて

これもおさらいとなりますが
Notifyは何回通知されてもHandlerは1回だけ実行されます。

この仕様がうまくPlaybookを保つ鍵になりますね。

``` yaml:roles/sample_role/tasks/main.yml
- name: sample_application common config copy job
  copy:
    src: foo/bar/common/
    dest: /foo/bar/common/
  notify: sample_application_restart

- name: sample_application baz library config copy job
  copy:
    src: foo/bar/baz/
    dest: /foo/bar/baz/
  notify: sample_application_restart

- name: sample_application variable copy job
  copy:
    src: etc/default/sample_application.conf
    dest: /etc/default/sample_application.conf
  notify: sample_application_restart  
```

このようなTaskの場合は3回Task上でchangedが走りますが
1回だけプロセスが再起動されるという事になります。


# 分割した時に悩むこと

さてここから、少し具体的な内容になります。

Handlerを使えば特定のミドルウェアに対して
いい感じに変更を通知できる事がわかりました。
しかし、Playbookを作りこんで行くと
Roleの共通化や汎用化をしたいときに少し考える必要がありました。

この問題は、複数の環境やサービスの構成を管理したりすると特に顕著にあらわれます。

同じアプリケーションの構成管理で、
共通する設定と個別の設定が分かれるときに
私はRoleを分割しています。


``` yaml:playbook
- hosts: all
  roles:
    - copy_sample_common_config
	- copy_sample_baz_library
	- copy_sample_variable_config
```

``` yaml:roles/copy_sample_common_config/tasks/main.yml
- name: sample_application common config copy job
  copy:
    src: foo/bar/common/
    dest: /foo/bar/common/
```

``` yaml:roles/copy_sample_baz_library/tasks/main.yml
- name: sample_application baz library config copy job
  copy:
    src: foo/bar/baz/
    dest: /foo/bar/baz/
```

``` yaml:roles/copy_sample_variable_config/tasks/main.yml
- name: sample_application variable copy job
  copy:
    src: etc/default/sample_application.conf
    dest: /etc/default/sample_application.conf
```

共通設定ファイルは汎用化したいけど、
ライブラリやプロジェクト固有の設定ファイルは各Playbookで個別設定したい時などあるかと思います。
こういう構成の時、Handlerをどう扱うか少し困っていました。

それぞれのRoleに対してHandlerを記載するには、当然ですが以下の問題があります。

* 同じHandlerを複数書かないといけないので冗長
* 設定ファイルが最後までコピーされないとそもそも正しく動かない
* 複数のRoleの設定をした時に複数回Hanlderが走る

また、最後のRoleにだけHandlerを設定しても
共通ファイルなどを変更した時にHandlerが実行されません。

どれも致命的なので、汎用化は諦めるかHandlerを使わない事が増えていました。

# 分割RoleでもHandlerを使おうとして試したこと

ただ、Ansibleを使う上でHandlerをいい感じに使いたかったので
書き方を考えていました。

## Metaを使う

最初に考えたのはMetaで依存関係を作る事でした。
ただ、共通化したRoleでMetaを作っていくと依存関係が入れ子状態になり
設定が必要以上に複雑化したので別の方法を取りました。

## フラグ化

変更管理用の共通フラグ変数を作り
そのフラグが立っている時のみ実行するTaskを作るというのも試しました。

上記設定だとこのような構成ですね。
（この設定は動きません）

``` yaml:roles/copy_sample_common_config/tasks/main.yml
- name: sample_application common config copy job
  copy:
    src: foo/bar/common/
    dest: /foo/bar/common/
  notify: sample_application_restart_flagged
```

``` yaml:roles/copy_sample_common_config/handlers/main.yml
- name: sample_application_restart_flagged
  set_fact:
    flag_sample_application_restart: yes
```

``` yaml:roles/restart_sample_application/handlers/main.yml
- name: sample_application_restart
  systemd:
    name: sample_application.service
    state: restarted
  become: yes
  when: "{{ flag_sample_application_restart | default(false) }}"
```

このようにするとよしなに冪等性が担保できるかなーと思ったのですが
**Handler内のset_factで定義した変数は別Taskでは有効にならないようです。**
恐らくHandlerとTaskの中でfactの取り扱いが違うのでしょうか。
https://github.com/ansible/ansible/issues/9628
このissueでもfact定義した変数とHandlerで苦労している事がわかります。
（今回行おうとした事とは別ですが）

## Handlerのグローバル化

上のissueを見た時、この文言に目が止まりました。

>This is not intended to be a thing, because handlers are defined globally and cannot be used at inventory scope in this manner.

**どうやら、Handlerはinventory scope内で共通して使えるそうです。**
とすると、最後にHandlerだけ定義した別Roleを作成すれば汎用化したRoleが作れそうです。

``` yaml:playbook
- hosts: all
  roles:
    - copy_sample_common_config
	- copy_sample_baz_library
	- copy_sample_variable_config
	- restart_sample_application
```

``` yaml:roles/copy_sample_common_config/tasks/main.yml
- name: sample_application common config copy job
  copy:
    src: foo/bar/common/
    dest: /foo/bar/common/
  notify: sample_application_restart
```

``` yaml:roles/copy_sample_baz_library/tasks/main.yml
- name: sample_application baz library config copy job
  copy:
    src: foo/bar/baz/
    dest: /foo/bar/baz/
  notify: sample_application_restart
```

``` yaml:roles/copy_sample_variable_config/tasks/main.yml
- name: sample_application variable copy job
  copy:
    src: etc/default/sample_application.conf
    dest: /etc/default/sample_application.conf
  notify: sample_application_restart
```

``` yaml:roles/restart_sample_application/handlers/main.yml
- name: sample_application_restart
  systemd:
    name: sample_application.service
    state: restarted
  become: yes
```

このような構成にすることで、期待していたRoleの分割ができました！
HandlerのみのRoleというのをあまり思いついていなかったのですが
きれいな形のPlaybookに落ち着いたのかなと思います。
この構成だと明示的に指定のタイミングでHandlerを走らせる事が可能なので
柔軟なRoleを作ることができるのではないでしょうか。


それでは、いいAnsibleライフを！