Bookmark this page

Ansible Role Structure

Objectives

  • Describe the purpose of an Ansible Role, its structure, and how roles are used in playbooks.

Structuring Ansible Playbooks with Roles

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

The Ansible Role Structure

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.yml

Table 6.1. Ansible Role Subdirectories

SubdirectoryFunction
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.

Defining Variables and Defaults

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: {{ VAR_NAME }}. 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.

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.

Important

Roles should not contain any hard-coded data, secrets, passwords, private keys, or similar information. Roles should be generic, reusable, and freely shareable.

Using Ansible Roles in a Play

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.

Including and Importing Roles as Tasks

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: role2

With 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).

Important

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: val2

The 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.

Using a Roles Section in a Play

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: role2

For 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: role1

Because roles run first, it generally makes sense to list the roles section before the tasks section, if you must have both.

Important

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: val2

Another 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.

Important

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" }

Special Tasks Sections

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: true

Note

In 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.

Revision: do457-2.3-7cfa22a