CUBE SUGAR CONTAINER

技術系のこと書きます。

Ansible: notify と handlers の使い方について調べた

今回は Ansible の Playbook で使える notify と handlers の使い方について調べてみる。 このふたつはペアになっていて、タスクに notify を書いておくと、そのタスクで状態に変更があった場合にそれと対応する handlers が実行される仕組みになっている。

環境には CentOS7 を使った。

$ cat /etc/redhat-release
CentOS Linux release 7.1.1503 (Core)
$ uname -r
3.10.0-229.11.1.el7.x86_64

Ansible をインストールする

まずは EPEL から Ansible をインストールしておく。

$ sudo yum -y install epel-release
$ sudo yum -y install ansible

基本的な使い方を試してみる

Apache httpd のインストールを題材にして基本的な使い方を確認しておく。

まずは notify と handlers を使った Playbook を用意する。 今回は Ansible をローカルホストで実行することにしたので connection: local の指定がある。 この Playbook では Apache httpd がインストールされた場合に、それを notify で通知する。 対応する handlers では Apache httpd のサービスを (再) 起動している。

$ cat << EOF > site.yml
---
- hosts: all
  connection: local
  sudo: True
  tasks:
    - name: install httpd
      yum: name=httpd
      notify: restart httpd

  handlers:
    - name: restart httpd
      service: name=httpd state=restarted

EOF

ローカルホストで実行するので Inventory は localhost のみ書いておく。

$ cat << EOF > hosts
localhost
EOF

Playbook を元に Ansible を実行する。

$ ansible-playbook -i hosts site.yml

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [install httpd] *********************************************************
changed: [localhost]

NOTIFIED: [restart httpd] *****************************************************
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0

初回なので Apache httpd がインストールされて、その通知を元にサービスの起動が走っている。

試しにもう一度実行してみよう。

$ ansible-playbook -i hosts site.yml

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [install httpd] *********************************************************
ok: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0

今度は Apache httpd が既にインストール済みなので、状態に変更はないため通知は行われずサービスの (再) 起動も行われることがない。

複数回 notify しても通知先が同じであれば実行は一回にまとめられる

何度同じ宛先に通知しても実行は一回にまとめられるという点が notify / handlers の特徴になっている。 例えばアプリケーションのコンフィグを複数回にわたって変更したとしても、そのアプリケーションの再起動は最後に一回行えば良いといった感じ。

上記の挙動を確認するために、次のような Playbook を用意した。 単なる debug モジュールを複数回実行しているだけだが、changed_when: True にすることで毎回同じ通知が行われるようになっている。

$ cat << EOF > site.yml
---
- hosts: all
  connection: local
  tasks:
    - name: notify1
      debug: msg="notify1"
      notify: handler
      changed_when: True

    - name: notify2
      debug: msg="notify2"
      notify: handler
      changed_when: True

  handlers:
    - name: handler
      debug: msg="handler"

EOF

Inventory は前回と変わらない

$ cat << EOF > hosts
localhost
EOF

実行してみる。

$ ansible-playbook -i hosts site.yml

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [notify1] ***************************************************************
changed: [localhost] => {
    "changed": true,
    "msg": "notify1"
}

TASK: [notify2] ***************************************************************
changed: [localhost] => {
    "changed": true,
    "msg": "notify2"
}

NOTIFIED: [handler] ***********************************************************
ok: [localhost] => {
    "msg": "handler"
}

PLAY RECAP ********************************************************************
localhost                  : ok=4    changed=2    unreachable=0    failed=0

複数回同じ宛先に通知が行われているが、ハンドラの実行は一回にまとめられている。

通知の条件を自分で決める

shell モジュールや command モジュールを使う場合、通知するか否かの条件を自分で決めたい場合がある。 そうした場合には changed_when を使うのがよさげ。

次の Playbook では command モジュールの実行結果を register で変数に格納した上で、その内容を changed_when でチェックしている。 この changed_when の条件が True になる場合に通知が行われる。 どうやら changed_when には Python のコードがそのまま記述できるようだ。

