Ansible

Ansible Roles : Basics & How to Combine Them With Playbooks

Ansible Roles

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, Working with Ansible Playbooks, and How to Use Different Types of 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:

test_role

This command generates this directory structure:

Directory structure

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 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 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 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, Ansible, and Vagrant installed locally.

Ansible searches for referenced roles in common paths like the orchestrating playbook’s directory, the roles/ directory, or the configured 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:

The final structure of our role

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 to create our target remote host.

webserver role

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.

vagrant ssh host1

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 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 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. If you are curious about Galaxy, check out its official documentation 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.

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.

To learn more, check out the 44 Ansible Best Practices to Follow.

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.

We encourage you to also explore how Spacelift’s vibrant ecosystem and excellent GitOps flow can greatly assist you in managing and orchestrating Ansible. By introducing Spacelift on top of Ansible, you can then easily create custom workflows based on pull requests and apply any necessary compliance checks for your organization. Get started on your journey by creating a free trial account.

Spacelift is currently running a closed beta of Ansible, sign up here if you wish to be part of the beta version testing!

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

Continuous Integration and Deployment for your IaC

Spacelift allows you to automate, audit, secure, and continuously deliver your infrastructure.  It helps overcome common state management issues and adds several must-have features for infrastructure management.

Start free trial