[小ネタ] Ansibleでnestedなdict変数を正しくdeep mergeする方法2つ

# Ansibleで変数のmergeをするには？
Ansibleでは変数のFilterにJinja2を使用します。

時として辞書を含む変数を綺麗にdeep mergeしたい時ってないですか？
今日はAnsibleで変数のdeep mergeを実現させる方法を２つ紹介します。
何が違うかというと、Ansible 2.0以上 とそれ以下のバージョンでそれぞれ方法が変わってきます。

## Ansible version ">=2.0" の場合
Ansible 2.0から新たに **combine filter** が実装されました。
http://docs.ansible.com/ansible/playbooks_filters.html#combining-hashes-dictionaries

```yaml:test.yml
- hosts: localhost
  gather_facts: no
  vars:
    dict:
      foo:
        bar: 1
    dict2:
      foo:
        baz: 2
      qux: 2
    # combining hashes/dictionaries (new in version 2.0)
    dict_combine: "{{ dict | combine(dict2, recursive=True) }}"
  tasks:
    - debug:
        var: dict_combine
```

上記を実行すると下記の結果が得られると思います。

```text
$ ansible-playbook -i localhost, test.yml

PLAY [localhost] ***************************************************************

TASK [debug] *******************************************************************
ok: [localhost] => {
    "dict_combine": {
        "foo": {
            "bar": 1,
            "baz": 2
        },
        "qux": 2
    }
}

```

非常に便利ですね。
きちんとdict->fooの中身がmergeされています。
ちなみに「recursive」を省略すると単なる update mergeになります。

```text:recursive=false
$ ansible-playbook -i localhost, test.yml

PLAY [localhost] ***************************************************************

TASK [debug] *******************************************************************
ok: [localhost] => {
    "dict_combine": {
        "foo": {
            "baz": 2
        },
        "qux": 2
    }
}

```

ちなみにrecursiveを省略したcombine相当の機能はAnsible 2.0以前のバージョンでもupdate()を使うことで下記のように実現することができました。

```yaml:test.yml
- hosts: localhost
  gather_facts: no
  vars:
    dict:
      foo:
        bar: 1
    dict2:
      foo:
        baz: 2
      qux: 2
    # **[Note]**
    # jinja2 'do' tag need expression-statement extension
    # please set below to [default] section in ansible.cfg
    # jinja2_extensions=jinja2.ext.do
    dict_update: |
      {% do dict.update(dict2) %}
      {{ dict }}
  tasks:
    - debug:
        var: dict_update
```

## Ansible version "<2.0" の場合
色々な理由でAnsible 1.X台を使わざるを得ない場合、combineが使えずに多少不便です。
無論2.0で実装されたcombineのソース部分を取り込めばよいのですが、せっかくなので独自にFilter pluginを作ってみたいと思います。
http://docs.ansible.com/ansible/developing_plugins.html#filter-plugins

pluginを使うにはansible.cfgにパス(filter_plugins=)を通すか、ansible本体のプラグインディレクトリに直接カスタムファイルを置くことでansible実行時に読み込んでくれます。

```py:/path-to/ansible/filter_plugins/dict_merge.py
from copy import deepcopy


def dict_merge(a, b):
    if not isinstance(b, dict):
        return b
    result = deepcopy(a)
    for k, v in b.iteritems():
        if k in result and isinstance(result[k], dict):
                result[k] = dict_merge(result[k], v)
        else:
            result[k] = deepcopy(v)
    return result


class FilterModule(object):
    def filters(self):
        return {'dict_merge': dict_merge}
```

これでプラグインの作成は終了です。思ったより簡単ですね。
ではテストしてみます。

```yaml:test.yml
- hosts: localhost
  gather_facts: no
  vars:
    dict:
      foo:
        bar: 1
    dict2:
      foo:
        baz: 2
      qux: 2
    # custom filter plugin
    dict_merged: "{{ dict | dict_merge(dict2) }}"
  tasks:
    - debug:
        var: dict_merged
```

実行結果は下記の通りです。

```text
$ ansible-playbook -i localhost, test.yml

PLAY [localhost] ***************************************************************

TASK [debug] *******************************************************************
ok: [localhost] => {
    "dict_merged": {
        "foo": {
            "bar": 1,
            "baz": 2
        },
        "qux": 2
    }
}

```

combine同様の結果を得ることが出来ました。
もちろん更に深い階層のdictもmergeしてくれます。
1.X 系でサクッとdeep mergeを使いたい場合はこういう手もあるので、検討されてみてはいかがでしょうか。

## その他
Filterプラグインの他にもAnsibleには独自に作ることのできるプラグインがいくつかあります。
http://docs.ansible.com/ansible/developing_plugins.html

Lookupやcallbacksプラグインは比較的情報も多く色んな事をしている方の情報がありますね。

Let's enjoy your Ansible life!