#はじめに
Ansibleのhandlerを使っていますか?
私はPlaybookに冪等性を持たせる為にも、積極的に使っていきたいと思っています。
ですが、Roleの粒度を分割していくと
Handlerの設定をどう書くか悩む事も出てきます。
そんな時、AnsibleのHandlerはグローバルに設定される事を意識してみると
いい感じのPlaybookにできるので紹介します。
Handlerのおさらい
AnsibleでのHandlerとは、Task上で状態が変更されていた時に
1回だけ実行されるジョブの仕組みです。
具体的には、Taskの実行時にcahngedが出て変更が走った場合
Handlerに記載されている名前のジョブが実行されます。
なお、この説明ではcopyされるFilesはすべて記載を省略しています。
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にのっとった構成
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にあわせると、このような形になりますね。
- hosts: all
roles:
- sample_role
- name: sample_application config copy job
copy:
src: foo/bar/
dest: /foo/bar/
notify: sample_application_restart
- name: sample_application_restart
systemd:
name: sample_application.service
state: restarted
become: yes
Notifyについて
これもおさらいとなりますが
Notifyは何回通知されてもHandlerは1回だけ実行されます。
この仕様がうまくPlaybookを保つ鍵になりますね。
- 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を分割しています。
- hosts: all
roles:
- copy_sample_common_config
- copy_sample_baz_library
- copy_sample_variable_config
- name: sample_application common config copy job
copy:
src: foo/bar/common/
dest: /foo/bar/common/
- name: sample_application baz library config copy job
copy:
src: foo/bar/baz/
dest: /foo/bar/baz/
- 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を作るというのも試しました。
上記設定だとこのような構成ですね。
(この設定は動きません)
- name: sample_application common config copy job
copy:
src: foo/bar/common/
dest: /foo/bar/common/
notify: sample_application_restart_flagged
- name: sample_application_restart_flagged
set_fact:
flag_sample_application_restart: yes
- 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が作れそうです。
- hosts: all
roles:
- copy_sample_common_config
- copy_sample_baz_library
- copy_sample_variable_config
- restart_sample_application
- 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
- name: sample_application_restart
systemd:
name: sample_application.service
state: restarted
become: yes
このような構成にすることで、期待していたRoleの分割ができました!
HandlerのみのRoleというのをあまり思いついていなかったのですが
きれいな形のPlaybookに落ち着いたのかなと思います。
この構成だと明示的に指定のタイミングでHandlerを走らせる事が可能なので
柔軟なRoleを作ることができるのではないでしょうか。
それでは、いいAnsibleライフを!