Ansible 101: #014 - Roles

Hello everyone.

We are slowly approaching the end of the "Ansible 101", the basics of Ansible. However, we still have one big topic to cover: Roles.

So far, we have been writing and running our Ansible code in playbooks. Now, if we want to write Ansible code that we want to use across projects - or even publish, for example - we quickly reach the limits of playbooks. This is where Ansible roles come in. They allow us to create reusable and parameterizable Ansible content. We can then integrate one - or more - playbooks into a role.

The structure of an Ansible role is represented by a directory structure. A typical role looks like this:

# playbooks
webservers.yml
roles/
    my-webserver-role/
        tasks/
          - main.yml
        handlers/
          - main.yml
        files/
        templates/
        vars/
          - main.yml
        defaults/
          - main.yml
        meta/
          - main.yml

In this example we see a role called "my-webserver-role". Let me briefly explain each subdirectory:

  • tasks - Here are files where we define our tasks
  • handlers - Here are handlers defined if our role needs them
  • files - This is where we put files we want to use with the copy module, for example
  • templates - If we need Jinja2 templates, we put them here
  • vars - This is where we store role-internal variable definitions
  • defaults - Here we can define default values for variables, which can be overridden by the "role user"
  • mta - This is where we store meta information about the role itself

An example of a more detailed web server role can be found here:

https://github.com/dermow/ansible-role-httpd

For our guide, however, I want to make the example a little simpler. Let's define the following tasks for the role:

Support limited to Ubuntu
Install the Apache web server
Providing a custom index.html
Starting and enabling the Apache service

First, it is important to note that by default, Ansible searches for roles in defined locations. These include "./roles" in your project directory. Let's start by creating the directory structure:

mkdir ~/ansible-guide-roles
cd ~/ansible-guide-roles
mkdir -p ./roles/my-webserver-role
cd ./roles/my-webserver-role
mkdir tasks templates handlers vars defaults

What's missing is an inventory - I'll use our test environment again. Of course, being the smart guy that I am, I took a snapshot beforehand and didn't have to reinstall it (chrm chrm).

[webservers]
ansible-guide-1 ansible_ssh_user=ansible ansible_host=192.168.0.11
ansible-guide-2 ansible_ssh_user=ansible ansible_host=192.168.0.12

~/ansible-guide-roles/inventory.txt

The next step is to create a file for our variable definitions:

---
packages_to_install:
  - apache2
  - libapache2-mod-php

~/ansible-guide-roles/roles/my-webserver-role/vars/main.yml

We have defined a list in which we specify which packages we want to install later. The next step is to create the corresponding task:

---
- name: install packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: "{{ packages_to_install }}"

~/ansible-guide-roles/roles/my-webserver-role/tasks/main.yml

Now we have a first version of the role that we can test. To do this, we will create a playbook with the following code:

---
- hosts: webservers
  roles:
    - my-webserver-role

~/ansible-guide-roles/playbook.yml

Lets start the playbook:

ansible-playbook -i inventory.txt playbook.yml
PLAY [webservers] ********************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]

TASK [my-webserver-role : install packages] ********************************************************************************************************************************************************
changed: [ansible-guide-1] => (item=apache2)
changed: [ansible-guide-1] => (item=libapache2-mod-php)
changed: [ansible-guide-2] => (item=apache2)
changed: [ansible-guide-2] => (item=libapache2-mod-php)

PLAY RECAP ********************************************************************************************************************************************************
ansible-guide-1                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 
ansible-guide-2                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Great, it worked: The Apache web server and the PHP module should now be installed on both target machines. The next step is to make sure that the web server service is started and running:

---
- name: install packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: "{{ packages_to_install }}"
  
- name: start and enable apache
  become: true
  ansible.builtin.systemd:
    name: apache2
    state: started
    enabled: true
    

~/ansible-guide-roles/roles/my-webserver-role/tasks/main.yml

Now we want to ship our own config with the role. To do this, I simply stole the "default.conf" from the default installation and removed the comments:

cat /etc/apache2/sites-available/000-default.conf | grep -v "#"
<VirtualHost *:80>

	ServerAdmin webmaster@localhost
	DocumentRoot /var/www/html


	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

Let's say we only want the "ServerAdmin" to be configurable. So we create the appropriate template:

<VirtualHost *:80>

	ServerAdmin {{ my_server_admin }}
	DocumentRoot /var/www/html


	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

Now we just have to define the variable "my_server_admin". Since this should be overridden by the "user", it makes sense to do this under "defaults":

my_server_admin: [email protected]

~/ansible-guide-roles/roles/my-webserver-role/defaults/main.yml

Now all you need are the tasks to deploy the template:

---
- name: install packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: "{{ packages_to_install }}"
  
- name: start and enable apache
  become: true
  ansible.builtin.systemd:
    name: apache2
    state: started
    enabled: true
    
