Ansible Roles: Basics & How to Combine Them With Playbooks
Tags: ansible, automation, iac
Published: 2022-06-09 19:05:54


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0pmtzwiy3ppt3g7v1q8a.png)

This blog post explores the concept of Ansible roles, their structure, and how we can combine them with our playbooks. We will analyze their functionality and usage along with ways to create new roles and retrieve public shared roles from Ansible Galaxy, a public repository for Ansible resources.

If you are new to Ansible, you might also find these tutorials helpful [Ansible Tutorial for Beginners](https://spacelift.io/blog/ansible-tutorial), [Working with Ansible Playbooks](https://spacelift.io/blog/ansible-playbooks), and [How to Use Different Types of Ansible Variables](https://spacelift.io/blog/ansible-variables).

## Why Roles Are Useful in Ansible

When starting with Ansible, it’s pretty common to focus on writing playbooks to automate repeating tasks quickly. As new users automate more and more tasks with playbooks and their Ansible skills mature, they reach a point where using just Ansible playbooks is limiting. Ansible Roles to the rescue!

Roles enable us to reuse and share our Ansible code efficiently. They provide a well-defined framework and structure for setting your tasks, variables, handlers, metadata, templates, and other files. This way, we can reference and call them in our playbooks with just a few lines of code while we can reuse the same roles over many projects without the need to duplicate our code.

Since we have our code grouped and structured according to the Ansible standards, it is quite straightforward to share it with others. We will see an example of how we can accomplish that later with Ansible Galaxy. 

Organizing our Ansible content into roles provides us with a structure that is more manageable than just using playbooks. This might not be evident in minimal projects but as the number of playbooks grows, so does the complexity of our projects. 

Lastly, placing our Ansible code into roles lets us organize our automation projects into logical groupings and follow the separation of concerns design principle. Collaboration and velocity are also improved since different users can work on separate roles in parallel without modifying the same playbooks simultaneously.

## Ansible Role Structure

Let’s have a look at the standard role directory structure. For each role, we define a directory with the same name. Inside, files are grouped into subdirectories according to their function. A role must include _at least one of these standard directories and can omit any_ that isn’t actively used.

To assist us with quickly creating a well-defined role directory structure skeleton, we can leverage the command **ansible-galaxy init <your_role_name>**. The _ansible-galaxy_ command comes bundled with Ansible, so there is no need to install extra packages.

Create a skeleton structure for a role named _test_role_:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zrg47735adjj7ep62ydb.png)

This command generates this directory structure:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pol0c3phi89tyqtgl5hi.png)

Ansible checks for main.yml files, possible variations, and relevant content in each subdirectory. It’s possible to include additional YAML files in some directories. For instance, you can group your tasks in separate YAML files according to some characteristic.

- **defaults** –  Includes default values for variables of the role. Here we define some sane default variables, but they have the lowest priority and are usually overridden by other methods to customize the role.
- **files**  – Contains static and custom files that the role uses to perform various tasks.
- **handlers** – A set of [handlers](https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html) that are triggered by tasks of the role. 
meta – Includes metadata information for the role, its dependencies, the author, license, available platform, etc.
- **tasks** – A list of tasks to be executed by the role. This part could be considered similar to the task section of a playbook.
- **templates** – Contains [Jinja2](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html) template files used by tasks of the role.
- **tests** – Includes configuration files related to role testing.
- **vars** – Contains variables defined for the role. These have quite a high [precedence](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html) in Ansible.

Another directory that wasn’t automatically generated by the _ansible-galaxy init_ command but is mentioned in the official Ansible docs, and you might find helpful in some cases, is the **library** directory. Inside it, we define any custom modules and plugins that we have written and used by the role. Finally, we also have a preconfigured **README.md** file that we can fill with details and useful information about our role.


## Creating Roles

A common tactic is to refactor an Ansible playbook into a role. To achieve that, we have to decompose the different parts of a playbook and stitch them together into an Ansible role using the directories we’ve just seen in the previous section. 

