Ansible – from tasks to roles

In this post I am going to introduce Ansible roles and walk you through a process of iterative refinement of an Ansible project.

The goal here will be to automatically deploy and configure a simple repository serving static files. I will start from a playbook with multiple tasks and slowly build towards a better, more versatile and reusable structure. By the end of this article you will have acquired the knowledge about Ansible roles and some of the best practices regarding Ansible project structure.

It is assumed that you know the basics of Ansible. In case you do not, please read the previous post first: Introduction to Ansible.

Task based playbook

Our repository will be a simple HTTP server that serves files from specific directory on a given TCP port.

So, what we need to do is:

  • install HTTP server
  • configure the HTTP server to serve files from a specified directory
  • open TCP port in firewall to allow remote access to the server
  • create files in the repository directory

The port of our choosing is 8080 and the files directory is /var/repositories/repository1/. We will create file1, file2 and file3 in that directory.

Let’s see how we can do it with Ansible playbook deploy/play.yml at first:

All the examples in this post are specific to CentOS Linux distribution. For example, yum module is used to install Nginx server. On another Linux distribution there might be a different package management tool, like apt, so the task will have to use apt module instead.

All that is left is to tell Ansible to play that playbook:

After Ansible successfully finishes the deployment, the server will be up and running. We can try fetching some static files to test it:

Great! We have our repository. That was quite easy. We have just used several Ansible modules: yum, copy, file, template, service, firewalld and selinux.

In case you wonder how Nginx HTTP server was installed and configured, you can find more info in references [4] and [5] at the end of this post.

Notice that in the task that is using template module, we are referencing repository.conf.j2 template file. This file is placed in the root folder of the project and it specifies Nginx HTTP server configuration. Here is its content:

The above file is a standard Nginx config file with Jinja2 placeholders {{ }}.

If you analyse that playbook in depth, you will notice that there are several places with repeated information, i.e. /var/repositories/repository1 and 8080 port. This is not the best solution, because changing these values in the future requires more diligence than needed in case a variable was used. To improve this, we can use variables file or Ansible facts feature, which allows us to capture data in temporary variables.

The /var/repositories string seems like a root directory for the actual repository directory, so we can place this value in a variable, e.g. in group_vars/every.yml file (notice that every is the group name of hosts for which all tasks are run):

For the repository name and port we can use the facts feature.

Let’s see how the playbook looks now:

That looks good. We have used captured variables using Jinja2 templates3 notation.

But, what if we need to create other repositories or add security measures to the same or another group of hosts? We can of course start copying some tasks to achieve this, but it is not a good approach for obvious reasons (the Don’t Repeat Yourself rule). What can we do?

Introducing Ansible roles

As you saw above, Ansible allows you to define playbooks in which you specify a sequence of tasks that are acted out on the hosts of your choosing. This is sufficient, provided there are not many things to accomplish.

However, when your servers require a lot of software components with complex configuration, specifying all these tasks in a single playbook can clutter it significantly. On top of that, you might need to use files with variables or configuration file templates that will be spread all over the place. This makes the playbook and the project itself hard to maintain and expand. To address these issues Ansible provides us with a role mechanism.

Ansible role is a concept of bringing related tasks, variables and files into a well-structured component. This component can then be invoked from a playbook or from another role and it can be parameterized. All these characteristics make roles very versatile.

A role is basically a directory with multiple specific subdirectories and files, resembling the one below:

In the above example you can see a http_server role in roles folder with several subfolders: defaults, tasks, files, templates and vars. These folder names are based on convention established by Ansible1.

The main.yml file is the default file that Ansible looks for when processing subfolders. For example, the tasks/main.yml file specifies default tasks which the role performs. You may also create files with other names (e.g. to improve a large role’s structure), but to use them you have to reference them explicitly from the default files.

The other folders are:

  • defaults – this folder is for default variables used by the role (these variables can be easily overridden; Ansible defines variable precedence in their documentation2),
  • files – here you place miscellaneous files, which might be copied to a remote server,
  • templates – here you place Jinja2 template files3, which Ansible will process and send to the server,
  • vars – here you place miscellaneous variables used by the role

Having this knowledge, we can start refactoring our playbook.

Moving tasks into roles

As you probably noticed, some tasks are related purely to installing and configuring the HTTP server, namely:

  • configuring Nginx repository
  • installing http server
  • removing default http server config file

Let’s then move these tasks into a new role – http_server_install:

And the content of tasks’ main file becomes:

I have decided to move task “setting permissive mode for SELinux” to this role as well, as it is related to allowing Nginx to access files from different directories

Now we can further refine the playbook:

Here is another form with a different way of invoking the role:

To further improve the playbook, we might consider moving or grouping other tasks.

Notice we have a task that stops the HTTP server and another one that starts it. Let’s move these into separate roles to hide the detail that Nginx is the server.

Setting up the repository requires several steps – creating its directory, preparing HTTP server config file for given TCP port and directory, and opening the port in firewall. As these steps are related to defining a repository, let’s move them into a role.

There is another activity lurking in the playbook – creating repository files. Seems like a nicely cut responsibility, so a role can be created for handling it.

Opening a firewall port can be placed in a separate role for convenience, too.

Let’s define the below roles:

  • http_server_start
  • http_server_stop
  • repository_setup
  • repository_files_setup
  • open_firewall_port

The http_server_start/tasks/main.yml file becomes:

The server stopping role is analogous, having state=stopped.

The open_firewall_port/tasks/main.yml file becomes:

I have parameterized the above role with port parameter. You will see later on how to invoke such a role.

Notice that you can reference a variable in task name, which makes it easier to troubleshoot issues while running the playbook.

The repository_files_setup/tasks/main.yml file becomes:

In the above role I have used Ansible’s with_items feature which allows iterating over list of values.

This role is also parameterized. It requires name and files params. The first one specifies the directory name, the latter one a list of file dictionary objects (“file” is just an abstract name here). These file objects require name and content attributes, first being the file name, second being the content of the file.

Lastly, let’s see how the repository_setup role looks like:

Apart from the above, I also moved the repository.conf.j2 Jinja2 template3 to roles/repository_setup/templates/ folder. Thanks to this you do not have to provide an absolute or relative path to the template file.

Notice that there is an invocation of open_firewall_port role with its parameter port. This is great, we just reused a role in another role.

Alright, we have a nicely defined project structure now:

And so, our playbook becomes much more concise:

Let’s see what happens. Firstly, we install the HTTP server. Then, we stop it to configure our repository1. Later on, we start the server and create files in the same repository by invoking repository_files_setup role.

The best gain is that we can now create another repository just by invoking repository_setup and repository_files_setup with different parameters.

Best practices summary

Here is a concise list of the best practices which I used above:

  • parameterize roles
  • reference variables in task names to aid troubleshooting playbook issues
  • encapsulate low-level details in roles
  • reuse roles in other roles


In this post I have presented Ansible roles – what they are and how you can use them in your projects. We have performed several iterations of an automation project for deploying and configuring static files serving repositories. We have learned the best practices for creating roles and structuring Ansible projects.

For your convenience, I have created a repository on GitHub with the above code and some documentation. Enjoy!


[1] Ansible roles:

[2] Ansible variables precedence:

[3] Ansible templating with Jinja2:

[4] Nginx installation instructions:

[5] Nginx – configuring static files serving:

Please follow and like us:

Leave a Reply

Your email address will not be published. Required fields are marked *