Ansible Advent Calendar 9日目の話です。
Ansibleのvarsをマージするために、既存の方法よりもう少しだけ便利なfilterを作りました。
Ansibleのvarsのマージする
filterの紹介の前に、既存の手段と問題点について。
Ansibleにおいて、varsをマージしようとすると、主に以下の2つの手段を取ることになります。
combine filter
公式にも記載がある通りですが、以下のようになります。
{{ {'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
デフォルトの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でいいのですが、上記方法ではそうはならず、この場合もまた上書きされてしまいます。
どういうケースで欲しくなるか
例えば、標準として必ず作成するディレクトリがある場合、概ね以下のような書き方になると思います。
default_dirs:
/etc/some_directory:
state: directory
mode: 0755
owner: root
group: root
/etc/some_directory2:
state: directory
mode: 0640
owner: root
group: root
- 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を作成します。
extra_dirs:
/etc/some_directory3:
state: directory
mode: 0755
owner: root
group: root
/etc/some_directory1:
owner: user1
group: user1
あとは、taskを以下のように書き換えます。
- 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をいい感じでマージしてくれています。
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は、更に複雑なマージができます。
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) }}
にかけると、以下のようになります。
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やミドルウェア、それらのバージョンに応じて追加の設定を行いたいときにマージとして使っています。