3
votes

TL; DR

With Ansible I am trying to define some kind of default variable at playbook level - let's call it playbook defaults - which have precedence other roles defaults but can be overriden by the inventories inventory group_vars/all variables.

How to define some kind of playbook defaults variables which would have precedence other roles defaults but be overridable by inventories (environments) ?


Currently Ansible 2.x variable precedence is such as:

  • role defaults
  • inventory file or script group vars
  • inventory group_vars/all
  • playbook group_vars/all

What I am looking to achieve is something like:

  • role defaults
  • playbook defaults
  • inventory file or script group vars
  • inventorygroup_vars/all
  • playbook group_vars/all

From most tools and apps I used before, variables defined at environment level (dev, test, QA, prod, etc.) have precedence other "application" variables, which themselves have precedence other "external" components. The precedence other would then be:

  • "External" components vars (Roles from Galaxy)
  • "Application" vars (Playbook vars)
  • "Environment" vars (dev, test, etc.)

With Environment having the most precedence. But I cannot find a way to reproduce this pattern with Ansible. Such things are easy in Chef (with external cookbooks defaults being overriden by wrapper cookbook default) and Puppet, but I can't find a way to achieve the same with Ansible.

Example with a simple application

Let's consider an application consisting of several services interacting with a webserver. These services are generic and re-used in other parts of my organization, but in my case I used apache2. I have these roles:

  • apache
  • green_service
  • blue_service
  • red_service

And these default vars:

# roles/apache/defaults/main.yml
apache_listen_port: 80

# roles/green_service/defaults/main.yml
green_service_port: 80

# roles/blue_service/defaults/main.yml
blue_service_port: 80

# roles/red_service/defaults/main.yml
red_service_port: 80

Each service must know the apache port and this is represented in the default. If other applications need the green service with Nginx or another webserver, they just have to include the related roles.

Now, what if I want to change the port from 80 ot 8081 in one or more environments?

The "straightforward" way

.. I end-up having to duplicate all these variables in each environment such as:

# production/group_vars/all/all.yml
apache_listen_port: 8081
green_service_port: 8081
blue_service_port: 8081
red_service_port: 8081

# ... and so on in each environments inventory!

This may be fine in this simple example, but when you have 10+ services with 5+ environment, it becomes a nightmare... Updating a simple variable may have impacts in lots of services and it becomes hard to maintain and understand.

Appart from the answer "Your service design may be wrong..." I am looking for a way to avoid this.

What I would like to achieve

What I would like to achieve is being able to represent the variables links between my services and the webserver at playbook level such as:

# somewhere in my playbook .yml
# these variables override roles default
# but can be overriden in my inventories
myapp_port: 80

apache_listen_port: "{{ myapp_port }}"
green_service_port: "{{ myapp_port }}"
blue_service_port: "{{ myapp_port }}"
red_service_port: "{{ myapp_port }}"

Now in each environment I just have to override one and only one variable such as:

# production/group_vars/all/all.yml
myapp_port: 8081

I did not find a sensible way without over complexifying my playbooks to achieve something similar.

With Ansible, is there a proper way to accomplish this Playbook vars having precedence other Defaults vars but not Inventories vars?

1

1 Answers

3
votes

Keep in mind that in Ansible {{ ... }} expressions are usually templated in lazy manner (except set_fact), so you not "assign" a variable, but tell Ansible how to get the value in runtime.

If you have control over roles:

you can make roles/apache/defaults/main.yml:

apache_listen_port: "{{ myapp_port | default(80) }}"

and roles/green_service/defaults/main.yml:

green_service_port: "{{ myapp_port | default(80) }}"

In this case when Ansible bumps into apache_listen_port in some expression, it will template the value to myapp_port's value (if it is available in that particular moment) or use 80 otherwise.

If you define myapp_port in required inventories, then apache_listen_port and green_service_port will get its value.

If you do NOT have control over roles:

you can make this trick inside playbook:

vars:
  myapp_port_playbook: "{{ myapp_port | default(80) }}"
  apache_listen_port: "{{ myapp_port_playbook }}"
  green_service_port: "{{ myapp_port_playbook }}"

in this case apache_listen_port and green_service_port are templated directly to myapp_port_playbook's value, but it's value in turn is myapp_port's value or 80 by default.