Skip to main content
  1. Tutorials/

How to Use Ansible Roles to Abstract your Infrastructure Environment

Tutorials Ansible Configuration Management Ubuntu
Introduction>

Introduction #

Ansible is a configuration management tool that is designed to automate controlling servers for administrators and operations teams. With Ansible you can use a single central server to control and configure many different remote systems using SSH and Python as only requirements.
Ansible carries out tasks on servers that it manages based on task definitions. These tasks invoke built-in and community maintained Ansible modules using small snippets of YAML for each task.
As the number and variety of systems that you manage with a single Ansible control node become more complex, it makes sense to group tasks together into Ansible playbooks. Using playbooks eliminates the need to run many individual tasks on remote systems, instead letting you configure entire environments at once with a single file.
However, playbooks can become complex when they are responsible for configuring many different systems with multiple tasks for each system, so Ansible also lets you organize tasks in a directory structure called a Role. In this configuration, playbooks invoke roles instead of tasks, so you can still group tasks together and then reuse roles in other playbooks. Roles also allow you to collect templates, static files, and variables along with your tasks in one structured format.
This tutorial will explore how to create roles, and how to add templates, static files, and variables to a role. Once you are familiar with the fundamentals of building roles, we’ll use Ansible Galaxy to incorporate community contributed roles into playbooks. By the end of this tutorial you will be able to create your own environment specific roles for your servers and use them in your own playbooks to manage one, or many systems.

Prerequisites>

Prerequisites #

To follow along with this tutorial, you will need to install and configure Ansible so that you can create and run playbooks. You will also need to understand how to write Ansible playbooks.

What is an Ansible Role?>

What is an Ansible Role? #

In the prerequisite tutorials, you learned how to run the core Ansible tool using the ansible command in a terminal. You also learned how to collect tasks into playbooks and run them using the ansible-playbook command. The next step in the progression from running single commands, to tasks, to playbooks is to reorganize everything using an Ansible role.
Roles are a level of abstraction on top of tasks and playbooks that let you structure your Ansible configuration in a modular and reusable format. As you add more and more functionality and flexibility to your playbooks, they can become unwieldy and difficult to maintain. Roles allow you to break down a complex playbook into separate, smaller chunks that can be coordinated by a central entry point. For example, in this tutorial the entire playbook.yml that we will work with looks like this:
Example Playbook

---
- hosts: all
  become: true
  roles:
    - apache
  vars:
    doc_root: /var/www/example

The entire set of tasks to be carried out to configure an Apache web server will be contained in the apache role that we will create. The roll will define all the tasks that need to be completed to install Apache, instead of listing each task individually like we did in the Configuration Management 101: Writing Ansible Playbooks prerequisite.
Organizing your Ansible setup into roles allows you to reuse common configuration steps between different types of servers. Even though this is also possible by including multiple task files in a single playbook, roles rely on a known directory structure and file name conventions to automatically load files that will be used within the play.
In general, the idea behind roles is to allow you to share and reuse tasks using a consistent structure, while making it easy to maintain them without duplicating tasks for all your infrastructure.

Creating a Role>

Creating a Role #

To create an Ansible role you will need a specifically laid out directory structure. Roles always need this directory layout so that Ansible can find and use them.
We’re assuming here that you’ve been using your user’s home directory as the Ansible working directory. If you are keeping your Ansible configuration in a different location you will need to change (cd) to that directory.
To get started, let’s create a directory called roles. Ansible will look here when we want to use our new role in a playbook later in this tutorial.

cd ~
mkdir roles
cd roles

Within this directory we will define roles that can be reused across multiple playbooks and different servers. Each role that we will create requires its own directory. We are going to take the example Apache playbook from the Configuration Management 101: Writing Ansible Playbooks tutorial and turn it into a reusable Ansible role.
For reference, this is the playbook from that tutorial:
playbook.yml

---
- hosts: all
  become: true
  vars:
    doc_root: /var/www/example
  tasks:
    - name: Update apt
      apt: update_cache=yes

    - name: Install Apache
      apt: name=apache2 state=latest

    - name: Create custom document root
      file: path={{ doc_root }} state=directory owner=www-data group=www-data

    - name: Set up HTML file
      copy: src=index.html dest={{ doc_root }}/index.html owner=www-data group=www-data mode=0644

    - name: Set up Apache virtual host file
      template: src=vhost.tpl dest=/etc/apache2/sites-available/000-default.conf
      notify: restart apache
  
  handlers:
    - name: restart apache
      service: name=apache2 state=restarted

First, let’s create an Apache directory for our role and populate it with the required directories:

mkdir apache
cd apache

Next we’ll create the required set of sub-directories that will let Ansible know that it should use the contents as a role. Create these directories using the mkdir command:

mkdir defaults files handlers meta templates tasks vars

