Authors – All

If you have a few years of experience in the DevOps ecosystem, and you're interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Introduction

While Ansible is helpful for automation and configuration management, its terminologies can get a bit confusing. The differences between interconnected concepts such as tasks, roles, plays, and playbooks easily get blurred. But considering their importance in Ansible, understanding what each one represents is vital.

In this tutorial, we compare tasks, roles, plays, and playbooks in Ansible, highlighting their differences and basic syntaxes.

2. Ansible Task

An Ansible task is basically the action-defining unit of a playbook. Each task in a playbook runs a code unit that configures resources in a certain way or executes a specific command on a managed node. These code units are called modules.

When defining a task, we typically pass arguments to the parameters of a module, particularly the required parameters. These arguments allow for specific module operation. For instance, if we wanted to install a package on a managed node, we could create a task using the ansible.builtin.package module:

- name: Install Apache
  ansible.builtin.package:
    name: apache2
    state: present

In the snippet above, because the state parameter gets present as its argument, the task installs apache2 – the name parameter’s argument.

We can also write a task where ansible.builtin.package ensures a package doesn’t exist on a managed node:

- name: Remove MariaDB
  ansible.builtin.package:
    name: mariadb-server
    state: absent

In this case, the task removes mariadb-server since the state parameter’s argument says absent.

As seen in the snippets above, tasks present as a list of dictionaries. However, the parameters of their modules may take on complex data structures such as dictionaries with lists as values.

3. Ansible Play

At its simplest, a play is a group of ordered tasks executed collectively. It’s the basic execution unit of a playbook as it maps a set of tasks to managed nodes for execution in a predefined order.

To map tasks to managed nodes, plays require the hosts parameter with a valid argument. Depending on the argument, the playbook may or may not need a host inventory. A host inventory is a grouped or ungrouped list of servers on which Ansible executes plays.

When hosts gets all*, or a pattern, we must pass a host inventory to the playbook. However, if we pass an FQDN or IP address directly to the hosts parameter, the playbook may not need a host inventory. Of course, when working with a large fleet of managed nodes, creating a host inventory would be more practical. Specifying FQDNs or IP addresses for so many servers in the playbook will surely leave things disorganized.

Let’s define a play with the two tasks from the previous section:

- hosts: all
  tasks:
    - name: Install Apache
      ansible.builtin.package:
        name: apache2
        state: present

    - name: Remove MariaDB
      ansible.builtin.package:
        name: mariadb-server
        state: absent

The play above will install apache2 on and remove mariadb-server from all the nodes in the provided host inventory. In addition, since plays execute tasks in their order of definition, the play will install apache2 before removing mariadb-server.

Apart from tasks and hosts, a play may also contain handlers, variables, and a reference to roles.

4. Ansible Role

Ansible roles are reusable collections of tasks, files, templates, variables, handlers, plugins, and so on. They promote modularization, which in turn encourages reusability while ensuring tidiness.

Unlike plays and tasks, roles don’t appear directly in a playbook. Instead, they come as a directory with at least one of seven subdirectories.

Using tree, let’s print the directory structure of a role with all seven standard subdirectories:

$ tree
.
└── role_1
    ├── README.md
    ├── defaults
    │   └── main.yml
    ├── files
    ├── handlers
    │   └── main.yml
    ├── meta
    │   └── main.yml
    ├── tasks
    │   └── main.yml
    ├── templates
    └── vars
        └── main.yml

9 directories, 6 files

Roles follow a specific directory structure, so sticking with said structure when using them is crucial.

4.1. Importing Roles Into a Play

As mentioned earlier, roles don’t appear directly in a playbook. Instead, we can import them in plays, in tasks, and as dependencies to other roles. Let’s import the role above into the play from the previous section:

- hosts: all
  roles:
    - baeldung_roles/role_1
  tasks:
    - name: Install Apache
      ansible.builtin.package:
        name: apache2
        state: present

    - name: Remove MariaDB
      ansible.builtin.package:
        name: mariadb-server
        state: absent

The roles parameter in the play definition takes a list whose values are relative paths of the roles in the working directory. In the above, we specify our role as baeldung_roles/role_1. This means role_1 is a subdirectory of baeldung_roles, which, in turn, is a subdirectory of the working directory.

If role_1 were a subdirectory of the working directory, our play would’ve been different:

- hosts: all
  roles:
    - role_1
  tasks:
...truncated...

4.2. Importing Roles Into a Task

Tasks use roles when we specify one of two keywords: import_role or include_role. The include_role keyword allows tasks to use roles dynamically:

- name: Include role_1
  include_role:
    name: role_1

In other words, Ansible only parses roles specified by include_role when it reaches the specific task.

Unlike include_roleimport_role lets tasks use roles statically:

- name: Import role_1
  import_role:
    name: role_1

This is possible because Ansible parses roles passed to import_role at the beginning of the playbook execution.

4.3. Using Role Dependencies

To use a role as a dependency of another role, we’ll specify the role under the dependencies keyword in the meta/main.yml file of the dependent role:

$ cat baeldung_roles/role_1/meta/main.yml
dependencies:
  - role: role_2

Above, we added a role named role_2 as a dependency of role_1.

5. Ansible Playbook

Playbooks are YAML files containing one or more plays. They form the basis of the execution of the ansible-playbook command and sit at the highest level in the Ansible automation structure.

Let’s create a playbook with two plays, with each one referencing different managed node groups:

$ cat playbook.yml
- hosts: databases
  tasks:
    - name: Remove MariaDB
      ansible.builtin.package:
        name: mariadb-server
        state: absent

- hosts: webservers
  tasks:
    - name: Install Apache
      ansible.builtin.package:
        name: apache2
        state: present

The first play in the playbook above will execute a task that removes mariadb-server from hosts under the databases group in the host inventory. Then, the second play will install apache2 in the hosts under the webservers group.

6. Conclusion

In this article, we examined the difference between a task, role, play, and playbook in Ansible. A playbook consists of multiple plays and is the file passed to the ansible-playbook command. A play, on the other hand, contains a group of tasks mapped to a specific server or group of servers.

Tasks define actions for execution on managed nodes by calling modules. However, roles are modularized, reusable collections of Ansible resources.

Authors – All

If you have a few years of experience in the DevOps ecosystem, and you're interested in sharing that experience with the community, have a look at our Contribution Guidelines.