AnsibleのDynamic IncludeとStatic Include


[Ansible Advent Calendar 2016](http://qiita.com/advent-calendar/2016/ansible)の7日目だけ空いていたので、遅ればせながら参加させていただきます。

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

# はじめに

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

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

* [docs.ansible.com: Dynamic versus Static Includes](http://docs.ansible.com/ansible/playbooks_roles.html#dynamic-versus-static-includes)
* [docs.ansible.com: include](https://docs.ansible.com/ansible/include_module.html)

# 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になります。実行例は下記の通りです。普通ですね。

```yaml:playbook.yml
---

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


```yaml: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になります。

```yaml: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として扱われるようになります。


```yaml: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扱いになります。

```yaml: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を用意しました。

```yaml:playbook.yml
---

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

```yaml:hello.yml
---

- name: hello
  command: echo hello > hello
  notify: ping handler
```

```yaml: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を使うパターンが考えられます。

```yaml: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して処理を切り分ける場合がよくあります。たとえば、下記のような例です。

```yaml:playbook.yml
---

- hosts: all
  tasks:

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

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

```yaml:ping_RedHat.yml
---

- name: ping A
  ping:

- name: ping B
  ping:

- name: ping C
  ping:
```

```yaml: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にしてみます。

```yaml: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を使うと有効です。