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.
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.
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:
- ---
- - 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.
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:
- ---
- - 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.
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.
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:
- ---
- - 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:
- ---
- - 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.
main.yml
FileNow 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:
- ---
- - 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.
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>
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.
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.
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
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
OutputPLAY [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.
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:
OutputFound 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:
Output- 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:
OutputPLAY [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)
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Thanks! How would I do defaults though?
Great article, but you are missing “defaults” directory in which you can add default values for variables which basically get overridden by everything else.
Nice tutorial, and definitely helpful for demonstrating how to convert a large playbook into one or many separate roles.
The only thing I would add is that, rather than creating the whole directory structure manually, you can instead use
ansible-galaxy role init <rolename>
as a handy shortcut to create it for you, e.g.:Excellent! Very well explained! Thanks!
thanks
clear explanation!! good one!
Thanks! This article is superb. It particularly helped me understand ‘why’ I would want to use roles, with clear examples.
Nice Job. Clear and concise…
Great article,very precise and clear,really thanks.
This comment has been deleted