はじめに
Ansible には、タスクのステータスが changed のときだけ実行したい処理を定義するために、ハンドラーという機能があります。
ハンドラーには、通常のタスクと同様に name を指定できます。ハンドラーで指定した name をこの記事ではハンドラー名と呼びます。
タスクとハンドラーを関連付ける方法は複数あります。その一つは、通常のタスクの notify ディレクティブで、呼びたいハンドラー名を指定する方法です。
(実際には一通りタスクの処理が終わってからハンドラーが呼ばれます。ロールを含めた処理順はこちら)
そのため、ハンドラー名は重要な識別子の役割を持ちます。もし、同じハンドラー名のハンドラーが複数あると思わぬ結果を招く場合あります。ハンドラー名はユニークにしておくべきと、ドキュメントに記載があります。
Each handler should have a globally unique name.
同じくドキュメントに記載されている、グロバルスコープで扱われるという点も重要です。
Handlers from roles are not just contained in their roles but rather inserted into the global scope with all other handlers from a play.
この記事では、同じ名前のハンドラーがある場合の挙動を、通常の場合と比較して検証します。
- 検証環境
- ansible-core 2.15.2
1. 通常の場合(ユニークなハンドラー名の場合)
まずは、比較のためにユニークなハンドラー名で構成された通常の場合の挙動です。
フォルダ・ファイル構成
それぞれハンドラーを持つ 2つのロール role1、role2 からなる構成とします。
.
├── playbook.yml
└── roles
├── role1
│ ├── handlers
│ │ └── main.yml
│ └── tasks
│ └── main.yml
└── role2
├── handlers
│ └── main.yml
└── tasks
└── main.yml
role1 ロール
role1/tasks/main.yml:
--- - name: Task with changed ansible.builtin.command: # changed cmd: "echo Hello!" notify: - Test Handler in role1
role1/handlers/main.yml:
--- - name: Test Handler in role1 # ロール固有の名前 ansible.builtin.debug: msg: "handler {{ ansible_role_name }}"
role2 ロール
(role1 とすべて同じです)
role2/tasks/main.yml:
--- - name: Task with changed ansible.builtin.command: # changed cmd: "echo Hello!" notify: - Test Handler in role2
role2/handlers/main.yml:
--- - name: Test Handler in role2 # ロール固有の名前 ansible.builtin.debug: msg: "handler {{ ansible_role_name }}"
Playbook
2つのロールを呼び出すPlaybookです。
playbook.yml:
--- - name: Test Play hosts: localhost connection: local gather_facts: false tasks: - name: Import role1 ansible.builtin.import_role: name: role1 - name: Import role2 ansible.builtin.import_role: name: role2
実行結果
以下のような実行順序になります。
role1のタスクrole2のタスクrole1のハンドラーrole2のハンドラー
コマンド実行ログは以下のとおりです。
$ ansible-playbook -i localhost, playbook.yml
PLAY [Test Play] ****************************************************************************************************
TASK [role1 : Task with changed] ************************************************************************************
changed: [localhost]
TASK [role2 : Test Handler in role1] ********************************************************************************
changed: [localhost]
RUNNING HANDLER [role1 : Test Handler in role1] *********************************************************************
ok: [localhost] => {
"msg": "handler role1"
}
RUNNING HANDLER [role2 : Test Handler in role2] *********************************************************************
ok: [localhost] => {
"msg": "handler role2"
}
PLAY RECAP **********************************************************************************************************
localhost : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2. 同じ名前のハンドラーがある場合
次に、同じ名前のハンドラーが複数に含まれている場合です。
role1 ロール
role1/tasks/main.yml:
--- - name: Task with changed ansible.builtin.command: # changed cmd: "echo Hello!" notify: - Test Handler
role1/handlers/main.yml:
--- - name: Test Handler # 他のロールにあるハンドラーと同じ名前 ansible.builtin.debug: msg: "handler {{ ansible_role_name }}"
role2 ロール
(role1 とすべて同じです)
role2/tasks/main.yml:
--- - name: Task with changed ansible.builtin.command: # changed cmd: "echo Hello!" notify: - Test Handler
role2/handlers/main.yml:
--- - name: Test Handler # 他のロールにあるハンドラーと同じ名前 ansible.builtin.debug: msg: "handler {{ ansible_role_name }}"
Playbook
2つのロールを呼び出すPlaybookです。
playbook.yml:
--- - name: Test Play hosts: localhost connection: local gather_facts: false tasks: - name: Import role1 ansible.builtin.import_role: name: role1 - name: Import role2 ansible.builtin.import_role: name: role2
実行結果
以下のような実行順序になります。
role1のタスクrole2のタスクrole2のハンドラー
role1 のハンドラーが呼ばれていないところがポイントです。
コマンド実行ログは以下のとおりです。
$ ansible-playbook -i localhost, playbook.yml
PLAY [Test Play] ****************************************************************************************************
TASK [role1 : Task with changed] ************************************************************************************
changed: [localhost]
TASK [role2 : Task with changed] ************************************************************************************
changed: [localhost]
RUNNING HANDLER [role2 : Test Handler] ******************************************************************************
ok: [localhost] => {
"msg": "handler role2"
}
PLAY RECAP **********************************************************************************************************
localhost : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
この、role1 のハンドラーが呼ばれなかったところが、ドキュメントの言葉でいう
shadowing all of the previous handlers with the same name.
かなと思います。
なお、ansible.builtin.import_role でインポートするロールを逆( role2、role1 の順)にしたら、role1 のハンドラーのみが呼ばれました。
おまけ
ハンドラーはグローバルで有効という性質上、以下のようなこともできます。
特定のロールのハンドラーを指定
notify で ロール名 : ハンドラー名 を指定すると、ロールを明示的に指定できます。ドキュメントにも方法が載っています(最近知りました)。
たとえば、role1/tasks/main.yml で、以下のように role2 内の ハンドラーを Test Handler 呼ぶとします。
--- - name: Task with changed ansible.builtin.command: cmd: "echo Hello!" notify: - "role2 : Test Handler"
これで、role2/handlers/main.yml の以下のハンドラーが呼ばれます。
--- - name: Test Handler ansible.builtin.debug: msg: "handler {{ ansible_role_name }}"
ただし、role2 も Playbook 側で読み込んでおく必要があります。
ハンドラーのみのロールでハンドラーを共通化
ハンドラーのみのロールを用意して、他のロールからハンドラーを呼ぶようにして、ハンドラーを共通化するような方法です。
.
├── playbook.yml
└── roles
├── role1
│ └── tasks
│ └── main.yml
└── role_handler_only
└── handlers
└── main.yml
role_handler_only/handlers/main.yml:
--- - name: Test Handler # role_handler_only ロールはこれのみ ansible.builtin.debug: msg: "handler {{ ansible_role_name }}"
role1/tasks/main.yml:
--- - name: Task with changed # role1 ロールはこれのみ ansible.builtin.command: cmd: "echo Hello!" notify: - Test Handler
playbook.yml:
--- - name: Test Play hosts: localhost connection: local gather_facts: false tasks: - name: Import role1 ansible.builtin.import_role: name: role1 - name: Import role_handler_only ansible.builtin.import_role: name: role_handler_only # ハンドラーのみのロール
実行ログ:
ansible-playbook -i localhost, playbook.yml
PLAY [Test Play] *************************************************************************************************************
TASK [role1 : Task with changed] *********************************************************************************************
changed: [localhost]
RUNNING HANDLER [role_handler_only : Test Handler] ***************************************************************************
ok: [localhost] => {
"msg": "handler role_handler_only"
}
PLAY RECAP *******************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ハンドラーのみのロールの実装のヒントは、以下の記事から得ました。ありがとうございます。
おわりに
同じ名前のハンドラーがあることで、起こる現象を検証しました。
ざっくり、ハンドラー名は Play 内でグローバルなスコープで処理される、というイメージでとらえておけばよいかなと思いました。