ネストしたAnsible varsをいい感じでマージするfilterを作った

[Ansible Advent Calendar 9日目](https://qiita.com/advent-calendar/2017/ansible)の話です。

Ansibleのvarsをマージするために、既存の方法よりもう少しだけ便利なfilterを作りました。

[deep_merge filter](https://github.com/tkit/ansible_filter_deep_merge)

# Ansibleのvarsのマージする

filterの紹介の前に、既存の手段と問題点について。

Ansibleにおいて、varsをマージしようとすると、主に以下の2つの手段を取ることになります。

## combine filter

[Combining hashes/dictionaries](http://docs.ansible.com/ansible/latest/playbooks_filters.html#combining-hashes-dictionaries)

公式にも記載がある通りですが、以下のようになります。

```
{{ {'a':{'foo':1, 'bar':2}, 'b':2}|combine({'a':{'bar':3, 'baz':4}}, recursive=True) }}
# -> {'a':{'foo':1, 'bar':3, 'baz':4}, 'b':2}
```

このように、最初にあるものを起点として、それに`combine`フィルターで指定しているものを重ねていくようなイメージになります。

## configのhash_behavior

[hash behavior](http://docs.ansible.com/ansible/latest/intro_configuration.html#hash-behaviour)

デフォルトのAnsibleのconfigでは、同一のvarsを複数の箇所で設定した場合、後勝ちになります。つまり、roleで指定したものは`group_vars`に上書きされ、さらに`host_vars`に上書きされます。
しかし、このconfigの値を変更することで、この「上書き」の振る舞いをmergeに変えることができます。

```
hash_behaviour = merge # replaceから変更
```

しかし、公式ではこのconfigを使うことには慎重になるような注意書きがなされています。

> We generally recommend not using this setting unless you think you have an absolute need for it, and playbooks in the official examples repos do not use this setting:

一見便利なのですが、この値を設定してしまうと、Ansibleの基本的な挙動そのものが変わってしまい、本来mergeしたかった部分「以外」も振る舞いが変わってしまうことと、結局複数の設定箇所のmergeが最終結果になることから「結局どれが最終的な値なのか」が分かりづらくなってしまうためだと思われます。

# 既存の解決策の問題点

hash(dictionary)としてのマージなので、基本的に後勝ちで上書きされてしまいます。また、リストの場合はappendでいいのですが、上記方法ではそうはならず、この場合もまた上書きされてしまいます。

# どういうケースで欲しくなるか

例えば、標準として必ず作成するディレクトリがある場合、概ね以下のような書き方になると思います。

```yaml:vars
default_dirs:
  /etc/some_directory:
    state: directory
    mode: 0755
    owner: root
    group: root
  /etc/some_directory2:
    state: directory
    mode: 0640
    owner: root
    group: root
```

```yaml:tasks
  - name: create directories
    file:
  	  path: "{{ item }}"
  	  state: "{{ default_dirs[item].state }}"
  	  mode: "{{ default_dirs[item].mode }}"
  	  owner: "{{ default_dirs[item].owner }}"
  	  owner: "{{ default_dirs[item].group }}"
  	with_items: "{{ default_dirs | list }}"
```

しかし、個別の要件があり、以下の対応を行おうとすると少し面倒になります。

* some_directory3を追加したい
* some_directory1のowner/groupをuser1に変更したい

同じ「ディレクトリを作成する」というtaskであるにも関わらず、with_itemsで2回動かすしかないように見えます。
まとめたほうが見通しは良くなるような気がしますが、これは既存の方法では対応できません。
また、そうであったとしても、既に定義済みのvarsの一部だけを変えたい場合にも対応できません。

# deep_merge filterによる解決

これを、deep_merge filterを使うことによって解決してくれます。
以下のようなvarsを作成します。

```yaml:vars
extra_dirs:
  /etc/some_directory3:
    state: directory
    mode: 0755
    owner: root
    group: root
  /etc/some_directory1:
    owner: user1
    group: user1
```

あとは、taskを以下のように書き換えます。

```yaml:tasks
  - set_fact:
      merged_dirs: "{{ default_dirs | deep_merge(extra_dirs) }}" # deep_mergeを追加

  - name: create directories
    file:
  	  path: "{{ item }}"
  	  state: "{{ merged_dirs[item].state }}"
  	  mode: "{{ merged_dirs[item].mode }}"
  	  owner: "{{ merged_dirs[item].owner }}"
  	  owner: "{{ merged_dirs[item].group }}"
  	with_items: "{{ merged_dirs | list }}"
```

これだけです。

原理的には、上記の例でいう default_dirsとextra_dirsをいい感じでマージしてくれています。

```yaml:deep_merge後
merged_dirs:
  /etc/some_directory:
    state: directory
    mode: 0755
    owner: user1
    group: user1
  /etc/some_directory2:
    state: directory
    mode: 0640
    owner: root
    group: root
  /etc/some_directory3:
    state: directory
    mode: 0755
    owner: root
    group: root
```

# もう少し複雑なマージ

実は、上記だけならcombineフィルターと同じです。
deep_mergeは、更に複雑なマージができます。

```yaml:deep_merge例
original:
	a:
		b: "varb"
		c: "varc"
		d: 
			- vard1
			- vard2
	e:
		b: "varb"
merge1:
	a:
		c: "cvar"
		d: 
			- vard3
	e:
		b:
merge2:
	a:
		d: 
			- vard3
```

これを `{{ original | deep_merge(merge1,merge2) }}` にかけると、以下のようになります。

```yaml:deep_merge後
	a:
		b: "varb"
		c: "cvar" # merge1より
		d: 
			- vard1
			- vard2
			- vard3 # merge1 より
			- vard3 # merge2 より
	e:
		b: "varb" # merge1によって上書きで消されそうだが、空の場合は消さない
```

このように、hashだけでなくlist形式でもちゃんとmergeしてくれます。また、複数のマージにも対応しています。
原理的には、`original` に対して`merge1`をマージし、その後の結果に`merge2`をマージしています。

# 終わりに

実は公式のcombineフィルターのソースを流用して、ほとんど同じことをしているのですが、必要になったので作りました。
あんまり複雑なマージをしすぎると、結局何と何を組み合わせて結果は何を作っているのかよく分からなくなるので、必要に応じて必要なところだけ使うのがいいのかなと思いました。

実際どこで使っているかというと、システムのデフォルトでの設定に加えて、OSやミドルウェア、それらのバージョンに応じて追加の設定を行いたいときにマージとして使っています。