$ cat << EOF > site.yml
---
- hosts: all
  connection: local
  tasks:
    - name: notify
      command: echo "Hello, World!"
      register: result
      changed_when: result.stdout.find('World')
      notify: handler

  handlers:
    - name: handler
      debug: msg="'World' is contained"

EOF

Inventory は先ほどと同じ。

$ cat << EOF > hosts
localhost
EOF

実行してみる。

$ ansible-playbook -i hosts site.yml

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [notify] ****************************************************************
changed: [localhost]

NOTIFIED: [handler] ***********************************************************
ok: [localhost] => {
    "msg": "'World' is contained"
}

PLAY RECAP ********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0

command の実行結果を元に状態に変更があったと判断されて通知が行われた。 ちなみに上記ではコマンドの標準出力を元に判断しているが、返り値を使う場合には Jinja2 のフィルタを使って "result | success" としたり、あるいはより直接的に "result.rc == 0" といったように書くことができる。

単なる条件分岐であれば when を使った方が良い

本題からはちょっと脱線するけど、単なる条件分岐であれば notify/handlers よりも when を使った方がよさげ。

次の Playbook では main processing の実行結果を元に post processing を実行するかを when で判断している。 main processing の結果を register で変数 result に格納した上で、その標準出力を when で条件分岐させる。

$ cat << EOF > site.yml
---
- hosts: all
  connection: local
  tasks:
    - name: main processing
      command: echo "Hello, World!"
      register: result

    - name: post processing
      debug: msg="'World' is contained"
      when: result.stdout.find('World')

EOF

Inventory については先ほどと同じ。

$ cat << EOF > hosts
localhost
EOF

実行してみる。

$ ansible-playbook -i hosts site.yml

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [main processing] *******************************************************
changed: [localhost]

TASK: [post processing] *******************************************************
ok: [localhost] => {
    "msg": "'World' is contained"
}

PLAY RECAP ********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0

when の条件が True になるので post processing が実行される。

Roles で notify / handlers を使う

Ansible では設定ファイルの再利用性を高めるために Roles を使うのがベストプラクティスになっているようだ。 Roles は Playbook を細かく分割するためのルールのように考えればよさそう。 今度は Roles を使った場合に notify / handlers を実行する方法について書く。

その前に、今度は題材を Apache httpd のインストールに戻すので一旦アンインストールしておく。

$ sudo yum -y remove httpd

Playbook は次の通り。 これで web というロールを実行することになる。

$ cat << EOF > site.yml
---

- name: deploy web
  hosts: webservers
  connection: local
  roles:
    - web

EOF

Inventory は先ほどとちょっと異なり localhost を webservers というグループに参加させてみた。

$ cat hosts
[webservers]
localhost

Roles を使う場合は roles というディレクトリ以下に所定のファイルを用意していく。 先ほど Playbook で指定した web/tasks と web/handlers という名前で更にディレクトリを掘る。 この中に分割した Playbook を置いていく。

$ mkdir -p roles/web/tasks
$ mkdir -p roles/web/handlers

tasks には文字通り先ほど Playbook の中にあった tasks を書く。 main.yml という名前のファイルを置いておけば自動的に読み込んでくれる。

$ cat << EOF > roles/web/tasks/main.yml
---

- name: install httpd
  yum: name=httpd
  notify: restart httpd
  sudo: True

EOF

handlers も同様に Playbook の中にあった handlers の内容を記述する。 ファイル名についても同じで main.yml を自動的に読む。

$ cat << EOF > roles/web/handlers/main.yml
---

- name: restart httpd
  service: name=httpd state=restarted
  sudo: True

EOF

この状態で実行してみる。

$ ansible-playbook -i hosts site.yml

PLAY [deploy web] *************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [web | install httpd] ***************************************************
changed: [localhost]

NOTIFIED: [web | restart httpd] ***********************************************
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0

ばっちり動いた。

まとめ

今回は notify と handlers を実際に使ってみて、その挙動について調べてみた。 何らかの変更があった場合に必ず必要な処理というのは結構あるので、そうした内容をスマートに扱う上で有用そうだ。