pez
pez

Reputation: 83

Ansible: Switch/Override variables depending on target inventory

I'm thinking about deploying rails apps on hosts in three different environments(prod/stag/dev) using Ansible. But struggling to set different RAILS_ENV for each host.

I've tried with the following setup.

Directory structure:

.
├── group_vars
│   └── app1.yml
├── inventories
│   ├── develop
│   │   └── group_vars
│   │        └── app1.yml
│   ├── production
│   └── staging
└── roles

group_vars/app1.yml(for common vars across environments)

services:
  - name: app
    environment:
      {env_vars other than RAILS_ENV}: foo
  - name: db
...

inventories/develop/group_vars/app1.yml(for inventory specific vars)

services:
  - name: app
    environment:
      RAILS_ENV: development

It seems Ansible only looks at keys at top level when judging if the variable is already defined. So the RAILS_ENV in the second file gets unintentionally ignored. (Not sure why inventory vars are lower in precedence order, I think it should be higher because it is more specific?)

Is there any clean way to do this?

Upvotes: 2

Views: 1527

Answers (1)

β.εηοιτ.βε
β.εηοιτ.βε

Reputation: 39264

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:

  1. 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
    
  2. 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

Upvotes: 2

Related Questions