Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

はじめに

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

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

Handlerのおさらい

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

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

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

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にあわせると、このような形になりますね。

playbook
- hosts: all
  roles:
    - sample_role
roles/sample_role/tasks/main.yml
- name: sample_application config copy job
  copy:
    src: foo/bar/
    dest: /foo/bar/
  notify: sample_application_restart
roles/sample_role/handlers/main.yml
- name: sample_application_restart
  systemd:
    name: sample_application.service
    state: restarted
  become: yes

Notifyについて

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

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

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を分割しています。

playbook
- hosts: all
  roles:
    - copy_sample_common_config
    - copy_sample_baz_library
    - copy_sample_variable_config
roles/copy_sample_common_config/tasks/main.yml
- name: sample_application common config copy job
  copy:
    src: foo/bar/common/
    dest: /foo/bar/common/
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/
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を作るというのも試しました。

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

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
roles/copy_sample_common_config/handlers/main.yml
- name: sample_application_restart_flagged
  set_fact:
    flag_sample_application_restart: yes
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が作れそうです。

playbook
- hosts: all
  roles:
    - copy_sample_common_config
    - copy_sample_baz_library
    - copy_sample_variable_config
    - restart_sample_application
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
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
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
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ライフを!

morin_river
好きな事してるインフラ寄りエンジニアです。のんびりと。 Python / Scheme / Golang / Rust / BigQuery など
mohikanz
エンジニアのための雑談コミュニティ
https://mohikanz.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away