I’d like to use task files and include_tasks for some lightweight reusable modules (I’m open to other module solutions, but want to avoid roles when my files are <50 lines). I’m trying to enable default values within the task files, while deferring to any outside values where possible.
Ansible’s behaviour seems to be to override task file variables with variables from the include_tasks, but not variables defined above:
playbook.yml
- name: var test
hosts: all
vars:
var: playbook-toplvl-default
tasks:
- name: Call task file
# setting vars here would override task-file.yml, but not above
# e.g. vars: { var: would-override }
include_tasks: task-file.yml
The example above prints Value: task-file-internal-default, where I would like it to print Value: playbook-toplvl-default instead. How best would I achieve my desired effect?
Two possible solutions I’ve come up with on my own:
use yaml anchors to duplicate vars:
- name: var test
hosts: all
vars: &vars
var: playbook-toplvl-default
tasks:
- name: Call task file
vars: *vars
include_tasks: task-file.yml
this requires an extra unusual step from the module user, increasing the chance of error every time the module is used. So not ideal
use a different internal variable name, and explicit default() usage
You do not have to create a complete role, if you want to have reuse only for particular exising role. But if you want reuse in other roles you will have to create separate role. You will just need role tasks/main.yml file and defaults/main.yml for defaults and put your “private” role in ./roles folder near tasks file you import/include that role
This is exacly what defaults/main.yml in your “private” role will do.
Create your role in ./roles folder in ./roles//tasks folder and include it (or import it).
When you import / include the role from your public role tasks file, all the variable from “private” role defaults will be overwritten by any other variable defined at the moment of import.
- include_role:
name: <your private role, no FQDN>
ansible will automatically look for role in ./roles in your public role tasks folder.
After further thought, I think allowing all upstream variables to override, like I had with solution 2., is more dangerous than the alternatives. If someone upstream sets the variable state to absent, that’s probably going to have a big impact on the behaviour of downstream modules without necessarily intending it. I should be keeping scope tight by requiring variables to be set next to the call.
With that in mind, this is the solution I’ve come up with:
playbook.yaml:
- name: include task test
hosts: all
vars:
_required_var: required
_var: toplvl-default
tasks:
- name: include task
include_tasks: task-file.yml
vars:
# renaming variable required because `var: {{ var }}` causes an infinite loop
var: '{{ _var }}'
required_var: '{{ _required_var }}'
task-file.yaml:
- name: included task
vars:
required_var: null
var: task-file-internal-default
block:
# might not be necessary if other tasks in this block do variable validation instead
- name: variable validation
assert:
that:
- required_var != None
fail_msg: |-
Missing required variables:
required_var: {{ required_var | default('UNDEFINED', true) }}
- name: print values
ansible.builtin.debug:
msg: 'Value: {{ var }} and {{ required_var }}'
By setting required_var: null, we ensure that if required_var is not explicitly set next to include_tasks, it doesn’t default to some other lower precedence value of required_var. That way we can keep the variable properly ‘required’.