Bookmark this page

Chapter 7. Simplifying Playbooks with Roles and Ansible Content Collections

Abstract

Goal Use Ansible Roles and Ansible Content Collections to develop playbooks more quickly and to reuse Ansible code.
Objectives
  • Describe the purpose of an Ansible Role, its structure, and how roles are used in playbooks.

  • Create a role in a playbook’s project directory and run it as part of one of the plays in the playbook.

  • Select and retrieve roles from external sources such as Git repositories or Ansible Galaxy, and use them in your playbooks.

  • Obtain a set of related roles, supplementary modules, and other content from an Ansible Content Collection and use them in a playbook.

  • Write playbooks that take advantage of system roles for Red Hat Enterprise Linux to perform standard operations.

Sections
  • Describing Role Structure (and Quiz)

  • Creating Roles (and Guided Exercise)

  • Deploying Roles from External Content Sources (and Guided Exercise)

  • Getting Roles and Modules from Content Collections (and Guided Exercise)

  • Reusing Content with System Roles (and Guided Exercise)

Lab
  • Simplifying Playbooks with Roles and Ansible Content Collections

Describing 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. Perhaps, you could repurpose a play to configure a MySQL database for one application to configure a MySQL database for another application, with different hostnames, passwords, and users.

That play might be long and complex, with many included or imported files and tasks and handlers to manage various situations. Copying all that code into another playbook might be nontrivial work.

Ansible roles make it easier to reuse Ansible code generically. You can package all the tasks, variables, files, templates, and other resources needed to provision infrastructure or deploy applications in a standardized directory structure. Copy a role from project to project by copying the directory, then call the role within a play.

A well-written role can take variables passed from the playbook. These variables can adjust the behavior of the role, setting all the site-specific hostnames, IP addresses, usernames, secrets, or other locally-specific details.

For example, you might write a role to deploy a database server to support variables that set the hostname, database admin user and password, and other parameters that are customized for your installation.

You also can ensure that reasonable default values are set for those variables in the role, if they are not set in the play that calls the role.

Ansible roles have the following benefits:

  • Roles group content together, enabling easy sharing of code with others.

  • Roles can define the essential elements of a system type, such as a web server, database server, or Git repository.

  • 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 other sources. You can find roles by using distribution packages, such as Ansible Content Collections. Or, you can download roles from the Red Hat automation hub, a private automation hub, and from the community's Ansible Galaxy website.

Red Hat Enterprise Linux includes some roles in the rhel-system-roles package. You learn more about rhel-system-roles later in this chapter.

Examining 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 and handlers.

The files and templates subdirectories contain files referenced by tasks in other playbooks and task files.

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 7.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's handler definitions.
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's 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's variable values. Often 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 of these directories.

Defining Variables and Defaults

Role variables are defined by creating a vars/main.yml file with key-value pairs in the role directory hierarchy. These variables are referenced in role task files like any other variable: {{ VAR_NAME }}. These variables have a high precedence and can not be overridden by inventory variables. These variables are used by the internal functioning of the role.

Default variables enable you to set default values for variables that can be used in a play to configure the role or customize its behavior. These variables are defined by creating a defaults/main.yml file with key-value pairs in the role directory hierarchy. Default variables have the lowest precedence of any available variables.

Default variable values can be overridden by any other variable, including inventory variables. These variables are intended to provide the person writing a play that uses the role with a way to customize or control exactly what it is going to do. You can use default variables to provide information to the role that it needs to configure or deploy something properly.

Define a specific variable in either vars/main.yml or defaults/main.yml, but not in both places. Use default variables when you intend that the variable values might be overridden.

Important

Roles should not have site specific data in them or contain any secrets like passwords or private keys because roles are supposed to be generic, reusable, and freely shareable. Therefore, site specific details should not be hard coded into them.

Secrets should be provided to the role through other means. This requirement is one reason that you might want to set role variables when calling a role. Role variables set in the play could provide the secret, or point to an Ansible Vault encrypted file containing the secret.

Using Ansible Roles in a Play

There are several ways to call roles in a play. The two primary methods are:

  • You can include or import them like a task in your tasks list.

  • You can create a roles list that runs specific roles before your play's tasks.

The first method is the most flexible, but the second method is also commonly used and was invented before the first method.

Including and Importing Roles as Tasks

Roles can be added to a play by using an ordinary task. Use the ansible.builtin.import_role module to statically import a role, and the ansible.builtin.include_role module 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 A normal task first, then imports the role2 role.

- name: Run a role as a task
  hosts: remote.example.com
  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:

  • If roles/role2/tasks/main.yml exists, Ansible adds the tasks in that file to the play.

  • If roles/role2/handlers/main.yml exists, Ansible adds the handlers 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 role's handlers, default variables, and role variables are all 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: remote.example.com
  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 the task's conditionals and loops to each of the tasks being imported.

  • ansible.builtin.include_role applies the task's 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 ansible.builtin.import_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 very similar to the tasks section, except instead of consisting of a list of tasks, it consists of a list of roles.

In the following example play, the role1 role runs, then the role2 role runs.

---
- name: A play that only has roles
  hosts: remote.example.com
  roles:
    - role: role1
    - role: role2

For each role specified, the role's tasks, handlers, 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 first, before any tasks that you define for that play. Whether the roles section is listed before or after the tasks section in the play does not matter.

- name: Roles always run first
  hosts: remote.example.com
  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. The preceding play can be rewritten as follows without changing how it runs:

    - name: A task.
      ansible.builtin.debug:
        msg: "This task runs after the role."

Important

A tasks section in a play is not required. In fact, it is generally a good practice to avoid both roles and tasks sections in a play to avoid confusion about the order in which roles and tasks run.

If you must have a tasks section and roles, 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 two role variables of role2, var1 and var2. Any defaults and vars variables are overridden when role2 is used.

---
- name: A play that runs the second role with variables
  hosts: remote.example.com
  roles:
    - role: role1
    - role: role2
      var1: val1
      var2: val2

Another equivalent playbook syntax that you might see in this case is:

---
- name: A play that runs the second role with variables
  hosts: remote.example.com
  roles:
    - role: role1
    - { role: role2, var1: val1, var2: val2 }

There are situations in which this can be harder to read, 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 one time:

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

Special Tasks Sections

There are two special task sections, pre_tasks and post_tasks, that 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. If any task in the pre-tasks section notify a handler, then those handler tasks run before the roles or normal tasks.

Plays also support a post_tasks keyword. These tasks run after the play's tasks and any handlers notified by the play's tasks.

The following play shows an example with pre_tasks, roles, tasks, post_tasks and handlers. It is unusual that a play would contain all of these sections.

- name: Play to illustrate order of execution
  hosts: remote.example.com
  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
  handlers:
    - name: my handler
      ansible.builtin.debug:
        msg: Running my handler

In the preceding example, an ansible.builtin.debug task runs in each tasks section and in the role in the roles section. Each of those tasks notifies the my handler handler, which means the my handler task runs three times:

  • After all the pre_tasks tasks run

  • After all the roles tasks and tasks tasks run

  • After all the post_tasks run

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: rh294-9.0-c95c7de