zaki work log

作業ログやら生活ログやらなんやら

[Ansible] 処理の中でターゲットノードがコロコロ替わるときはdelegate_toを使ってみる

論理的に同一の一連の処理だけど、実行ホストがコロコロ変わる場合、delegate_toを使うことで、ターゲットホストの切り替えのためのplayを区切ることなく、一つのplayで処理できます。

docs.ansible.com

delegate_toって

解説記事とか見ると「処理を実行するホストを移譲する」と書かれており、タスクを実行するノードを一時的に切り替える機能があることはわかるんですが、使いどころがよくわかってませんでした。

というのも、「play区切ってターゲットノード変えればいいし、、と思ってた」ので。。

delegate_toの使いどころ

よくあるパターンとして

  1. ターゲットノードAで秘密鍵・公開鍵キーペアを作成する
  2. ターゲットノードB,C,D,E,...に公開鍵を配布する

というのをAnsibleで実現しようとすると、ターゲットノード基準で考えると

  1. ターゲットノードA
    • キーペア作成
    • コントロールノードへ公開鍵コピー
  2. all
    • 公開鍵を配布

と、2つのターゲットノードで処理が行われるので、(delegate_toを使わないのであれば)playを分割する必要があります。

---
- hosts: target-a
  tasks:
  - openssh_keypair:
  - fetch:
- hosts: all
  tasks:
  - authorized_key:

こんな感じですね。 で、これ、playbookに全部書いてあれば、それほど見通しも悪くないしまぁ良いかなーという感じですが、roleに分割すると

---
- hosts: target-a
  roles:
  - create_ssh_keypair_by_targeta
- hosts: all
  roles:
  - authorized_key_to_target

こんな感じですね。
これみたいにroleが2つでplayも2分割ならまだ何とかなるけど、鍵作成にlocalhostだけ足したいとか、ターゲットの中で一部だけ追加のroleが必要になったとか、要件が追加されるたびにplayが増殖、roleも増加(=sshの鍵設定という 目的は一つなのにrole毎のディレクトリが増えて見通しが悪くなる )してしまいます。

やりたいことは「sshの鍵設定」という論理的に一つのことなので、出来ればplayからは「sshの鍵設定というrole」を一つ呼んで終わりにしたい、という発想です。
(呼んだroleから先が細分化されてるのはroleの内部実装的な話なのでplay的には気にしない、という考え)

そこでdelegate_toの「ターゲットノードを一時的に切り替える」という機能を使えば、「sshの鍵設定」という1つのroleを作り、その中で要件に応じてターゲットノードを切り替えることができるので、全体の構成をスッキリさせることができます。

delegate_to おためし

単体で使用

inventory

[master]
master02 ansible_host=192.168.0.122

[worker]
worker03 ansible_host=192.168.0.127
worker04 ansible_host=192.168.0.128

role

- name: create directory for ssh keypair
  file:
    path: "{{ lookup('env','HOME') }}/.ssh/"
    state: directory
    mode: 0700
  delegate_to: localhost

これをターゲットノードallのplayから実行すると

TASK [configure_ssh : create directory for ssh keypair] ********
changed: [worker04 -> localhost]
ok: [worker03 -> localhost]
ok: [master02 -> localhost]

となります。

allのノード毎にtaskが実行されるけど実行ノードはlocalhostになってるので、allのノード数3回分が実行されている」状態。

Ansibleの冪等性によって2度目以降はok(変化なし)となってるけど、これはさすがに無駄が多いので、こういう時はrun_onceを使います。

run_once: trueと併用

- name: create directory for ssh keypair
  file:
    path: "{{ lookup('env','HOME') }}/.ssh/"
    state: directory
    mode: 0700
  delegate_to: localhost
  run_once: true

実行結果

TASK [configure_ssh : create directory for ssh keypair] *************
changed: [worker03 -> localhost]

これでallのノード数と関係なく、1回の実行になりました。

(余談: ターゲットノードの内1台で実行するならplayのhosts:hoge_groupnodes[0]で行けるからrun_onceっていつ使うんだろうと思ってたけど、なるほど、delegate_toと組み合わせてtask/role内で一時的に動作変更するときに使えば良かったんだな)

複数の実行ノードで動かしたい

上の例ではlocalhost1台で実行しましたが、特定ノードグループで実行したい場合はどうするか。

以下、ダメな例

- name: publickey copy to target-nodes
  authorized_key:
    user: "{{ ansible_env.USER }}"
    key: "{{ lookup('file', lookup('env', 'HOME') + '/.ssh/id_rsa.pub' )}}"
  delegate_to: worker

delegate_toにinventoryに定義してるノードグループ名workerを指定。
これを実行すると

TASK [configure_ssh : publickey copy to target-nodes] ************
fatal: [worker03]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname worker: Name or service not known'
  unreachable: true
fatal: [worker04]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname worker: Name or service not known'
  unreachable: true
fatal: [master02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname worker: Name or service not known'
  unreachable: true

workerってホスト名無いよ、と怒られる。
つまりdelegate_toに指定するのはノードグループ名でなく、単にホスト名を指定する必要がある。

じゃあどうするかというと、loopを使ってループ処理にすればOK

- name: publickey copy to target-nodes
  authorized_key:
    user: "{{ ansible_env.USER }}"
    key: "{{ lookup('file', lookup('env', 'HOME') + '/.ssh/id_rsa.pub' )}}"
  delegate_to: "{{ item }}"
  run_once: true
  loop: "{{groups['worker']}}"

これで、各workerノードグループで実行されるようになる。
run_once: trueが無いと、(taskのターゲットホストの)allの1ホスト毎にloopによるworkerノードの処理が実行されるので注意。(元のターゲットホスト x delegate_to+loopの掛け算になる)(伝われ)

実装例

github.com

まさにこのお試しの例で1roleにまとめてスッキリできました。

github.com

その他の用途

やっぱり証明書関連は特定ノードで作成して、それを各ノードに配布という処理が多いので、delegate_to使うとすっきりするパターンは多そう。
また、Kubernetes環境だと、ノードの処理は各ターゲットノードだけど、処理の結果コンテナクラスタ上の状態を見る場合にmasterノードのうち1台だけで参照処理、みたいなのもありそう。
(平たく言うとmasterでkubeadmで鍵作成、workerノードでkubeadm joinクラスタ参加、masterでReadyになるのを監視、みたいな。)


去年仕事で初めて書いたplaybook/role、delegate_to知らなくてまさにターゲットノード毎に分割しちゃってたので、書き直したいなぁ(既にPJから離任してるけど)


playbookがmain()関数、roleは処理単位の関数、playbookはroleの詳細は触らない、という発想なので、「いや、Ansibleってそういうんじゃないよ?」という話だったらいろいろ変わってくるかも😅