This section will go through an example of creating a new role for installing and configuring a minimal Nginx web server from scratch. If you wish to follow along, you will need [VirtualBox](https://www.virtualbox.org/), [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html), and [Vagrant](https://www.vagrantup.com/) installed locally.

Ansible searches for referenced roles in common paths like the orchestrating playbook’s directory, the roles/ directory, or the configured [roles_path](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#default-roles-path) configuration value. It’s also possible to set a custom path when referencing a role:

```
- hosts: all
  roles:
    - role: "/custom_path/to/the/role"
```

Using _the ansible-galaxy init_ command, we generate the initial directory structure for a role named _webserver_ inside a parent directory named _roles_. Let’s go ahead and delete the tests directory since we won’t be using it. We will see how to utilize all the other directories during our demo. 

The final structure of our role looks like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6slevcwpicl2j9fdm157.png)

First, let’s define the most fundamental part of our role, its tasks. Head to the _tasks_ directory and edit the _main.yml_ file.

_roles/webserver/tasks/main.yml_

```
---
# tasks file for webserver
- name: Update and upgrade apt
  ansible.builtin.apt:
    update_cache: yes
    cache_valid_time: 3600
    upgrade: yes
 
- name: "Install Nginx to version {{ nginx_version }}"
  ansible.builtin.apt:
    name: "nginx={{ nginx_version }}"
    state: present
 
- name: Copy the Nginx configuration file to the host
  template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/sites-available/default
- name: Create link to the new config to enable it
  file:
    dest: /etc/nginx/sites-enabled/default
    src: /etc/nginx/sites-available/default
    state: link
 
- name: Create Nginx directory
  ansible.builtin.file:
    path: "{{ nginx_custom_directory }}"
    state: directory
 
- name: Copy index.html to the Nginx directory
  copy:
    src: files/index.html
    dest: "{{ nginx_custom_directory }}/index.html"
  notify: Restart the Nginx service
```

Here, we define a handful of tasks that update the operating system, install an Nginx web server, and set up a minimal custom configuration for demo purposes.

Next, we move to the _defaults_ directory, where we will set default values for the variables used in the tasks. If there is no other definition for these variables, they will be picked up and used by the role, but usually, they are meant to be easily overwritten.

_roles/webserver/defaults/main.yml_

```
---
# defaults file for webserver
nginx_version: 1.18.0-0ubuntu1.3
nginx_custom_directory: /var/www/example_domain
```

Moving on to our _vars_ directory, we define values with higher precedence that aren’t meant to be overridden at the play level. Here, we override the default variable that defines the Nginx custom directory.

_roles/webserver/vars/main.yml_

```
---
# vars file for webserver
nginx_custom_directory: /home/ubuntu/nginx
```

In the _handlers_ directory, we define any handler that is triggered by our tasks. One of our tasks includes a _notify_ keyword since it needs to trigger our _Restart the Nginx service_ handler.

_roles/webserver/handlers/main.yml_

```
---
# handlers file for webserver
- name: Restart the Nginx service
  service:
    name: nginx
    state: restarted
```

In the _templates_ directory, we leverage a Jinja2 template file for the Nginx configuration that gets the Nginx custom directory value from one of our previously defined variables.

_roles/webserver/templates/nginx.conf.j2_

```
server {
        listen 80;
        listen [::]:80;
        root {{ nginx_custom_directory }};
        index index.html;
        location / {
                try_files $uri $uri/ =404;
        }
}
```

In the _files_ directory, we define a static file _index.html_ that will serve as our static demo webpage.

_roles/webserver/files/index.html_

```
<html>
 <head>
   <title>Hello from Ngnix </title>
 </head>
 <body>
 <h1>This is our test webserver</h1>
 <p>This Nginx web server was deployed by Ansible.</p>
 </body>
</html>
```

We use the _meta_ directory to add metadata and information about the role. Any role dependencies by other roles go here as well.

_roles/webservers/meta/main.yml_

```
galaxy_info:
  author: Ioannis Mosutakis
  description: Installs Nginx and configures a minimal test webserver
  company: ACME Corp
  license: Apache-2.0
  role_name: websercer
 
  min_ansible_version: "2.1"
 
 # If this is a Container Enabled role, provide the minimum Ansible Container version.
 # min_ansible_container_version:
 
 #
 # Provide a list of supported platforms, and for each platform a list of versions.
 # If you don't wish to enumerate all versions for a particular platform, use 'all'.
 # To view available platforms and versions (or releases), visit:
 # https://galaxy.ansible.com/api/v1/platforms/
 #
  platforms:
  - name: Ubuntu
    versions:
      - bionic
      - focal
 
  galaxy_tags:
    - nginx
    - webserver
    - development
    - test
 
dependencies: []
 # List your role dependencies here, one per line. Be sure to remove the '[]' above,
 # if you add dependencies to this list.
```

Lastly, we update the _README.md _file accordingly. The autogenerated file provided by the _ansible-galaxy init_ command includes many pointers and guidance on filling this nicely. 

_roles/webserver/README.md_

```
Role Name
=========
 
Role Name
=========
 
This is a role created for demonstration purposes that configures a basic nginx webserver with a minimal configuration.
 
Requirements
------------
 
Any prerequisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
 
* Ansible
* Jinja2
 
Role Variables
--------------
 
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
 
### defaults/main.yml
Default nginx installation variables.
 
* nginx_version: Specific version of nginx to install
* nginx_custom_directory: Custom directory for nginx installation
 
### vars/main.yml
Here we define variables that have high precedence and aren't intended to be changed by the play.
 
* nginx_custom_directory: Custom directory for nginx installation
 
Dependencies
------------
 
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
 
Example Playbook
----------------
 
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
 
   - hosts: all
     become: true
     roles:
       - webserver
 
License
-------
 
Apache-2.0
 
Author Information
------------------
 
Ioannis Moustakis

```

## Using Roles

Once we have defined all the necessary parts of our role, it’s time to use it in plays. The classic and most obvious way is to reference a role at the play level with the **roles** option:

```
- hosts: all
  become: true
  roles:
    - webserver
```

With this option, each _role_ defined in our playbook is executed before any other tasks defined in the play.

This is an example play to try out our new webserver role. Let’s go ahead and execute this play. To follow along, you should first run the `vagrant up` command from the top directory of [this repository](https://github.com/spacelift-io-blog-posts/Blog-Technical-Content/tree/master/ansible-roles) to create our target remote host.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ubzktr2mwuzprxsb2zs.png)

Sweet! All the tasks have been completed successfully. Let’s also validate that we have configured our Ngnix web server correctly.

Use the command `vagrant ssh host1` to connect to our demo Vagrant host. Then execute `systemctl status nginx` to verify that Nginx service is up and running. Finally, run the command `curl localhost` to check if the web server responds with the custom page that we configured.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qo29mvu44k28zr8ektps.png)

When using the _roles_ option at the play level, we can override any of the default role’s variables or pass other keywords, like _tags_. Tags are added to all tasks within the role.

```
- hosts: all
 become: true
 roles:
   - role: webserver
     vars:
       nginx_version: 1.17.10-0ubuntu1
     tags: example_tag
```

Here, we override the default variable _nginx_version_ with another version.

Apart from defining roles at the play level with the _roles_ option, we can use them also at the tasks level with the options **include_role** dynamically and **import_role** statically. These are useful when we would like to run our role tasks in a specific order and not necessarily before any other playbook tasks. This way, roles run in the order they are defined in plays.

```
- hosts: all
  tasks:
    - name: Print a message
      ansible.builtin.debug:
        msg: "This task runs first and before the example role"
 
    - name: Include the example role and run its tasks
      include_role:
        name: example
 
    - name: Print a message
      ansible.builtin.debug:
        msg: "This task runs after the example role"
  
    - name: Include the example_2 role and run its tasks in the end
      include_role:
        name: example_2
```

Check the [official docs](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html#using-roles) for achieving more fine-grained control of your role’s task execution order. 

Even if we define a role multiple times, Ansible will execute it only once. Occasionally, we might want to run a role multiple times but with different parameters. Passing a different set of parameters to the same roles allows executing the role more than once. 

Example of executing the role test three times:

```
- hosts: all
  roles:
    - role: test
      message: "First time"
    - role: test
      message: "Second time"
    - role: test
      message: "Third time"
```

## Sharing Roles with Ansible Galaxy

[Ansible Galaxy](https://galaxy.ansible.com/) is an online open-source, public repository of Ansible content. There, we can search, download and use any shared roles and leverage the power of its community. We have already used its client, _ansible-galaxy_, which comes bundled with Ansible and provides a framework for creating well-structured roles.

You can use Ansible Galaxy to browse for roles that fit your use case and save time by using them instead of writing everything from scratch. For each role, you can see its code repository, documentation, and even a rating from other users. Before running any role, check its code repository to ensure it’s safe and does what you expect. Here’s a blog post on [How to evaluate community Ansible roles](https://www.jeffgeerling.com/blog/2019/how-evaluate-community-ansible-roles-your-playbooks). If you are curious about Galaxy, check out its [official documentation](https://galaxy.ansible.com/docs/) page for more details.

To download and install a role from Galaxy, use the ansible-galaxy install command. You can usually find the installation command necessary for the role on Galaxy. For example, look at [this role that installs a PostgreSQL server](https://galaxy.ansible.com/geerlingguy/postgresql).

Install the role with:

```
ansible-galaxy install geerlingguy.postgresql
```
Then use it in a playbook while overriding the default role variable _postgresql_users_ to create an example user for us.

```
---
- hosts: all
  become: true
  roles:
    - role: geerlingguy.postgresql
      vars:
        postgresql_users:
          - name: christina

```

## Ansible Roles Tips & Tricks
This section gathers some tips and tricks that might help you along your journey with Ansible roles.

- Always use **descriptive names for your roles, tasks, and variables. Document the intent** and the purpose of your roles thoroughly and point out any variables that the user has to set. Set **sane defaults** and simplify your roles as much as possible to allow users to get onboarded quickly.
- **Never place secrets and sensitive data in your roles YAML files**. Secret values should be passed to the role at execution time by the play as a variable and should never be stored in any code repository.
- At first, it might be tempting to define a role that handles many responsibilities. For instance, we could create a role that installs multiple components, a common anti-pattern. Try to follow the separation of concerns design principle as much as possible and **separate your roles based on different functionalities or technical components**.
- Try to keep your **roles as loosely coupled as possible** and avoid adding too many dependencies. 
- To control the execution order of roles and tasks, use the _import_role_ or _include_role_ tasks instead of the classic _roles_ keyword.
- When it makes sense, group your tasks in separate task files for improved clarity and organization.

## Key Points

We deep dived into Ansible Roles and their utility and saw how to refactor our playbooks into roles or generate them from scratch. We went through a complete example of creating and using a role and explored how we can benefit from the Ansible Galaxy community.

Thank you for reading, and I hope you enjoyed this “Ansible Roles” blog post as much as I did.