I’m quoting your initial “Simulated Playbook (code not tested)” here because I want to point out something that I think is relevant to your situation. The preceding discussions of variable precedence and playbook vs. host scopes are all well and good, but you need a strategy to deal with this information.
That wouldn’t pass code review in our shop. You’ve got two, maybe three separate things here depending on how your count them, all with the same name – a recipe for confusion.
Now that you’ve become aware of how scoping and precedence interact (to over-generalize, hoping that things will “just work” is insufficient), you need some guidelines for when data goes through various “scope transitions”. For our purposes a scope transition includes data from outside of a role being passed to or used inside of roles.
Taking the example above, the set_fact above is fine in and of itself, but it’s happening outside of the my_role role, so it’s name should reflect that out-of-role origin — well, at least to the extent that it shouldn’t have a name that makes it look like a role variable. Call it something else, then be explicit about the scope transition when you do the include_role:
- name: Set the list of relevant fruits
ansible.builtin.set_fact:
fruits_of_my_loom:
- apple
- banana
- apple
- blueberry
- apple
- name: Include modular role
ansible.builtin.include_role:
name: my_role
tasks_from: main.yml
vars:
my_role_config: "{{ fruits_of_my_loom }}"
After which my_role_config only exists in the context of this host’s invocation of my_role.
Likewise, any set_facts done within a role should (and these are our guidelines, not rules, so “should” rather than “must”) be prefixed with the role name. Therefore any data prefixed by my_role_ was either passed to the role explicitly, set_facted by the role (that’s how the role exposes values it wants to “export”), or came from role defaults or role vars.
Here’s a case where the above was causing minor confusion and how we fixed it. We set a variable in group_vars/mw_itweb_non.yml (non-prod apache httpd servers) that was
mw_common_apache_module_override :
- 'ssl'
- ['mpm_prefork', false ]
- ['mpm_worker', true ]
- 'proxy_wstunnel'
This is used by our mw_common_apache role to override that role’s default list of which apache modules to load. The confusion arose because it wasn’t set where the role was invoked, nor in the role itself. We renamed the variable mw_itweb_apache_module_override to match the related adjacent variables (still not great, but at least consistent), and assigned the role variable at role invocation:
- name: Common apache configuration
ansible.builtin.include_role:
name: mw_common_apache
vars:
mw_common_apache_version: "{{ mw_itweb_common_config.apache }}"
mw_common_apache_server_doc_root: "{{ mw_itweb_common_config.docroot }}"
mw_common_apache_instance: "{{ mw_itweb_apache_instance }}"
mw_common_apache_service_type: "{{ mw_itweb_common_config.type | d('init') }}"
mw_common_apache_port: "{{ mw_itweb_common_config.port }}"
mw_common_apache_timeout: '120'
mw_common_apache_shibboleth: true
mw_common_apache_config_notify: "apache_config_change_{{ mw_itweb_common_config.type }}"
mw_common_apache_module_override: "{{ mw_itweb_apache_module_override }}"
mw_common_apache_mpm_worker_maxrequestworkers: "{{ mw_itweb_mpm_worker_maxrequestworkers | d(1000) }}"
We bend these guidelines when relevant data comes from multiple places – group_vars, host_vars, role defaults – and get combined by name. For example, our splunk_forwarder role contains the default variable splunk_forwarder_inputs_conf_defaults, but various group_vars or host_vars may exist for the current play host that should be combined with. They would have names like splunk_forwarder_inputs_conf_fwhostd0p or splunk_forwarder_inputs_conf_fwgrp4. A template task within the splunk_forwarder role combines these variables in its vars: section as shown below:
- name: Populate splunk inputs.conf
become: true
become_user: splunk
ansible.builtin.template:
src: '{{ splunk_forwarder_inputsconf }}'
dest: '{{ splunk_forwarder_homedir }}/etc/system/local/inputs.conf'
owner: splunk
group: splunk
mode: 0644
notify: restart splunkforwarder
vars:
# 'conf_var_names' contains a list of all the variables starting with
# "splunk_forwarder_inputs_conf_" except "…defaults".
conf_var_names: "{{ [ [lookup('ansible.builtin.varnames',
'^splunk_forwarder_inputs_conf_(?!defaults$).*', default='')]
] | flatten
| map('split', ',')
| flatten }}"
# Combine the 'splunk_forwarder_inputs_conf_defaults' role default
# and any host- or group-specific inputs_conf variables that might
# apply to the current play host.
conf_var_values: "{{ splunk_forwarder_inputs_conf_defaults
| combine(lookup('ansible.builtin.vars', *conf_var_names)) }}"
(There’s also the community.general.merge_variables lookup that could be used if your variable names let you get the defaults combined first. We did it this way because - say it with me - “We’ve always done it this way.”)