はじめてのAnsible #3 notifyとhandler編

前回はPlaybookの分割に挑戦しました。今回は特定のタスクが完了した場合に後続処理を定義する方法についてまとめます。

notify と handler

後続処理を実行する

各タスクに記述することができるnotifyを利用すると、特定のタスクが終了したタイミングで、handlers内に定義された後続のタスクを自動的に実行してくれます。notifyは「通知する」という意味の英単語です。

- hosts: servers
  remote_user: ansibleman
  become: yes
  tasks:
    #-- Apacheをインストール --#
    - name: Install Apache
      yum: name=httpd state=present
      notify:
        - start apache

  #------------------------------------
  # ハンドラー
  #------------------------------------
  handlers:
    - name: start apache
      service:
        name: httpd
        state: started

ご覧の通りnameに半角スペースが入っていても大丈夫です。気持ち悪い場合はアンダーバー(_)をつけたりキャメルケース(startApache)といった感じで書いても良いかもしれません。

またnotifyが配列になっているところからも分かる通り、複数のハンドラーを同時に指定することも可能です。

- hosts: servers
  remote_user: ansibleman
  become: yes
  tasks:
    #-- 必要なミドルウェをインストール --#
    - name: Install Middleware
      service: name={{ item }} state=present
      with_items:
        - httpd
        - memcached
      notify:
        - start apache
        - start memcached

  #------------------------------------
  # ハンドラー
  #------------------------------------
  handlers:
    #-- Apacheを起動 --#
    - name: start apache
      service: name=httpd state=started
    #-- Memcachedを起動 --#
    - name: start memcached
      service: name=memcached state=started

注意点

👉 同じハンドラーの呼び出しは1回にまとめられる

特に意味のない例ですが、以下のPlaybookを実行するとansible.logには3行の日付が記録されていることを期待しますが、実際には1つにまとめられます。

- hosts: servers
  remote_user: ansibleman
  become: yes
  tasks:
    - name: ls 1st
      shell: ls
      notify: write date
    - name: ls 2nd
      shell: ls
      notify: write date
    - name: ls 3rd
      shell: ls
      notify: write date

  handlers:
    - name: write date
      shell: date >> ansible.log

ご覧の通りです。1行だけしか記録されていません。

$ cat ansible.log 
20191030日 水曜日 20:39:36 JST

なぜこのようなことになるかと言うと、notify/handlerはまとめて最後に実行されるからのようです。

これはあえての仕様で、重複して処理を行う方を防ぐ目的でこのような動作になっているようです。知らないとハマりそうですよねw

👉 ハンドラーはタスクがchangedのときだけ実行される

これも知らないとハマりそうなところです。以下のPlaybookを2回目に実行するとどういう挙動になるか考えてみます。

- hosts: servers
  remote_user: ansibleman
  become: yes
  tasks:
    #-- slコマンドをインストール --#
    - name: Install sl
      yum: name=sl state=present
      notify:
        - write date

  handlers:
    - name: write date
      shell: date >> ansible.log

slコマンドはすでにインストールされており、yumモジュールのstateにはpresentが指定されているためこのタスクは通常何も実行しません。ここで注目すべきはnotifyです。結論から言うとここでもhandlerは何も実行されません。notifyは何らかの処理が実行された場合にのみ、つまり実行結果がchangedになった場合にだけ通知されるのです。

さらに言うと、Playbook実行中にエラーが発生した場合、それまで正常終了していたタスクのハンドラー分もあわせてすべて実行されません。notify/handlerは最後にまとめて実行されるからです。

つまり以下のようなPlaybookを書いた場合、write date1は正常終了しているので実行されるかに見えますが実際には実行されません。何という罠。

- hosts: servers
  remote_user: ansibleman
  become: yes
  tasks:
    #-- [正常終了] slコマンドをインストール --#
    - name: Install sl
      yum: name=sl state=present
      notify:
        - write date1

    #-- [エラー] 存在しないパッケージをインストール --#
    - name: Install XXXXX
      yum: name=XXXXX state=present
      notify:
        - write date2

  handlers:
    - name: write date1
      shell: date >> ansible1.log
    - name: write date2
      shell: date >> ansible2.log

ちなみにslコマンドはTerminal上でSLが走るコマンドですw lsコマンドを打ち間違えた際に同僚をビビらせるための物ですね← いつもサーバに忍ばせています。

動画は2倍速にしてありますが実際にはもっとゆっくり通過するため、急いでるときだと殺意の波動に目覚めそうになりますのでご注意くださいw

ハンドラーをエラー発生時でも実行する

実行時にエラーが発生するとすべてのハンドラーが実行されないのは納得がいかない、成功したタスクの分だけでも実行したいという場合には、公式ドキュメントに記載されている手段を利用します。

Handlers and Failure You can change this behavior with the --force-handlers command-line option, or by including force_handlers: True in a play, or force_handlers = True in ansible.cfg. When handlers are forced, they will run when notified even if a task fails on that host. (Note that certain errors could still prevent the handler from running, such as a host becoming unreachable.)

適当に日本語訳すると、以下の通り。

  • コマンドラインで実行する際に --force-handlersオプションをつける
  • Playbookに force_handlers: True を記述
  • ansible.cfgに force_handlers = True を記述

先ほどのPlaybookにforce_handlers: Trueを追加しました。

- hosts: servers
  remote_user: ansibleman
  become: yes
  force_handlers: True
  tasks:
    #-- [正常終了] slコマンドをインストール --#
    - name: Install sl
      yum: name=sl state=present
      notify:
        - write date1

    #-- [エラー] 存在しないパッケージをインストール --#
    - name: Install XXXXX
      yum: name=XXXXX state=present
      notify:
        - write date2

  handlers:
    - name: write date1
      shell: date >> ansible1.log
    - name: write date2
      shell: date >> ansible2.log

実行すると最初にハンドラーだけ無事に実行されていますね。

$ ansible-playbook playbook.yml

PLAY [servers] ********************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************
ok: [neecbox.net]

TASK [Install sl] *************************************************************************************************************************
changed: [example.com]

TASK [Install XXXXX] **************************************************************************************************************************
fatal: [neecbox.net]: FAILED! => {"changed": false, "cmd": "dnf install -y python2-dnf", "msg": "[Errno 2] そのようなファイルやディレクトリはありません", "rc": 2}

RUNNING HANDLER [write date1] *****************************************************************************************************************
changed: [example.com]

PLAY RECAP ************************************************************************************************************************************
example.com                : ok=3    changed=2    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   

管理対象のサーバを確認するとちゃんと最初のログファイルだけ生成されているのがわかります。

$ ls
ansible1.log

$ cat ansible1.log 
2019116日 水曜日 21:38:38 JST

続き

blog.katsubemakito.net

参考ページ

Ansible実践ガイド 第3版 (impress top gear)
北⼭ 晋吾 佐藤 学 塚本 正隆 畠中幸司 横地 晃
インプレス (2019-10-18)
売り上げランキング: 14,785