Describe the purpose of an Ansible Role, its structure, and how roles are used in playbooks.
As you develop more playbooks, you are likely to discover that you have many opportunities to reuse code from playbooks that you wrote previously.
For example, you might create a play to configure a network managed node, then reuse that same play to configure other managed nodes with different configuration settings. That play might be long and complex, with many included or imported files and tasks to manage various situations. Copying all that code into another playbook might take a lot of time.
Ansible Roles make it easy to reuse Ansible code. You can package all the tasks, variables, files, templates, and other resources into a standardized directory structure. You can copy a role from project to project by copying the role directory and subdirectories, and then calling that role within a play.
A well-written role can take variables passed from the playbook. These variables can adjust the behavior of the role by setting hostnames, IP addresses, usernames, or other specific details. You can also ensure that reasonable default values are set for the variables in the role.
Ansible Roles provide the following benefits:
Roles group content together, enabling easy sharing of code with others.
Roles make larger projects more manageable.
Roles can be developed in parallel by different users.
In addition to writing, using, reusing, and sharing your own roles, you can obtain roles from sources such as the following:
Distribution packages, such as Ansible Content Collections
The hosted automation hub (https://console.redhat.com/ansible/automation-hub)
A private automation hub
The community Ansible Galaxy website
An Ansible Role is defined by a standardized structure of subdirectories and files.
The top level directory defines the name of the role itself.
Files are organized into subdirectories that are named according to each file's purpose in the role, such as tasks.
The following tree command displays the directory structure of the user.example role:
[user@host roles]$ tree user.example
user.example/
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.ymlTable 6.1. Ansible Role Subdirectories
| Subdirectory | Function |
|---|---|
defaults
| The main.yml file in this directory contains the default values of role variables that can be overwritten when the role is used. These variables have low precedence and are intended to be changed and customized in plays. |
files
| This directory contains static files that are referenced by role tasks. |
handlers
| The main.yml file in this directory contains the role handler definitions.
Handlers are not covered in this course. |
meta
| The main.yml file in this directory contains information about the role, including author, license, platforms, and optional role dependencies. |
tasks
| The main.yml file in this directory contains the role task definitions. |
templates
| This directory contains Jinja2 templates that are referenced by role tasks. |
tests
| This directory can contain an inventory and test.yml playbook that can be used to test the role. |
vars
| The main.yml file in this directory defines the role variable values.
These variables are used for internal purposes within the role.
These variables have high precedence and are not intended to be changed when used in a playbook. |
Not every role has all the directories in the preceding table.
Role variables are defined by creating a vars/main.yml file with key-value pairs.
These variables are referenced in role task files like any other variable: {{ .
These variables have a high precedence and cannot be overridden by inventory variables.
These variables are used within the role itself, not by playbooks that reference the role.VAR_NAME }}
Default variables enable you to set default values for variables that can be used in a play within the role.
These variables are defined by creating a defaults/main.yml file with key-value pairs in the role directory hierarchy.
Default variable values can be overridden by any other variable, including inventory variables. These variables are intended to provide you with a way to customize how the role functions.
Define a specific variable in either vars/main.yml or defaults/main.yml, but not in both places.
Use default variables in defaults/main.yml when you intend that the variable values might be overridden.
Variables defined in vars/main.yml override variables defined in defaults/main.yml.
Roles should not contain any hard-coded data, secrets, passwords, private keys, or similar information. Roles should be generic, reusable, and freely shareable.
There are several ways to call roles in a play. The two primary methods are:
Including or importing them in your tasks list
Creating a roles list that runs specific roles before your play's tasks
The first method is newer and offers more flexibility, but the second method is still commonly used.
Use the ansible.builtin.import_role module in a task to statically import a role.
Use the ansible.builtin.include_role module in a task to dynamically include a role.
The following play demonstrates how you can import a role by using a task with the ansible.builtin.import_role module.
The example play runs the task called A normal task first, and then imports the role2 role:
- name: Run a role as a task
hosts: ios
tasks:
- name: A normal task
ansible.builtin.debug:
msg: 'first task'
- name: A task to import role2 here
ansible.builtin.import_role:
name: role2With the ansible.builtin.import_role module, Ansible treats the role as a static import and parses it during initial playbook processing.
In the preceding example, when the playbook is parsed, Ansible performs the following tasks:
If roles/role2/tasks/main.yml exists, Ansible adds the tasks in that file to the play.
If roles/role2/defaults/main.yml exists, Ansible adds the default variables in that file to the play.
If roles/role2/vars/main.yml exists, Ansible adds the variables in that file to the play (possibly overriding values from role default variables due to precedence).
Because ansible.builtin.import_role is processed when the playbook is parsed, the variables in the role's default and vars directories are exposed to all the tasks and roles in the play, and can be accessed by tasks and roles that precede it in the play (even though the role has not run yet).
You can also set variables for the role when you call the task, in the same way that you can set task variables:
- name: Run a role as a task
hosts: ios
tasks:
- name: A task to include role2 here
ansible.builtin.import_role:
name: role2
vars:
var1: val1
var2: val2The ansible.builtin.include_role module works in a similar way, but it dynamically includes the role when the playbook is running instead of statically importing it when the playbook is initially parsed.
One key difference between the two modules is how they handle task-level keywords, conditionals, and loops:
ansible.builtin.import_role applies task conditionals and loops to each of the tasks being imported.
ansible.builtin.include_role applies task conditionals and loops to the statement that determines whether the role is included or not.
In addition, when you include a role, its role variables and default variables are not exposed to the rest of the play, unlike when you import a role.
Another way you can call roles in a play is to list them in a roles section.
The roles section is similar to the tasks section, except instead of having a list of tasks, it has a list of roles.
In the following example play, the role1 role runs first, followed by the role2 role:
---
- name: A play that only has roles
hosts: ios
roles:
- role: role1
- role: role2For each specified role, the role's tasks, variables, and dependencies are imported into the play in the order in which they are listed.
When you use a roles section to import roles into a play, the roles run before any tasks that you define for that play.
Roles are run before tasks whether the roles section is listed before or after the tasks section.
- name: Roles always run first
hosts: ios
tasks:
- name: A task
ansible.builtin.debug:
msg: "This task runs after the role."
roles:
- role: role1Because roles run first, it generally makes sense to list the roles section before the tasks section, if you must have both.
A tasks section is not required in a play, especially if you have a roles section.
In fact, it is generally a good practice to avoid using both roles and tasks sections in a play at the same time to avoid confusion about the order in which roles and tasks run.
If you must have a tasks section and a roles section, it is better to create tasks that use ansible.builtin.import_roles and ansible.builtin.include_roles to run at the correct points in the play's execution.
The following example sets values for the var1 and var2 variables, which are associated with the role2 role.
If the var1 and var2 variables are defined in the defaults or vars directories for the role2 role, then the variables are overridden when the role2 role is used.
---
- name: A play that runs the second role with variables
hosts: ios
roles:
- role: role1
- role: role2
var1: val1
var2: val2Another equivalent playbook syntax that you might see in this case is as follows:
---
- name: A play that runs the second role with variables
hosts: ios
roles:
- role: role1
- { role: role2, var1: val1, var2: val2 }This can be harder to read in some situations, even though it is more compact.
Ansible looks for duplicate role lines in the roles section.
If two roles are listed with exactly the same parameters, the role only runs once.
For example, the following roles section only runs role1 once:
roles:
- { role: role1, service: "httpd" }
- { role: role2, var1: true }
- { role: role1, service: "httpd" }To run the same role a second time, it must have different parameters defined:
roles:
- { role: role1, service: "httpd" }
- { role: role2, var1: true }
- { role: role1, service: "syslogd" }Two special task sections, pre_tasks and post_tasks, are occasionally used with roles sections.
The pre_tasks section is a list of tasks, similar to tasks, but these tasks run before any of the roles in the roles section.
Plays also support a post_tasks keyword.
These tasks run after the play's tasks.
The following play shows an example with pre_tasks, roles, tasks, and post_tasks.
It is unusual that a play would contain all four sections.
- name: Play to illustrate order of execution
hosts: ios
pre_tasks:
- name: This task runs first
ansible.builtin.debug:
msg: This task is in pre_tasks
notify: my handler
changed_when: true
roles:
- role: role1
tasks:
- name: This task runs after the roles
ansible.builtin.debug:
msg: This task is in tasks
notify: my handler
changed_when: true
post_tasks:
- name: This task runs last
ansible.builtin.debug:
msg: This task is in post_tasks
notify: my handler
changed_when: trueIn general, if you think you need pre_tasks and post_tasks sections in your play because you are using roles, consider importing the roles as tasks and including only a tasks section.
Alternatively, it might be simpler to have multiple plays in your playbook.