These directories will contain all of the code to implement our role. Many roles will only use one or a few of these directories depending on the complexity of the tasks involved. When you are writing your own roles, you may not need to create all of these directories.
Here is a description of what each directory represents:

defaults: This directory lets you set default variables for included or dependent roles. Any defaults set here can be overridden in playbooks or inventory files.
files: This directory contains static files and script files that might be copied to or executed on a remote server.
handlers: All handlers that were in your playbook previously can now be added into this directory.
meta: This directory is reserved for role metadata, typically used for dependency management… For example, you can define a list of roles that must be applied before the current role is invoked.
templates: This directory is reserved for templates that will generate files on remote hosts. Templates typically use variables defined on files located in the vars directory, and on host information that is collected at runtime.
tasks: This directory contains one or more files with tasks that would normally be defined in the tasks section of a regular Ansible playbook. These tasks can directly reference files and templates contained in their respective directories within the role, without the need to provide a full path to the file.
vars: Variables for a role can be specified in files inside this directory and then referenced elsewhere in a role.

If a file called main.yml exists in a directory, its contents will be automatically added to the playbook that calls the role. However, this does not apply to files and templates directories, since their contents need to be referenced explicitly.

Turning a Playbook Into a Role>

Turning a Playbook Into a Role #

Now that you are familiar with what each directory in an Ansible role is used for, we’ll turn the Apache playbook into a role to organize things better.
We should already have the roles/apache2/{subdirectories} structure set up from the last section. Now, we need to create some YAML files to define our role.

Creating the Tasks main.yml File>

Creating the Tasks main.yml File #

We’ll start with the tasks subdirectory. Move to that directory now:

cd ~/roles/apache/tasks

We need to create a main.yml file in this directory. We will populate it with the entire contents of the Apache playbook and then edit it to only include tasks.

nano main.yml

The file should look like this when you begin:
main.yml

---
- hosts: all
  become: true
  vars:
    doc_root: /var/www/example

  tasks:
    - name: Update apt
      apt: update_cache=yes

    - name: Install Apache
      apt: name=apache2 state=latest

    - name: Create custom document root
      file: path={{ doc_root }} state=directory owner=www-data group=www-data

    - name: Set up HTML file
      copy: src=index.html dest={{ doc_root }}/index.html owner=www-data group=www-data mode=0644

    - name: Set up Apache virtual host file
      template: src=vhost.tpl dest=/etc/apache2/sites-available/000-default.conf
      notify: restart apache

  handlers:
    - name: restart apache
      service: name=apache2 state=restarted

We only want to keep the first --- line and the lines in the tasks section that are highlighted. We can also remove the extraneous spaces to the left of our tasks. We will also add a new section to enable an Apache module called modsecurity that we will configure later in this tutorial. After these changes, our new ~/roles/apache/tasks/main.yml file will look like this:
main.yml

---
- name: Update apt
  apt: update_cache=yes

- name: Install Apache
  apt: name=apache2 state=latest

- name: Create custom document root
  file: path={{ doc_root }} state=directory owner=www-data group=www-data

- name: Set up HTML file
  copy: src=index.html dest={{ doc_root }}/index.html owner=www-data group=www-data mode=0644

- name: Set up Apache virtual host file
  template: src=vhost.tpl dest=/etc/apache2/sites-available/000-default.conf
  notify: restart apache

Now the tasks file is easier to follow and understand because it only contains the actual steps that will be performed when we use the Apache role.
Note how the copy and template lines use src=index.html and src=vhost.tpl respectively to reference files in our role, without any preceding path. The directory structure of our role allows referencing files and templates directly by their name, and Ansible will find them automatically for us.
Make sure to save and close the file when you are finished editing it.

Creating the Handlers main.yml File>

Creating the Handlers main.yml File #

Now that we have the bulk of the playbook in the tasks/main.yml file, we need to move the handlers section into a file located at handlers/main.yml.
First cd into the handlers subdirectory in our role:

cd ~/roles/apache/handlers

Again, open the file in your text editor and paste the entire contents of the original playbook.yml:

nano main.yml

The parts that we need to keep are highlighted again:
playbook.yml

---
- hosts: all
  become: true
  vars:
    doc_root: /var/www/example
  tasks:
    - name: Update apt
      apt: update_cache=yes

    - name: Install Apache
      apt: name=apache2 state=latest

    - name: Create custom document root
      file: path={{ doc_root }} state=directory owner=www-data group=www-data

    - name: Set up HTML file
      copy: src=index.html dest={{ doc_root }}/index.html owner=www-data group=www-data mode=0644

    - name: Set up Apache virtual host file
      template: src=vhost.tpl dest=/etc/apache2/sites-available/000-default.conf
      notify: restart apache

  handlers:
    - name: restart apache
    service: name=apache2 state=restarted

