Ansibleで同じディクショナリーに属する変数をset_factで複数回上書きしても、反映されるのは一番最後の上書きだけだった


概要

たまにCI/CDでやりたくなるAnsibleのvars上書き芸。

こんな感じの優先順位で上書きされますよね。(下に行くほど優先順位が高い)

command line values (eg “-u user”)
role defaults [1]
inventory file or script group vars [2]
inventory group_vars/all [3]
playbook group_vars/all [3]
inventory group_vars/* [3]
playbook group_vars/* [3]
inventory file or script host vars [2]
inventory host_vars/* [3]
playbook host_vars/* [3]
host facts / cached set_facts [4]
play vars
play vars_prompt
play vars_files
role vars (defined in role/vars/main.yml)
block vars (only for tasks in block)
task vars (only for the task)
include_vars
set_facts / registered vars
role (and include_role) params
include params
extra vars (always win precedence)

参考: Using Variables — Ansible Documentation



例えば… ansible.cfghash_behaviour=merge としておけば…

roles/hoge_role/defaults/main.yml とかで下記みたいに変数を定義して…

dict:
  a: "aaa"
  b: "bbb"
  c: "ccc"

hosts/host_group_a/group_vars/all.yml で下記のように定義すれば。

dict:
  a: "override"

こんな結果が得られます。

dict:
  a: "override"
  b: "bbb"
  c: "ccc"

このノリで set_fact を触ったらハマった。

やりたかったこと

roles/hoge_role/defaults/main.yml とかで下記みたいに変数を定義。

dict:
  a: "aaa"
  b: "bbb"
  c: "ccc"

hosts/host_group_a/group_vars/all.yml で下記のように定義した上で…

dict:
  a: "override"

更に bc についても上書きする。この上書きは特定の --extra-vars で指定した変数から値を持ってくる。
なおかつこれらの変数は指定されないこともある…

などというトリッキーなことをしたいとする。

ハマった方法

- name: "Load B"
  set_fact:
    dict:
      b: "{{ B }}"
  when: B is defined

- name: "Load C"
  set_fact:
    dict:
      c: "{{ C }}"
  when: C is defined

とかってして、 --extra-vars "B=hoge" とか --extra-vars "C=fuga" とか --extra-vars "B=hoge C=fuga" としたかった。

で、 --extra-vars "B=hoge C=fuga" の時にうまく行かない。こんな感じになるわけですね。

dict:
  a: "override"
  b: "bbb"
  c: "fuga"

本当に必要としていた結果はこれですよね。

dict:
  a: "override"
  b: "hoge"
  c: "fuga"

どうやら、set_factのmargeには癖があるらしい。

どうするのが良いんだろうね

こんな感じ?釈然としない。

- name: "Load B"
  set_fact:
    dict:
      b: "{{ B }}"
  when:
    - B is defined
    - not C is defined

- name: "Load C"
  set_fact:
    dict:
      c: "{{ C }}"
  when:
    - not B is defined
    - C is defined

- name: "Load B and C"
  set_fact:
    dict:
      b: "{{ B }}"
      c: "{{ C }}"
  when:
    - B is defined
    - C is defined

追記: 寄せられたもう少しスマートな方法

@hiroyuki_onodera さんよりコメント頂いた方法。

- name: Load B,C
  set_fact:
    dict:
      b: "{{ B|default(omit) }}"
      c: "{{ C|default(omit) }}"

なるほど。 default フィルターってのがあるんですね。こちょこちょいじらない場合はこれがスマートそう。

フィルター — Ansible Documentation

まとめ

あんまりトリッキーなことはやめておきましょう。
ドキュメント流し読みしかしてないけど、どこかにこの挙動って書いてあったっけ…?