Ansible
AnsibleDay 7

AnsibleのDynamic IncludeとStatic Include

More than 1 year has passed since last update.

Ansible Advent Calendar 2016の7日目だけ空いていたので、遅ればせながら参加させていただきます。

ちなみに、InventoryじゃなくてIncludeのはなしです。

はじめに

Ansible 2.0以降、includeにはDynamic IncludeとStatic Includeの2種類が存在するようになりました。実は、意識せずとも、playbookの書き方次第で、dynamicになっていたり、staticになっていたりします。また、Ansibleの実行バージョンによっても、同じplaybookに対するincludeの解釈が異なるため、注意が必要な場合があります。

ここでは、下記公式のドキュメントを参考に、dynamicとstaticの違いを噛み砕いてみたいと思います。

Dynamic IncludeとStatic Include

簡単な例

Dynamic IncludeとStatic Includeの違いを一言で言うと、実行時にincludeが評価される(dynamic)か、実行前にincludeが評価される(static)という違いです。
まずは簡単な例で、それぞれの動作を見てみたいと思います。なお、下記はAnsible 2.2.0.0 にて動作を確認しています。

Static Includeの例

Ansible 2.1以降、普通にincludeした場合、Static Includeになります。実行例は下記の通りです。普通ですね。

playbook.yml
---

- hosts: all
  tasks:
  - include: ping.yml
ping.yml
---

- name: ping
  ping:

$ ansible-playbook -c local playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [ping] ********************************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0


$ ansible-playbook --list-tasks playbook.yml

playbook: playbook.yml

  play #1 (all): all    TAGS: []
    tasks:
      ping  TAGS: []

Ansible 1.9以前は、常にStatic Includeとして処理されていました。ただし、Ansible 2.0のバージョンのみ、常にDynamic Includeになっていたので、注意が必要です。

Dynamic Includeの例

Dynamic Includeになるケースは3通りあります。(Ansible 2.1以降)

  • static:noを宣言している場合
  • includeのファイル名に変数を含む場合
  • ループを使用したincludeを実行する場合

ただし、ansible.cfgでstaticを強制するオプションを設定している場合、Dynamic Include自体が無かったAnsible 1.9以前と同様、Dynamic Includeを実施しようとすると、エラーになります。

static:noを宣言している場合

include時、static: no を付けると、Dynamic Includeになります。

playbook.yml
---

- hosts: all
  tasks:
  - include: ping.yml
    static: no

$ ansible-playbook -c local playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [include] *****************************************************************
included: /home/heriet/work/ansible/example_include/example_dynamic/ping.yml for localhost

TASK [ping] ********************************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0                : ok=2    changed=0    unreachable=0    failed=0


$ ansible-playbook --list-tasks playbook.yml

playbook: playbook.yml

  play #1 (all): all    TAGS: []
    tasks:
      include   TAGS: []

実行してみると、Dynamic Includeでは ansible-playbook 実行時、 [include] のTASKが表示されることがわかります。
また、 ansible-playbook --list-tasks の出力結果をみると、Static Includeのときはpingが表示されていますが、Dynamicの場合はincludeになっていることがわかります。

includeのファイル名に変数を含む場合

Ansible 2.0以降、include時に変数を利用することができるようになりました。変数を利用してincludeした場合、明示的に static: no を付与していなくても、Dynamic Includeとして扱われるようになります。

playbook.yml
---

- hosts: all
  tasks:
  - include: "{{ include_file }}.yml"

$ ansible-playbook -c local --extra-vars="include_file=ping" playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [include] *****************************************************************
included: /home/heriet/work/ansible/example_include/example_dynamic/ping.yml for localhost

TASK [ping] ********************************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0


$ ansible-playbook --list-tasks playbook.yml

playbook: playbook.yml

  play #1 (all): all    TAGS: []
    tasks:
      include   TAGS: []

ループを使用したincludeを実行する場合

ループを使用したincludeも、Ansible 2.0以降でできるようになりました。下記のようなwith_itemsなどを使った例でも同様にDynamic Include扱いになります。

playbook.yml
---

- hosts: all
  tasks:
  - include: ping.yml
    with_items:
    - host1
    - host2
$ ansible-playbook -c local playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [include] *****************************************************************
included: /home/heriet/work/ansible/example_include/example_dynamic/ping.yml for localhost
included: /home/heriet/work/ansible/example_include/example_dynamic/ping.yml for localhost

TASK [ping] ********************************************************************
ok: [localhost]

TASK [ping] ********************************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=5    changed=0    unreachable=0    failed=0


$ ansible-playbook --list-tasks playbook.yml

playbook: playbook.yml

  play #1 (all): all    TAGS: []
    tasks:
      include   TAGS: []

ansible.cfgでstaticを強制する

Ansible 2.1 以降 ansible.cfgでstaticを強制するオプションを設定することもできます。

  • task_includes_static すべてのタスクが静的であることを強制する
  • handler_includes_static すべてのハンドラが静的であることを強制する

ためしに、task_includes_static=True に設定してループ内のincludeを実施してみると

$ ansible-playbook -c local playbook.yml
ERROR! You cannot use 'static' on an include with a loop

The error appears to have been in '/home/heriet/work/ansible/example_include/example_dynamic/playbook.yml': line 5, column 5, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

  tasks:
  - include: ping.yml
    ^ here

ERROR! You cannot use 'static' on an include with a loop と怒られてしまいました。

基本的にはデフォルトのまま、このオプションは無効化して使うことのほうが多いと思われます。

Dynamic Include と Static Includeの違い