Remove the whitespace from before the handlers also. In the end, the file should look like this:

---
- name: restart apache
  service: name=apache2 state=restarted

Save and close the file when you are finished.

Adding Files and Templates>

Adding Files and Templates #

Now that we have tasks and handlers in place, the next step is to make sure there is an index.html file and a vhost.tpl template so that Ansible can find and place them on our remote servers. Since we referenced these files in the tasks/main.yml file, they need to exist or Ansible will be unable to run the role properly.
First, create the index.html file in the ~/roles/apache/files directory:

cd ~/roles/apache/files
nano index.html

Paste the following into the editor, then save and close it:

<html>
<head><title>Configuration Management Hands On</title></head>

<h1>This server was provisioned using <strong>Ansible</strong></h1>

</html>

Next we’ll edit the vhost.tpl template. Change to the templates directory and edit the file with nano:

cd ~/roles/apache/templates
nano vhost.tpl

Paste these lines into the editor, then save and close it:

<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot {{ doc_root }}

<Directory {{ doc_root }}>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
The Meta directory>

The Meta directory #

If our role depended on another role, we could add a file in the meta directory called main.yml. This file might specify that this role depends on a role called “apt”. In the Apache role that we have created we do not require any dependencies. However, in the hypothetical case of requiring another role like “apt”, the file at ~/roles/apache/meta/main.yml might look like this:

---
dependencies:
  - apt

This would ensure that the “apt” role is run before our Apache role. Creating dependencies like this is useful with more complex roles that require other pieces of software or configuration to be in place before running the actual role.

The Vars directory>

The Vars directory #

We said earlier that there is a “vars” directory that can be used to set variables for our role. While it is possible to configure default parameters for a role through a vars/main.yml file, this is usually not recommended for smaller roles.
The reason for not using the “vars” directory is that it makes the details of your configuration reside within the roles hierarchy. A role is mostly generic tasks and dependencies, whereas variables are configuration data. Coupling the two makes it harder to reuse your role elsewhere.
Instead, it is better to specify configuration details outside of the role so that you can easily share the role without worrying about exposing sensitive information. Also, variables declared within a role are easily overridden by variables in other locations. It is much better to place variable data in playbooks that are used for specific tasks.
However, the “vars” directory is still worth mentioning here because it is useful with more complex roles. For example, if a role needs to support different Linux distributions, specifying default values for variables can be useful to handle different package names, versions, and configurations.

Including other files>

Including other files #

Sometimes when you create roles with lots of tasks, dependencies, or conditional logic, they will become large and difficult to understand. In situations like this you can split tasks out into their own files and include them in your tasks/main.yml.
For example, if we had an additional set of tasks to configure TLS for our Apache server, we could separate those out into their own file. We could call the file tasks/tls.yml and include it like this in the tasks/main.yml file:

. . .
tasks:
- include: roles/apache/tasks/tls.yml
Create a Skeleton Playbook>

Create a Skeleton Playbook #

Now that we have configured our role structure, we can use it with a minimal playbook compared to the monolithic version at the beginning of this tutorial.
Using roles this way allows us to use playbooks to declare what a server is supposed to do without having to always repeat creating tasks to make it so.
To create a minimal playbook that includes our Apache role, cd out of the role directory (our home directory in this example). Now we can create a playbook file:

cd ~
nano playbook.yml

Once you have the file open, paste the following then save and close the file:

---
- hosts: all
  become: true
  roles:
    - apache
  vars:
    - doc_root: /var/www/example

There is very little information required in this file. First, we list the servers that we want to run this role on, so we use - hosts: all. If you had a group of hosts called webservers you could target them instead. Next, we declare the roles we are using. In this case there is only one, so we use the - apache line.
This is our entire playbook. It is very small and quick to read and understand. Keeping playbooks tidy like this allows us to concentrate on the overall goals for configuring servers, instead of the mechanics of individual tasks. Even better, if we have multiple role requirements, we can now list them under the roles section in our playbook and they will run in the order they appear.
For instance, if we had roles to set up a WordPress server using Apache and MySQL, we might have a playbook that looks like this:

---
- hosts: wordpress_hosts
  become: true
  roles:
    - apache
    - php
    - mysql
    - wordpress
  vars:
    - doc_root: /var/www/example

This playbook structure allows us to be very succinct about what we want a server to look like. Finally, since playbooks call roles, the command to run ours is exactly the same as if it all lived in a single file:

ansible-playbook playbook.yml

PLAY [all] ******************************************************************************************

TASK [Gathering Facts] ******************************************************
ok: [64.225.15.1]

TASK [apache : Update apt] **************************************************
ok: [64.225.15.1]

TASK [apache : Install Apache] **********************************************
changed: [64.225.15.1]

