はじめに
Ansible Playbookを利用して、with_itemsでループ処理を実行したうち、changedになったアイテムだけ特定の処理をさせたいことがあると思います。
Handlerを使うと、1つ以上changedになると指定のHandlerが最後に1回実行されるので、何らかの方法でchangedになったアイテムのリストを渡さなければいけません。
または、notify: 'xxxxxxx {{ item }}'とすると、今度は1つでもchangedになると、リストの全アイテムの分だけHandlerが呼ばれてしまいます。仮にchangedのアイテムのみのHandlerを呼び出す方法があったとしても、事前に全アイテム分のHandlerを記述しておかなければいけないため、保守性に欠けます。(アイテムが増えたら、Handlerも追加しなければいけません)
これまで試行錯誤した結果、現時点で満足のいく結果が得られたので、記念に残しておきます。
想定ケース
カスタマイズした、あるいは新規のサービスに関するファイルを/etc/systemd/system以下に置く。そのうち追加・更新があったものだけリスタートする。
環境
VirtualBox (+ vagrant)
CentOS 7.7
ansible 2.9.6
Playbook
記事を分かりやすくするため、1本のPlaybooksystemctl.ymlに全て記載します。
まず冒頭で、ファイルを置き換える対象のファイル名をリスト変数で定義します。同名のファイルをPlaybookと同じディレクトリに配置します。
---
- name: systemctl
hosts: localhost
become: true
vars:
systemctl_services:
- tuned.service
- chronyd.service
今回の例では、/usr/lib/systemd/systemにあるファイルのうち、プロセスダウン時に自動的に復旧させたいものに対し、Restart=on-failureを追加したファイルを用意するケースを想定します。(ファイルの中身は本題とは関係ないので省略します)
1つ目のタスクで、copyモジュールを用いてファイルをコピーします。その結果をregisterで登録します。
tasks:
- name: /etc/systemd/system files are copied
copy:
src: "{{ item }}"
dest: "/etc/systemd/system/"
owner: root
group: root
mode: '0644'
with_items:
- "{{ systemctl_services }}"
register: systemctl_update
2つ目のタスクでは、2つのリスト変数(冒頭定義したサービスの一覧と、1つ目のタスクの結果)を、with_togetherでループ処理します。これにより、サービス1つ1つの名称と、それぞれの1つ目のタスクの結果をセットにして、順にループ処理することが出来ます。
with_together:
- "{{ systemctl_services }}"
- "{{ systemctl_update.results }}"
そして、1つ目のタスクの結果がchangedの時だけ処理させます。item.1.changedの1は、with_togetherで2つ目に指定した変数と言う意味です。1つ目は0です。
when:
- item.1.changed
処理させる内容は、serviceモジュールを使用したリスタートです。
- name: Updated systemd services are restarted
service:
name: "{{ item.0 }}"
state: restarted
あとおまじないを1つ仕掛けます。(説明は後で)
loop_control:
label: "{{ item.0 }} changed={{ item.1.changed }}"
Playbook全体はこんな感じになります。
---
- name: systemctl
hosts: localhost
become: true
vars:
systemctl_services:
- tuned.service
- chronyd.service
tasks:
- name: /etc/systemd/system files are copied
copy:
src: "{{ item }}"
dest: "/etc/systemd/system/"
owner: root
group: root
mode: '0644'
with_items:
- "{{ systemctl_services }}"
register: systemctl_update
- name: Updated systemd services are restarted
service:
name: "{{ item.0 }}"
state: restarted
when:
- item.1.changed
with_together:
- "{{ systemctl_services }}"
- "{{ systemctl_update.results }}"
loop_control:
label: "{{ item.0 }} changed={{ item.1.changed }}"
実行結果
実行すると、ファイルに更新があったもののみ、サービスのリスタートが実行されます。以下は、一度実行した後、/etc/systemd/system/chronyd.serviceを削除してから再度実行した時の出力例です。
$ ansible-playbook -i inventories/test systemctl.yml
PLAY [systemctl] *******************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]
TASK [/etc/systemd/system files are copied] ****************************************************************************
ok: [localhost] => (item=tuned.service)
changed: [localhost] => (item=chronyd.service)
TASK [Updated systemd services are restarted] **************************************************************************
skipping: [localhost] => (item=tuned.service changed=False)
changed: [localhost] => (item=chronyd.service changed=True)
PLAY RECAP *************************************************************************************************************
localhost : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
labelによるループ出力の制限
説明を後回しにした loop_control ですが、AnsibleのDocumentを眺めていてたまたま見つけた以下の記述をもとにしたものです。
Limiting loop output with label
これをつけないと、2つ目のタスク実行結果の出力が大変なことになります。(ブラウザによっては右にずっとスクロールして見てください・・・ターミナルだと、これが改行されて全部見せられます)
$ ansible-playbook -i inventories/test systemctl.yml
PLAY [systemctl] *******************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]
TASK [/etc/systemd/system files are copied] ****************************************************************************
ok: [localhost] => (item=tuned.service)
ok: [localhost] => (item=chronyd.service)
TASK [Updated systemd services are restarted] **************************************************************************
skipping: [localhost] => (item=['tuned.service', {'diff': {'before': {'path': '/etc/systemd/system/tuned.service'}, 'after': {'path': '/etc/systemd/system/tuned.service'}}, 'path': '/etc/systemd/system/tuned.service', 'changed': False, 'uid': 0, 'gid': 0, 'owner': 'root', 'group': 'root', 'mode': '0644', 'state': 'file', 'size': 376, 'invocation': {'module_args': {'owner': 'root', 'group': 'root', 'mode': '0644', 'dest': '/etc/systemd/system/', '_original_basename': 'tuned.service', 'recurse': False, 'state': 'file', 'path': '/etc/systemd/system/tuned.service', 'force': False, 'follow': True, 'modification_time_format': '%Y%m%d%H%M.%S', 'access_time_format': '%Y%m%d%H%M.%S', '_diff_peek': None, 'src': None, 'modification_time': None, 'access_time': None, 'seuser': None, 'serole': None, 'selevel': None, 'setype': None, 'attributes': None, 'content': None, 'backup': None, 'remote_src': None, 'regexp': None, 'delimiter': None, 'directory_mode': None, 'unsafe_writes': None}}, 'checksum': '4f2a27700ca6cc6da49f8678dec1c4cad782befa', 'dest': '/etc/systemd/system/tuned.service', 'failed': False, 'item': 'tuned.service', 'ansible_loop_var': 'item'}])
skipping: [localhost] => (item=['chronyd.service', {'diff': {'before': {'path': '/etc/systemd/system/chronyd.service'}, 'after': {'path': '/etc/systemd/system/chronyd.service'}}, 'path': '/etc/systemd/system/chronyd.service', 'changed': False, 'uid': 0, 'gid': 0, 'owner': 'root', 'group': 'root', 'mode': '0644', 'state': 'file', 'size': 495, 'invocation': {'module_args': {'owner': 'root', 'group': 'root', 'mode': '0644', 'dest': '/etc/systemd/system/', '_original_basename': 'chronyd.service', 'recurse': False, 'state': 'file', 'path': '/etc/systemd/system/chronyd.service', 'force': False, 'follow': True, 'modification_time_format': '%Y%m%d%H%M.%S', 'access_time_format': '%Y%m%d%H%M.%S', '_diff_peek': None, 'src': None, 'modification_time': None, 'access_time': None, 'seuser': None, 'serole': None, 'selevel': None, 'setype': None, 'attributes': None, 'content': None, 'backup': None, 'remote_src': None, 'regexp': None, 'delimiter': None, 'directory_mode': None, 'unsafe_writes': None}}, 'checksum': 'cc8a888eeccff040ae6e5d37b6c35804817086a5', 'dest': '/etc/systemd/system/chronyd.service', 'failed': False, 'item': 'chronyd.service', 'ansible_loop_var': 'item'}])
PLAY RECAP *************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
ループ処理の結果をregisterで登録したリスト変数を、以降のループ処理で単純に使うと、登録された情報が全て画面に表示されてしまいます。ずっとこの膨大な出力の抑制方法が無いものかと悩んでいたのですが、先のマニュアル記載を発見し、実行結果のようなシンプルな結果となったのでした。
応用、改善
これで、サービスを追加・変更したい場合は、ファイルを所定の場所に置き、あるいは編集して、追加の場合は変数を追加するだけで、追加・変更したサービスだけを再起動させることが出来るようになりました。
この手法は、他の用途にも応用可能と思います。例えば、shellモジュールで何かしらの状態(作成済みかどうか、など)をコマンドやシェルスクリプトでチェックし、その出力結果やリターンコードに応じて処理を変える、といった用途に使うと、shellモジュールを使わざるを得ない場合に、冪等性を作り込むのにも役立ちそうな気がします。(1つ目のタスクはchanged_when: trueとして、必要ものだけ2つ目のタスク処理を実行するイメージ)
また、例のようにファイルのcopyをトリガーにする場合は、with_fileglobを使うと、変数の編集も不要になり、もっと幸せになるかもしれませんが、それはまた別の機会にでも。