Ansible taskを1ノードずつ実行する3つの方法
Ansible において、特段工夫をせずにtaskを記述していると、全ノードに対して一度に処理を実行することになります。
サーバの再起動やサーバミドルウェアの更新などが必要になる場合、仮に冗長構成を取っていたとしてもダウンタイムが発生してしまう可能性があります。
メンテナンス期間を設けて、その間は利用できないことにしてもいいのですが、止めることができない場合は一度に1ノードずつ処理を実行する、いわゆるローリングアップデートが必要になります。
Ansibleでローリングアップデートを実現するにはいくつか方法があります。
確認した環境
- Python 2.7
- Ansible 2.5
方法1. serial
以下の公式リンクで記載されている通り、Ansibleではserialキーワードがサポートされています。
Delegation, Rolling Updates, and Local Actions — Ansible Documentation
以下のようなplaybookを記載すると、hosts: allに対して1ノードずつ xxx サービスを再起動してくれます。
--- - name: rolling update (serial) hosts: all serial: 1 tasks: - name: service restart service: state: restarted name: xxx
ただし、serialキーワードはtask単位で使用することができないため、特定taskだけローリングアップデートしたいといった用途には向きません。(サポートされればいいんですが...)
方法2. delegate_to + run_once
run_onceを使うと、実行グループの先頭ホストでのみtaskが実行されます。 これとdelegate_toを組み合わせて、各ノードに処理を委譲させることで、ローリングアップデートを実現することができます。
--- - name: rolling update (run_once) hosts: all tasks: - name: service restart service: state: restarted name: xxx delegate_to: "{{ item }}" run_once: yes with_items: "{{ play_hosts }}"
この方法で大抵は解決すると思いますが、やはり欠点があります。
serialは一度に実行する数を指定することができますが、こちらは必ず1ノードずつ実行されます。
また、上述の通り、run_onceの仕様として実行時のグループの先頭ホストが主体となって処理が行われます。 そのため、whenなどで先頭ホストで実行されないtaskとなってしまうと、taskの各ノードへの委譲自体がされなくなるため、結果的に全ノードでskipされてしまうことになります。
方法3. include_tasks
私自身run_onceを使っていたのですが、その仕様によってtaskが実行されない状況に陥ってしまいました。
何とか解決できないかと調べていたところ、以下のgithub issuesのコメントにて別の方法が提示されていました。
support for "serial" on an individual task · Issue #12170 · ansible/ansible · GitHub
include_tasksとloopを組み合わせて動的にtaskを生成する方法です。
# playbook.yml - name: rolling update (include_tasks) hosts: all tasks: - name: service restart include_tasks: include.yml with_items: "{{ play_hosts }}" when: inventory_hostname == host_item loop_control: loop_var: host_item # include.yml - name: service restart service: state: restarted name: xxx
こうすることで、ローリングアップデートが可能になります。
各ノードで実行するtaskをそれぞれ動的に作成しているため、結果的に1ノードずつ実行されるtaskが出来上がります。
イメージとしては以下のymlに相当します。
- tasks: - name: service restart host1 service: state: restarted name: xxx when: inventory_hostname == host1 - name: service restart host2 service: state: restarted name: xxx when: inventory_hostname == host2
注意点として、include_tasksはtagsの影響を受けます。 tags指定で実行すると、includeされる側のymlが読まれずにバグを引き起こすことになります。 なのでincludeされる側のymlは以下のようにしておくといいです。
- name: service restart service: state: restarted name: xxx tags: - always
これでたとえtagsでフィルタされたとしても必ず実行されることになります。
まとめ
以上、3つの方法を紹介しました。
一口にAnsible でローリングアップデートと言っても複数の方法があります。
それぞれの特性を把握した上で最も適している方法を選ぶといいと思います。