- name: template web config
  become: true
  ansible.builtin.template:
    src: 000-default.conf
    dest: /etc/apache2/sites-available/000-default.conf
    

Now, of course, it would be great if the web server could be restarted automatically when changes are made to the configuration. We already discussed a suitable tool for this in Part 4: Handler.

So we create a handler...

---
- name: restart-apache
  become: true
  ansible.builtin.systemd:
    name: "apache2"
    state: restarted
  

~/ansible-guide-roles/roles/my-webserver-role/handlers/main.yml

... And adapt our mission accordingly:

---
- name: install packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: "{{ packages_to_install }}"
  
- name: start and enable apache
  become: true
  ansible.builtin.systemd:
    name: apache2
    state: started
    enabled: true
    
- name: template web config
  become: true
  ansible.builtin.template:
    src: 000-default.conf
    dest: /etc/apache2/sites-available/000-default.conf
  notify: restart-apache # <---- HANDLER
    

~/ansible-guide-roles/roles/my-webserver-role/tasks/main.yml

And go:

ansible-playbook -i inventory.txt playbook.yml
PLAY [webservers] ********************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]

TASK [my-webserver-role : install packages] ********************************************************************************************************************************************************
ok: [ansible-guide-1] => (item=apache2)
ok: [ansible-guide-1] => (item=libapache2-mod-php)
ok: [ansible-guide-2] => (item=apache2)
ok: [ansible-guide-2] => (item=libapache2-mod-php)

TASK [my-webserver-role : start and enable apache] ********************************************************************************************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]

TASK [my-webserver-role : template web config] ********************************************************************************************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]

RUNNING HANDLER [my-webserver-role : restart-apache] ********************************************************************************************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]

PLAY RECAP ********************************************************************************************************************************************************
ansible-guide-1                  : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ansible-guide-2                  : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

That looks good :) All that remains is the last part of our to-do list: Provide our own index.html. With what we've learned so far, this shouldn't be a problem: we simply create another template:

<html>
  <head>
    <title>{{ my_website_title }}</title>
  </head>
  <body>
    {{ my_website_content }}
  </body>
</html>

So we created another template in which we want to use two new variables. We also create default values for them:

my_server_admin: [email protected]
my_website_title: "Ansible 101"
my_website_content: "Hello World"

~/ansible-guide-roles/roles/my-webserver-role/defaults/main.yml

Finally, the task to deliver the index.html:

---
- name: install packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: "{{ packages_to_install }}"
  
- name: start and enable apache
  become: true
  ansible.builtin.systemd:
    name: apache2
    state: started
    enabled: true
    
- name: template web config
  become: true
  ansible.builtin.template:
    src: 000-default.conf
    dest: /etc/apache2/sites-available/000-default.conf
  notify: restart-apache # <---- HANDLER
  
- name: template index.html
  become: true
  ansible.builtin.template:
    src: index.html
    dest: /var/www/html/index.html
    

~/ansible-guide-roles/roles/my-webserver-role/tasks/main.yml

But let's say we want to use our own value for our title instead of the default from the role:

ansible-playbook -i inventory.txt -e "my_website_title='My fancy title'" playbook.yml
 PLAY [webservers] ********************************************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]

TASK [my-webserver-role : install packages] ********************************************************************************************************************************************************
ok: [ansible-guide-1] => (item=apache2)
ok: [ansible-guide-1] => (item=libapache2-mod-php)
ok: [ansible-guide-2] => (item=apache2)
ok: [ansible-guide-2] => (item=libapache2-mod-php)

TASK [my-webserver-role : start and enable apache] ********************************************************************************************************************************************************
ok: [ansible-guide-1]
ok: [ansible-guide-2]

TASK [my-webserver-role : template web config] ********************************************************************************************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]

TASK [my-webserver-role : template index.html] ********************************************************************************************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]

RUNNING HANDLER [my-webserver-role : restart-apache] ********************************************************************************************************************************************************
changed: [ansible-guide-1]
changed: [ansible-guide-2]

PLAY RECAP ********************************************************************************************************************************************************
ansible-guide-1                  : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ansible-guide-2                  : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Done! Now a quick test to see if it all worked:

curl 192.168.0.11
 <html>
  <head>
    <title>My fancy title</title>
  </head>
  <body>
    Hello World
  </body>
</html>

Note: In a real scenario, you should define the variables you want to override in your groupvars, hostvars, or in the playbook using the role instead of setting them in the ansible command.

Pro tip

Ready made rolls are available for many applications. Take a look here:

https://galaxy.ansible.com/

THE END

This concludes the 101 Guide. You should now be able to continue learning on your own and just work with it in your own environment. I'm not sure yet if I'll start an "Advanced Guide" in the same style, or if I'll just cover certain topics as they come up.

Of course I would be very happy to get some feedback. See you soon

Mow