日々の備忘録

技術について日々学んだことを書きます。

Ansible taskを1ノードずつ実行する3つの方法

Ansible において、特段工夫をせずにtaskを記述していると、全ノードに対して一度に処理を実行することになります。

サーバの再起動やサーバミドルウェアの更新などが必要になる場合、仮に冗長構成を取っていたとしてもダウンタイムが発生してしまう可能性があります。

メンテナンス期間を設けて、その間は利用できないことにしてもいいのですが、止めることができない場合は一度に1ノードずつ処理を実行する、いわゆるローリングアップデートが必要になります。

Ansibleでローリングアップデートを実現するにはいくつか方法があります。

確認した環境

方法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 でローリングアップデートと言っても複数の方法があります。

それぞれの特性を把握した上で最も適している方法を選ぶといいと思います。