As suggested in the hash behaviour article of Ansible documentation:
DEFAULT_HASH_BEHAVIOUR
Description: This setting controls how variables merge in Ansible. By default Ansible will override variables in specific precedence orders, as described in Variables. When a variable of higher precedence wins, it will replace the other value. Some users prefer that variables that are hashes (aka ‘dictionaries’ in Python terms) are merged. This setting is called ‘merge’. This is not the default behavior and it does not affect variables whose values are scalars (integers, strings) or arrays. 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 In version 2.0 a combine filter was added to allow doing this for a particular variable (described in Filters).
Source: https://docs.ansible.com/ansible/latest/reference_appendices/config.html#default-hash-behaviour
The recommended way to achieve this would be to use the combine filter.
This said, you do have a list inside your dictionary, and this will make it quite a complex task to achieve.
Switching it to a dictionary would ease the pain:
services:
app:
environment:
RAILS_ENV: development
overridable_var: foo
db:
foo: bar
If I understand it properly, you are trying to put in place the alternative directory layout of Ansible best practices.
If this is the case, here could be a solution matching your use case, given that you can transpose your list inside services in a dictionary, as proposed above:
- In the group_var files for each environment, prefix the
services dictionary key by the name of the environment, e.g. develop_services
develop_services:
app:
environment:
RAILS_ENV: development
overridable_var: develop from inventories group_vars
- In your playbook, as a first task, or even as a
pre_task, combine the services dictionary with the one matching your environment, which you can get as part of your inventory_file path:
pre_tasks:
- set_fact:
services: "{{ services | combine(vars[inventory_file.split('/')[-2] ~ '_services'], recursive=True) }}"
Then just use it.
Given the directory layout:
.
├── group_vars
│ └── app1.yml
├── inventories
│ ├── develop
│ │ ├── group_vars
│ │ │ └── app1.yml
│ │ └── hosts
│ └── staging
│ ├── group_vars
│ │ └── app1.yml
│ └── hosts
└── play.yml
group_vars/app1.yml
services:
app:
environment:
bar: foo
overridable_var: from root group_vars
do_not_override_me: from root group_vars
db:
engine: postgres
inventories/develop/hosts
all:
children:
app1:
hosts:
app:
inventories/develop/group_vars/app1.yml
develop_services:
app:
environment:
RAILS_ENV: development
overridable_var: develop from inventories group_vars
inventories/staging/hosts
all:
children:
app1:
hosts:
app:
inventories/staging/group_vars/app1.yml
staging_services:
app:
environment:
RAILS_ENV: staging
overridable_var: staging from inventories group_vars
play.yml
- hosts: all
gather_facts: no
pre_tasks:
- set_fact:
services: "{{ services | combine(vars[inventory_file.split('/')[-2] ~ '_services'], recursive=True) }}"
tasks:
- debug:
msg: "{{ services }}"
Running it for develop would give the recap:
$ ansible-playbook play.yml -i inventories/develop
PLAY [all] *********************************************************************************************************
TASK [set_fact] ****************************************************************************************************
ok: [app]
TASK [debug] *******************************************************************************************************
ok: [app] => {
"msg": {
"app": {
"environment": {
"RAILS_ENV": "development",
"bar": "foo",
"do_not_override_me": "from root group_vars",
"overridable_var": "develop from inventories group_vars"
}
},
"db": {
"engine": "postgres"
}
}
}
PLAY RECAP *********************************************************************************************************
app : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Running it for staging would give the recap:
$ ansible-playbook play.yml -i inventories/staging
PLAY [all] *********************************************************************************************************
TASK [set_fact] ****************************************************************************************************
ok: [app]
TASK [debug] *******************************************************************************************************
ok: [app] => {
"msg": {
"app": {
"environment": {
"RAILS_ENV": "staging",
"bar": "foo",
"do_not_override_me": "from root group_vars",
"overridable_var": "staging from inventories group_vars"
}
},
"db": {
"engine": "postgres"
}
}
}
PLAY RECAP *********************************************************************************************************
app : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
An extra note: mind that this would not allow the playbook to be run on two environment at the same time, as pointed by the note under the section using multiple inventory sources of the documentation:
Keep in mind that if there are variable conflicts in the inventories, they are resolved according to the rules described in How variables are merged and Variable precedence: Where should I put a variable?. The merging order is controlled by the order of the inventory source parameters. If [all:vars] in staging inventory defines myvar = 1, but production inventory defines myvar = 2, the playbook will be run with myvar = 2. The result would be reversed if the playbook was run with -i production -i staging.
Source: https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#using-multiple-inventory-sources