How to include vars and tasks in Ansible
The following is based on the latest version of Ansible 2.9 (2.9.9) as of June 15, 2020.
Intro
In the Ansible System Roles project, the role code uses include_vars
and include_task
to include vars and tasks files which depend on the platform and
version. For example, you may have a role which manages a system service,
and most of the code is platform independent, but the list of packages to
pass to the package
module, and the name of the service to manage with the
service
module, are platform specific. We use an idiom like this in our
role tasks/main.yml
:
#
- name: Set version specific variables
include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_distribution }}_{{ ansible_distribution_version }}.yml"
- "{{ ansible_distribution }}_{{ ansible_distribution_major_version }}.yml"
- "{{ ansible_distribution }}.yml"
- "{{ ansible_os_family }}.yml"
- "default.yml"
- name: Ensure role packages
package:
name: "{{ __rolename_packages }}"
state: present
- name: Ensure role services are enabled and running
service:
name: "{{ item }}"
state: started
enabled: yes
loop: "{{ __rolename_services }}"
#
This uses the Ansible first_found
lookup plugin in the form of a with_first_found
loop to load
the first matching variables file, where the order of the lookup is from most platform/version specific
to least. That is, include the file that most specifically defines the variables used
by this role for the platform and version.
The file vars/main.yml
is included by default when the role is included, and
contains platform independent variables, or variables with default values. The
files such as vars/RedHat.yml
, vars/Fedora.yml
, vars/RedHat_8.yml
, etc. contain
variable definitions specific to that platform/version. That allows use to define
variables such as __rolename_packages
and __rolename_services
to be platform
dependent.
Problems
One problem is that you may have to duplicate some definitions. For example, if you have
some definitions that are the same in both vars/CentOS_8.yml
and vars/RedHat_8.yml
,
you will have to define everything in both files. In some cases, you can symlink one
file to the other.
Another problem is when you have multiple roles that use the above idiom that include
each other, and not every role defines every platform/version specific file. What can happen is that when one role includes
another role, the lookup path ansible_search_path
will contain the path for both roles.
In this example, role_a
includes role_b
.
#
- name: in role_a tasks/main.yml
debug:
msg: in role_a tasks/main.yml
- name: show ansible_search_path before
debug:
msg: ansible_search_path before {{ ansible_search_path }}
#
At this point, ansible_search_path
will contain ['/base/roles/role_a','/base/roles/role_a/tasks']
. Now, include role_b
:
#
- name: include role_b
include_role:
name: role_b
#
Where role_b
looks like this:
#
- name: show ansible_search_path in role_b
debug:
msg: ansible_search_path role_b {{ ansible_search_path }}
#
At this point, ansible_search_path
will contain ['/base/roles/role_b','/base/roles/role_a','/base/roles/role_b/tasks']
. Note the /base/roles/role_a
in there. Now, use the variable inclusion idiom:
#
- name: Set version-specific variables for role_b
include_vars: "{{ item }}"
with_first_found:
- files:
- "{{ ansible_distribution }}_{{ ansible_distribution_version }}.yml"
- "{{ ansible_distribution }}_{{ ansible_distribution_major_version }}.yml"
- "{{ ansible_distribution }}.yml"
- "{{ ansible_os_family }}.yml"
- "default.yml"
#
If role_b
does not define vars/Fedora_31.yml
, but role_a
does, role_b
will include vars/Fedora_31.yml
from role_a
, and will not include any vars from role_b
.
Solutions
It is best to explicitly specify the path to use, rather than relying on the default behavior:
#
- name: Set version-specific variables for role_b
include_vars: "{{ item }}"
with_first_found:
- files:
- "{{ ansible_distribution }}_{{ ansible_distribution_version }}.yml"
- "{{ ansible_distribution }}_{{ ansible_distribution_major_version }}.yml"
- "{{ ansible_distribution }}.yml"
- "{{ ansible_os_family }}.yml"
- "default.yml"
paths:
- "{{ role_path }}/vars"
#
The role_path
is a built-in which will be /base/rolename
, so you are guaranteed to
look in /base/role_b/vars
for the files, and find only the files for your role.
Alternatives
with_first_found
will fail if no files are found. If you don’t want to create files
for every possible combination of platform/version, or you don’t want to create vars/default.yml
,
you can use the skip: true
flag:
#
- name: Set version-specific variables for role
include_vars: "{{ item }}"
with_first_found:
- files:
- "{{ ansible_distribution }}_{{ ansible_distribution_version }}.yml"
- "{{ ansible_distribution }}.yml"
paths:
- "{{ role_path }}/vars"
skip: true
#
In this example, I rely on vars/main.yml
for most everything, and only provide some
platform/version customizations.
Another way to do this is the following:
#
- name: Set version-specific variables for role
include_vars: "{{ item }}"
loop:
- "{{ role_path }}/vars/{{ ansible_os_family }}.yml"
- "{{ role_path }}/vars/{{ ansible_distribution }}.yml"
- "{{ role_path }}/vars/{{ ansible_distribution }}_{{ ansible_distribution_major_version }}.yml"
- "{{ role_path }}/vars/{{ ansible_distribution }}_{{ ansible_distribution_version }}.yml"
when: item is file
#
The files in the loop
are in order from least specific to most specific.
Each file in the loop
list will allow you to add or override additional
variables to specialize the values for platform and/or version. Using the
when: item is file
test means that you do not have to provide all of the
vars/
files, only the ones you need. For example, if every platform except
Fedora uses srv_name
for the service name, you can define __myrole_service:
srv_name
in vars/main.yml
then define __myrole_service: srv2_name
in
vars/Fedora.yml
. In cases where this would lead to duplicate vars files for
similiar distibutions (e.g. CentOS 7 and RHEL 7), use symlinks to avoid the
duplication.
Tasks
Platform specific tasks, however, are different. You probably want to perform
platform specific tasks once, for the most specific match. In that case, use
with_first_found
with the file list in order of most specific to least
specific, including a “default”:
#
- name: Perform platform/version specific tasks
include_tasks: "{{ item }}"
with_first_found:
- files:
- "setup_{{ ansible_distribution }}_{{ ansible_distribution_version }}.yml"
- "setup_{{ ansible_distribution }}_{{ ansible_distribution_major_version }}.yml"
- "setup_{{ ansible_distribution }}.yml"
- "setup_{{ ansible_os_family }}.yml"
- "setup_default.yml"
paths:
- "{{ role_path }}/tasks"
#
And same with the vars files above, if you don’t want to have to provide the default,
or only some files, use skip: true
:
#
- name: Perform platform/version specific tasks
include_tasks: "{{ item }}"
with_first_found:
- files:
- "setup_{{ ansible_distribution }}_{{ ansible_distribution_version }}.yml"
- "setup_{{ ansible_distribution }}.yml"
paths:
- "{{ role_path }}/tasks"
skip: true
#