TASK [apache : Create custom document root] *********************************
changed: [64.225.15.1]

TASK [apache : Set up HTML file] ********************************************
changed: [64.225.15.1]

TASK [apache : Set up Apache virtual host file] *****************************
changed: [64.225.15.1]

RUNNING HANDLER [apache : restart apache] ***********************************
changed: [64.225.15.1]

PLAY RECAP ******************************************************************
64.225.15.1              : ok=7    changed=5    unreachable=0    failed=0

You could also call the playbook.yml file apache.yml for example, to make the name of the file reflect the role(s) that it contains.

Ansible Galaxy>

Ansible Galaxy #

A tutorial about Ansible roles would not be complete without exploring the resources available via Ansible Galaxy. The searchable Galaxy is a repository of user contributed roles that you can add to playbooks to accomplish various tasks without having to write them yourself.
For example, we can add a useful Apache module called mod_security2 to our playbook to configure Apache with some extra security settings. We will use an Ansible Galaxy role called apache_modsecurity. To use this role, we’ll download it locally and then include it in our playbook.
First let’s get familiar with the ansible-galaxy tool. We will search the Galaxy using the tool and then choose a role from the list that is returned from our search command:

ansible-galaxy search "PHP for RedHat/CentOS/Fedora/Debian/Ubuntu"

The search command will output something like the following:

Found 21 roles matching your search:

 Name                            Description
 ----                            -----------
 alikins.php                     PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 bpresles.php                    PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 entanet_devops.ansible_role_php PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 esperdyne.php                   PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 fidanf.php                      PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 frogasia.ansible-role-php       PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 geerlingguy.php                 PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 icamys.php                      PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 jhu-sheridan-libraries.php      PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 jibsan94.ansible_php            PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 KAMI911.ansible_role_php        PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 monsieurbiz.geerlingguy_php     PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 nesh-younify.ansible-role-php   PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 net2grid.php                    PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 thom8.ansible-role-php          PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 v0rts.php                       PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 vahubert.php                    PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 Vaizard.mage_php                PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 viasite-ansible.php             PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
 vvgelder.ansible-role-php       PHP for RedHat/CentOS/Fedora/Debian/Ubuntu.
(END)

Ansible will use the less command to output the search results if there are many results, which will block your terminal until you press q to exit. This is useful for when the search results are extensive and you need to paginate across them, which you can do by pressing space.
We will pick the role geerlingguy.php for our playbook. If you would like to read more about the roles returned by your search results, you can visit the Galaxy search page and paste in the role name that you’d like to learn more about.
To download a role for use in our playbook, we use the ansible-galaxy install command:

ansible-galaxy install geerlingguy.php

When you run that command you should see output like this:

- downloading role 'php', owned by geerlingguy
- downloading role from https://github.com/geerlingguy/ansible-role-php/archive/3.7.0.tar.gz
- extracting geerlingguy.php to /home/sammy/.ansible/roles/geerlingguy.php
- geerlingguy.php (3.7.0) was installed successfully

Now we can add the role to our playbook.yml file:

---
- hosts: all
  become: true
  roles:
    - apache
    - geerlingguy.php
  vars:
    - doc_root: /var/www/example
    - php_default_version_debian: "7.2"

By placing the role after our apache role, we ensure Apache is set up and configured on remote systems before any configuration for the geerlingguy.php role place. We could also include mysql, and wordpress roles in any order we choose depending on how we want remote servers to behave.
Running ansible-playbook playbook.yml with the added Galaxy role will result in output like the following:

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [64.225.15.1]

TASK [apache : Update apt] *****************************************************
changed: [64.225.15.1]

TASK [apache : Install Apache] *************************************************
changed: [64.225.15.1]

TASK [apache : Install modsecurity] ********************************************
changed: [64.225.15.1]

TASK [apache : Create custom document root] ************************************
changed: [64.225.15.1]

TASK [apache : Set up HTML file] ***********************************************
changed: [64.225.15.1]

TASK [apache : Set up Apache virtual host file] ********************************
changed: [64.225.15.1]

TASK [geerlingguy.php : Include OS-specific variables.] ************************
ok: [64.225.15.1]

TASK [geerlingguy.php : Define php_packages.] **********************************
ok: [64.225.15.1]

. . .

PLAY RECAP *********************************************************************
64.225.15.1                : ok=37   changed=15   unreachable=0    failed=0
(END)
Conclusion>

Conclusion #

Ansible roles are an excellent way to structure and define what your servers should look like. It is worth learning how to use them even if you could rely solely on playbooks for each of your servers. If you plan on using Ansible extensively, roles will keep your host-level configuration separate from your task, and ensure your Ansible code is clean and readable. Most importantly, roles allow you to easily reuse and share code, and to implement your changes in controlled and modular fashion.