Ansible Role Variables as Defaults

[jump directly to the code]

Here is a trick for creating Ansible role vars (defined in role/vars/main.yml) so they act as role defaults (defined in role/defaults/main.yml)

What does this mean, and why would it be interesting?

An Ansible Role is a reusable and redistributable unit of code. Ansible Galaxy encourages code sharing via Roles. But the author of an Ansible Role cannot predict how the end user would like to customize variables for their environment using group_vars, host_vars, play vars, inventory, etc. Everything ought to be customizable by each different user who downloads a role, because every environment is different.

The problem is that role vars (role/vars/main.yml) have high precedence and can’t be easily overridden by group_vars or inventory.

A solution might be to set the variables as role defaults instead of role vars. This would be an ideal solution, except Ansible doesn’t permit logic or python code in role/defaults/main.yml file, so how do you define different variables for different OS distributions? With variables it can be done like this:

- name: Include Distribution version specific variables
  include_vars: "{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml"

That works for “include_vars” and vars, but not “include_defaults” and defaults. There is no such thing as “include_defaults” in Ansible.

When you run “include_vars”, the precedence is at the level of “var”, and it cannot be overridden by inventory variables.

Solution 1: (not optimal)

Let’s say the variable is called “some_variable”.

In the role vars (defined in role/vars/main.yml), create the variable as

default_some_variable: "abc"

Notice that is has been prefixed with the text “default_”

Then, in your own customized inventory such as group_vars/all, create the variable with the regular name such as

some_variable: "xyz"

Finally, whenever you reference the variable some_variable in a task, write it as this:

{{ some_variable | default(default_some_variable) }}

That is the jinja2 filter called “default”, and if the original variable doesn’t exist, then it goes to the default instead.

Yes. This is the desired logic. This is the goal. However, the problem here is how cumbersome it is to always write “{{ some_variable | default(default_some_variable) }}”, and to not forget to add it in every task.

Is there a way for all the tasks that reference “some_variable” to just write “some_variable” and nothing more?

Solution 2: ( the trick!! )

Copy into role/tasks/main.yml:

# Variables ##########

- name: Include OS-specific variables.
  include_vars: "{{ ansible_os_family }}-family.yml"

- name: Include Distribution version specific variables
  include_vars: "{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml"

# code from https://www.samdarwin.com/ansible-default-vars/
- name: set facts based on defaults
  set_fact:
    "{{ item[8:] }}": "{{ lookup('vars', item) }}"
  when:
    - item is match("^default_")
    - vars[item[8:]] is not defined
  with_items:
    "{{ vars | list }}"


##########

#and then proceed adding tasks as usual...

How does this all work?

As with the earlier Solution 1, you will add default_ variables into the vars files. Prefix them with “default_”

default_some_variable: abc
default_another_variable: 123

and in the group_vars or any other locations, write the real values:

some_variable: xyz
another_variable: 456

Let’s analyze the “set facts based on defaults” task, in reverse order, to understand it.

the line: with_items: “{{ vars | list }}”
explanation: It’s going to parse all variables Ansible knows about. All of them.

the lines: when: item | match(“^default_”)
explanation: If the variable starts with “default_”, then something is going to be done, otherwise it will be ignored.

the line: vars[item[8:]] is not defined
explanation: item[8:] means chop off the first eight characters. So it would convert default_some_variable to some_variable. Let’s rewrite it again, with that as the example. If vars[some_variable] is not defined, then an action will be taken. A default is required when the variable doesn’t exist.

the line: set_fact: “{{ item[8:] }}”: “{{ lookup(‘vars’, item) }}”
explanation: item[8:] means chop off the first eight characters. So it would convert default_some_variable to some_variable. Let’s rewrite it again, with that as the example.

the line: set_fact: “{{ some_variable }}”: “{{ lookup(‘vars’, default_some_variable) }}”
explanation: some_variable will be set to the value of default_some_variable

4 responses to “Ansible Role Variables as Defaults”

  1. Vineet says:

    Hi , So i was also struggling to apply it correctly in my roles , I come from chef background where this is really driven by overrides in attributes and we just declare it in metadata.rb there and all other overrides in attributes and recipes just works so inheritance is just induced in chef , In Ansible still I am finding it hard to do that stuff , the concept is same we need to include the generic role in wrapper , there must be a good way to wrap the attributes and easily pass the data in custom roles , it’s really decrease the flexibility in this tool.

    I still don’t know if I should overwrite the generic role variable be defining the wrapper variables inside vars/main.yml or I just use separate file and just include that inside the play which defeats the purpose , so please tell me if you got any good way to do the wrapper role other than the above !

    • sdarwin says:

      Hi Vineet,
      Nice to hear from you.
      How to handle wrapper roles? Yes, this is something which Chef excels at.
      – If the source role has set “default” variables, you can override them in the wrapper with “vars”. Or, even better, “group_vars”.
      – If the source role has set “vars” variables, you can fork the role, rather than use a wrapper. OR, set meta variables as mentioned here https://www.samdarwin.com/ansible-wrapper-roles/, although it’s not the most elegant implementation. OR, send the author a pull request to fix their role to utilize “defaults” as mention in the current article.
      An idea to consider is moving inventory into it’s own subdirectory. inventory/staging/group_vars & hosts. inventory/production/group_vars & hosts. Then customize the variables per group. So, you would be setting the so-called “override” variables in a different place than usual, which is now in the inventory.

  2. Joe Gainey says:

    How do you handle the case when you have a boolean variable?

    After applying this to my own role variables they ended up being of type str instead of type bool.

    • sdarwin says:

      Hi Joe,
      I have just tested this, and interestingly the results were exactly the opposite. The variables ended up being of type bool, not type string. This is somewhat corroborated by https://github.com/ansible/ansible/issues/42599 where set_fact converts variables to boolean.
      Are you using the latest ansible?
      Could you post a very minimal test case on github which demonstrates the problem?

Leave a Reply

Your email address will not be published. Required fields are marked *