上で挙げていたとおり、Dynamic Include と Static Includeの根本的な違いは実行時に評価されるか、実行前に評価されるかというところですが、それによって下記のような影響があります

  • Dynamic Includeされるhandlerにはnotifyできない
  • Dynamic Include内にしか存在しないタグは –list-tags で出力されない
  • Dynamic Include内のタスクは –list-tasks で出力されない

Dynamic Includeされるhandlerにはnotifyできない

例として、たとえば、OSによってhandlerを切り替えたい場合があるとします。下記のようなplaybookを用意しました。

playbook.yml
---

- hosts: all
  tasks:
  - include: hello.yml
  handlers:
  - include: "ping_handler_{{ ansible_os_family }}.yml"
hello.yml
---

- name: hello
  command: echo hello > hello
  notify: ping handler
ping_handler_Debian.yml
---

- name: ping handler
  ping:
$ ansible-playbook -c local playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [hello] *******************************************************************
ERROR! The requested handler 'ping handler' was not found in either the main handlers list nor in the listening handlers list

上記を実行すると。'ping handler' was not found と言われてエラーになってしまいます。ping_handler_Debian.ymlのなかに ping handler は居るのに何故でしょうか?

そう、handlersのincludeに変数を使用しているので、この部分はDynamic Includeになっています。当然、Dynamic Includeでは処理されるまで評価されないので、taskの実行時点ではping handlerは無い状態ということになります。

もし、上記のようなことがしたい場合は、whenを使うパターンが考えられます。

playbook.yml
---

- hosts: all
  tasks:
  - include: hello.yml
  handlers:
  - include: ping_handler_Debian.yml
    when: ansible_os_family == 'Debian'

この場合、Static Includeになるため、実行前に ping handler を認識してくれます

$ ansible-playbook -c local playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [hello] *******************************************************************
changed: [localhost]

RUNNING HANDLER [ping handler] *************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0

実際には、handlerだけOSごとにincludeすることはあまりないかもしれませんが、handlerを上手く認識していないときの可能性の1つとして頭に入れておいてもよいかもしれません。

Dynamic Include内にしか存在しないタグは –list-tags で出力されない

Dynamic Include内のタスクは –list-tasks で出力されない

上記の簡単な例で示した通り、Dynamic Includeされたものはその中のタスクは認識されず、includeされる旨だけが出力されます。--list-tagsも同様です。ちなみに、--start-at-taskや--tagsを指定した場合は意図通りDynamic Includeでも内部的なtaskやtagを都度認識して実行されます。

意図的にDynamic Includeにする例

汎用的なplaybook/roleの場合、OSやそのバージョンごとにincludeして処理を切り分ける場合がよくあります。たとえば、下記のような例です。

playbook.yml
---

- hosts: all
  tasks:

  - include: ping_RedHat.yml
    when: ansible_os_family == 'RedHat'

  - include:  ping_Debian.yml
    when: ansible_os_family == 'Debian'
ping_RedHat.yml
---

- name: ping A
  ping:

- name: ping B
  ping:

- name: ping C
  ping:
ping_Debian.yml
---

- name: ping D
  ping:

- name: ping E
  ping:

- name: ping F
  ping:

上記の例ではping A〜Fと名前をつけただけのものですが、実際にはなんらかのOSごとの処理がたくさん並んでいるものと思ってください。Dynamic Includeになる条件を満たしていないので、上記はStatic Includeになります。これを実行すると、下記のようになります。

$ ansible-playbook -c local playbook.yml

PLAY [all] *********************************************************************



TASK [setup] *******************************************************************
ok: [localhost]

TASK [ping A] ******************************************************************
skipping: [localhost]

TASK [ping B] ******************************************************************
skipping: [localhost]

TASK [ping C] ******************************************************************
skipping: [localhost]

TASK [ping D] ******************************************************************
ok: [localhost]

TASK [ping E] ******************************************************************
ok: [localhost]

TASK [ping F] ******************************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0

Debian環境で実行したので、A〜Cはそれぞれスキップされ、D〜Fは実行されたことがわかります。しかし、ping_RedHat.yml内にping A〜Cだけではなく、実際にはたくさんのタスクが並んでいたら、実行結果にskipが大量に並び、わずらわしいことになるでしょう。

そこで、明示的なDynamic Includeにしてみます。

playbook.yml
---

- hosts: all
  tasks:

  - include: ping_RedHat.yml
    when: ansible_os_family == 'RedHat'
    static: no

  - include:  ping_Debian.yml
    when: ansible_os_family == 'Debian'
    static: no
$ ansible-playbook -c local playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [include] *****************************************************************
skipping: [localhost]

TASK [include] *****************************************************************
included: /home/heriet/work/ansible/example_include/example_os/ping_Debian.yml for localhost

TASK [ping D] ******************************************************************
ok: [localhost]

TASK [ping E] ******************************************************************
ok: [localhost]

TASK [ping F] ******************************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=5    changed=0    unreachable=0    failed=0

初回のinclude(ping_RedHat.ymlのぶん)がスキップされた結果がでるだけで、見通しがよくなりました。大量のtaskのskipが煩わしく、Static Includeにする必然性がなければ、Dynamic Includeにしてしまうのが手です。

まとめ

Playbookの書き方によって、Dynamic IncludeになったりStatic Includeになる例を示しました。普段はその差を意識せずとも問題ないケースが多いですが、notifyやtask、tagなどの取扱いが微妙に異なるので、思わぬ落とし穴に落ちる場合があるかもしれません。また、内部taskのskipを表示させたくない場合に、意図的にDynamic Includeを使うと有効です。