Ansible Wrapper Roles

An Ansible role can include other roles as dependencies. This leads to the possibility of following the “wrapper” pattern, where you take a community role from Ansible Galaxy, which is being kept up-to-date with bug fixes and features, and then instead of forking the role, you merely include it within your own role, and adjust a small number of variables to customize it for your environment.

As an analogy in Chef, see Doing Wrapper Cookbooks Right.

The advantage of wrapping, rather than forking, is you can easily upgrade to the latest community version of the role. You don’t have to maintain the role with new bugfixes.


NOTE: Since this post was written I have discovered a better way to deal with roles variables Ansible Role Variables as Defaults. In fact, that obsoletes much of what was written here. Consider this post of historical interest only and read the newer one. 🙂


One difficulty when doing this in Ansible, is being able to overwrite the variables of the main role. A generic role might set variables in the following ways.

– vars/main.yml file
– defaults/main.yml file
– include_vars
– set_fact

Let’s say that it has set myvar=blue. What are the ways to override this in the wrapper role so that myvar=green ?

The wrapper role, for it’s part, might set this in:

– meta/main.yml as a role block parameter
– vars/main.yml file
– defaults/main.yml file
– include_vars
– set_fact
– a variable set at the playbook level, rather than in a role

Which of those will be effective to override the variable? YES means override worked. NO means override didn’t happen.

wrapped:vars/main.ymldefaults/main.ymlinclude_varsset_fact
wrapper meta/main.ymlYESYESYESYES
wrapper vars/main.ymlNOYESNONO
wrapper defaults/main.yml NONONONO
wrapper include_varsNONONONO
wrapper set_factNONONONO
playbook varNOYESNONO

In other words, the most effective method, and nearly the only method, is to set meta/main.yml in the wrapper role:

---
dependencies:
- { role: wrappedrole, myvar: "green" }

or another format:

---
dependencies:
- role: wrappedrole
  myvar: "green"

Next, what if we wanted to include some conditional logic? For example, the difference between RedHat and Debian.

---
dependencies:
- role: wrappedrole
  myvar: "debian_red"
  var2: "zxcvbn"
  when: ansible_os_family == 'Debian'
- role: wrappedrole
  myvar: "redhat_red"
  var2: "qwerty"
  when: ansible_os_family == 'RedHat'

This appears to be the best solution.

Next, what about overriding a template? The following method seems to do the trick:
– create a new template source file in templates/ directory of the wrapper role
– create a new template task in tasks/ directory of the wrapper role
– If you had propagated variables via the meta/main.yml file, as show in the previous step, those would be specific to the dependent role, and wouldn’t be available here, so you can set them again in tasks:

- name: set a fact
  set_fact:
    myvar: "debian_red"
    var2: "zxcvbn"
  when: ansible_os_family == 'Debian'
- name: set another fact
  set_fact:
    myvar: "redhat_red"
    var2: "qwerty"
  when: ansible_os_family == 'RedHat'

Or within vars/, defaults/, etc.

It’s less than ideal, that during each ansible run the template will be created by the generic role, and then recreated yet again by the wrapper role.  A brief window of time will exist during every deploy, when the file will be incorrect.  If this isn’t acceptable, then forking the generic role might be required, rather than a wrapper.

In the case of standard configuration files, as long as the service isn’t restarted while the file is being changed, it’s probably alright.   Consider that Ansible handlers are only called at the end of the play.   A web server or database server will only pick up the configuration change when it’s reloaded or restarted, which ought to be happening in the handlers section, and by then the final configuration file will be in place.

Other Experiments:

In the recommended solution just covered, more code is now present in meta/main.yml, which might otherwise be kept in tasks/main.yml, defaults/main.yml and vars/main.yml. Is there some way to move the variables back into vars/main.yml where they belong?

Experiment 1:

Define myvar in the wrapper’s vars/main.yml. In meta/main.yml change { role: wrappedrole, myvar: “green” } to { role: wrappedrole, myvar: “{{ myvar }}” } , thereby referencing the variable in vars/main.yml. The meta file now looks like:

---
dependencies:
- { role: wrappedrole, myvar: "{{ myvar }}" }

Result – “recursive loop detected in template string”. It failed.

Experiment 2:

Define _myvar in the wrapper’s vars/main.yml. (notice the underscore). In meta/main.yml change { role: wrappedrole, myvar: “green” } to { role: wrappedrole, myvar: “{{ _myvar }}” } , thereby referencing the variable in vars/main.yml. The meta file now looks like:

---
dependencies:
- { role: wrappedrole, myvar: "{{ _myvar }}" }

Result – This is successful. Variables set in vars/main.yml or defaults/main.yml will be funneled through meta/ , and override the wrapped role. However, variables set in tasks/ such as set_facts or include_vars will fail because they aren’t processed yet. Dependencies are run first, before tasks, and those items (set_facts and include_vars) are tasks. They haven’t been processed yet. So Experiment 2 is a partial success, in that it allows vars/ and defaults/ to override the wrapped role.   Again, the trick is to have different variable names (by adding an underscore) in the wrapper role, but leave the standard variable names in the generic role, since that won’t be modified at all.

Experiment 3:

Create a sibling role, which will be run before the wrapped role. meta/main.yml now looks like:

---
dependencies:
- { role: siblingrole }
- { role: wrappedrole, myvar: "{{ _myvar }"} }

In the sibling role, set _myvar in any way you’d like: As a default. As a variable. As a fact. In all these cases, because that dependency is run first, it will be set when running the wrappedrole, and the variable will be set successfully.

However, the question is whether that convoluted path of creating two wrapper roles (both the sibling and the main wrapper) is really worth the effort. Probably it is not.

In conclusion, those experiments 1-3 are quite interesting to clarify the order of Ansible processing, but they aren’t really streamlined solutions. Just placing the variables directly in meta/main.yml seems like the best answer.

Leave a Reply

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