Ansible Automation on Ubuntu 22.04 – Roles

In previous posts, we’ve seen how to install ansible, set up inventory, create a basic playbook and use templates. Today we’ll look at roles.

Ansible roles are a way to reuse ansible code in other playbooks, so it’s a slightly advanced topic. It’s quite similar to a function in programming, or a perhaps a library. It’s just a way to avoid repeating yourself or copy/pasting code, which is almost never a good idea. You define a role in a separate directory in a YAML file. We’ll use two playbooks to show how to reuse a role in each one.

Topology

In the spirit of reusing stuff, I’m reusing the same topology from a previous post. We have a single subnet of 10.0.0.0/24, with the ansible controller sitting at 10.0.0.1 and two managed nodes at 10.0.0.2 and 10.0.0.3.

Inventory

If you’re unsure how to create inventory, check the post I created (link at the top) on doing that. You don’t have to put ansible files in any particular place, as long as you specify the inventory file and playbook on the CLI.

I have created a hosts file in the home directory on the ansible controller. I’ve already set up SSH key authentication, so no need for the user password. The hosts file looks like this:

servers:
  hosts:
    ubuntu1:
      ansible_host: 10.0.0.2
      ansible_user: james
    ubuntu2:
      ansible_host: 10.0.0.3
      ansible_user: james
  vars:
    ansible_python_interpreter: /usr/bin/python3

Create a role

To create a role, we need to follow some rules that ansible has laid out for the directory and naming structure. First create a roles directory in the main ansible directory. Inside that, create another folder that can be named anything. The name of this second folder will be the name of the role itself. I’ll name mine rolesarecool.

mkdir -p roles/rolesarecool/tasks
touch roles/rolesarecool/tasks/main.yml

Inside rolesarecool, create yet another directory called tasks. Inside tasks create main.yml. We’ll put our task(s) in this YAML file. This role is just for demonstration purposes, so it contains a single task and creates a file in the home directory called rolesarecool.txt.

- name: Create rolesarecool.txt
  copy:
    dest: ./{{ ansible_play_name }}_rolesarecool.txt
    content: |
      {{ ansible_play_name  }} created this. Roles are cool, right?

You need to follow the directory structure roles/<your_role_name>/tasks/main.yml in order for this to work, as ansible will look specifically for it to be set up this way.

You might have noticed the task is not part of a larger play, i.e. there’s no hosts or gather_facts or tasks list. You just list out your tasks in your role, and they’ll be incorporated into the playbook/play that is using the role (you can have multiple plays in a playbook, but many have just one play).

In addition, I’ve used the built-in ansible variable {{ ansible_play_name }} which will originate from the playbook that is calling this role. This variable’s value comes from the name field, specified at the top of the play. We’ll see it in the next section. This way the role will create a unique text file and file name for each time it’s called.

Create two different playbooks

Now we will create two playbooks, each writing their own text file and also pulling in the role we wrote rolesarecool.

Back in the main ansible directory (where the hosts file is, in my case the is the user’s home directory) we’ll write the first playbook in playbook-a.yml. Mine looks like this:

---
- hosts: all
  name: super_duper_play_A
  gather_facts: false
  tasks:
    - ansible.builtin.include_role:
        name: rolesarecool

    - name: Create playbook-a.txt
      copy:
        dest: ./playbook-a.txt
        content: |
          playbook-a created this text file!

Notice the name field (super_duper_play_A), which will be used in the text file name when the role rolesarecool runs.

Then we’ll create the second playbook, playbook-b.yml. Mine looks like this:

---
- hosts: all
  name: super_duper_play_B
  gather_facts: false
  tasks:
    - ansible.builtin.include_role:
        name: rolesarecool

    - name: Create playbook-b.txt
      copy:
        dest: ./playbook-b.txt
        content: |
          playbook-b created this text file!

Same thing as the first playbook except I substituted “A” for “B” in various places. After running these two playbooks, a total of 4 text files should be created on both managed Ubuntu nodes:

  • playbook-a.txt
  • playbook-b.txt
  • super_duper_play_A.txt
  • super_duper_play_B.txt

The first two are created by each playbook, the second two are created by the role when it’s included by each playbook.

Last thing – there are several different ways to pull in role code to a playbook, here we’re using include_role. Each method has its own nuances, so be sure to check the official ansible documentation for further detail.

Run the playbooks

Here we go, let’s run both playbook-a.yml and playbook-b.yml:

james@james:~$ ansible-playbook -i hosts playbook-a.yml 
----

PLAY [super_duper_play_A] ******************************************************

TASK [ansible.builtin.include_role : rolesarecool] *****************************

TASK [rolesarecool : Create rolesarecool.txt] **********************************
changed: [ubuntu2]
changed: [ubuntu1]

TASK [Create playbook-a.txt] ***************************************************
ok: [ubuntu1]
ok: [ubuntu2]

PLAY RECAP *********************************************************************
ubuntu1                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ubuntu2                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
ansible-playbook -i hosts playbook-b.yml 
----

PLAY [super_duper_play_B] ******************************************************

TASK [ansible.builtin.include_role : rolesarecool] *****************************

TASK [rolesarecool : Create rolesarecool.txt] **********************************
changed: [ubuntu1]
changed: [ubuntu2]

TASK [Create playbook-b.txt] ***************************************************
ok: [ubuntu2]
ok: [ubuntu1]

PLAY RECAP *********************************************************************
ubuntu1                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ubuntu2                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Looks like everything went through ok!

Verification

Let’s make sure the playbooks and role did what we expected. On the first managed node, let check the home directory:

ls -l
----

total 16
-rw-rw-r-- 1 james james 56 Nov  9 03:30 playbook-a.txt
-rw-rw-r-- 1 james james 35 Nov  9 03:16 playbook-b.txt
-rw-rw-r-- 1 james james 56 Nov  9 03:33 super_duper_play_A_rolesarecool.txt
-rw-rw-r-- 1 james james 56 Nov  9 03:33 super_duper_play_B_rolesarecool.txt

Let’s check their contents:

cat *.txt
----

playbook-a created this text file in its own task list!
playbook-b created this text file!
super_duper_play_A created this. Roles are cool, right?
super_duper_play_B created this. Roles are cool, right?

Looks good! Checking the second managed node should produce similar results.

Happy automating!