In Ansible 2.0 the combine filter was added. At a glance it appears useful, but the examples they provide in the documentation are missing some grunt. A bigger and more useful example would be “I need to manage 2 similar but different named databases per environment across 100 environments.” Manage in this article will be scoped to only users/passwords.

You could do this with a massive set of variables files, or you could do it with, say, 1 set of inventory variables and "{{ ... | combine({'key':'value'}) }}" .

To start with, I’m going to make the following assumptions about our fake environment:

One environment named “prod”

One environment named “staging”

Every other environment has a variable attached to it named “env_name”

The databases outside of production/staging are always (using Jinja2) "db_{{ env_name }}"

Given the following defaults/main.yml and vars/main.yml :

vars/main.yml:

--- base_dbs: main: prod: main staging: main_staging events: prod: events staging: events_staging

defaults/main.yml:

--- env_name: default db_users: - name: writer password: prod: aaa staging: bbb default: ccc - name: reader password: prod: aaa staging: bbb default: ccc default_dbs: main: default: main_development events: default: main_development ### The good stuff! dbs: "{{ default_dbs | combine({'main':{env_name: 'main_'+env_name}},{'events':{env_name: 'events_'+env_name}},base_dbs,recursive=True) }}" main_perms: "SELECT,INSERT,UPDATE,DELETE,CREATE TEMPORARY TABLES,TRIGGER,SHOW VIEW,EXECUTE" read_perms: "SELECT,SHOW VIEW" permissions: writer: "{{ dbs.main[env_name] }}.*:{{ main_perms }}/{{ dbs.events[env_name].*:{{ main_perms }}" reader: "{{ dbs.main[env_name] }}.*:{{ read_perms }}/{{ dbs.events[env_name].*:{{ read_perms }}"

We can now use the following task, say, tasks/configure_users.yml to configure these users under MySQL:

--- - name: MYSQL USERS | Create MySQL Users mysql_user: name: "{{ item.name }}" password: "{{ item.password[env_name] | default(item.password['default'])] }}" # Don't use this outside of an example! host: "%" priv: "{{ permissions[item.name] }}" with_items: db_users

Now a breakdown of what’s going on here…

The combine filter takes in a dictionary ( default_dbs ), it then takes a,b,c,…,n dictionaries ( {'main':{env_name: 'main_'+env_name}} , {'events':{env_name: 'events_'+env_name}} , and base_dbs ). An important part of this process is that out of a,b,c,…,n — a overrides the input, b overrides a (and the input), c overrides b (and a, and the input), and the final dictionary “n” (note: it’s perfectly acceptable to have a single dictionary here) overrides everything. recursive=True is explained in the documentation:

The filter also accepts an optional recursive=True parameter to not only override keys in the first hash, but also recurse into nested hashes and merge their keys too: {{ {'a':{'foo':1, 'bar':2}, 'b':2}|combine({'a':{'bar':3, 'baz':4}}, recursive=True) }} This would result in: { 'a' :{ 'foo' : 1 , 'bar' : 3 , 'baz' : 4 }, 'b' : 2 }

This isn’t crystal clear though, if you leave out recursive=True I’ve found that it simply overrides the hash, so in the above example you’d end up with a dictionary of {'a':{'bar':3, 'baz':4}, 'b':2} , as 'a' would be over-ridden entirely by the combine filter.

A side-note on why “base_dbs” lives in vars/main.yml instead of defaults/main.yml . The vars directory is one of the highest precedence values, and typically something like a production database name is something that one would not want to override. Because of this, it is better suited to be stored as a variable instead of a default.