Introduction to Ansible roles

Ansible is a free and open source provisioning system written in Python and sponsored by Red Hat. In previous tutorials we learned the Ansible basics, and we saw how to organize tasks in playbooks and how to secure sensitive data using ansible-vault. There is another, very important concept we need to focus on when dealing with Ansible: roles.

Ansible roles represent a way to organize and reuse code and tasks targeted on performing specific actions. In this tutorial we learn how to create and use Ansible roles on Linux.

In this tutorial you will learn:

  • What is an Ansible role
  • How to create a role from scratch
  • How to install a role from Ansible galaxy
  • How to use a role in a playbook
Introduction to Ansible roles
Introduction to Ansible roles
Software Requirements and Linux Command Line Conventions
Category Requirements, Conventions or Software Version Used
System Distribution-agnostic
Software ansible
Other None
Conventions # – requires given linux-commands to be executed with root privileges either directly as a root user or by use of sudo command
$ – requires given linux-commands to be executed as a regular non-privileged user

Introduction

Let’s start by summarizing what an Ansible role is. We could basically define a role as a reusable group of tasks focused on obtaining a specific result, distributed together with associated variables, handlers and files.



Ansible roles are usually hosted on the the ansible galaxy platform from which they can be installed via the ansible-galaxy utility.

Good practices dictates each role to be focused on a specific target: we, can for example create a role that is used to deploy dotfiles from a git repository, a role to install Samba, etc. Ansible roles adopt a specific directory structure, which can be manually created, or, more conveniently, generated by using dedicated commands. let’s see how.

Bootstrapping a role

For the sake of this tutorial, we will create a role we will use to deploy dotfiles maintained in a git repository. Instead of creating the role directory structure manually, we can use the ansible-galaxy utility:

$ ansible-galaxy role init dotfiles

The command above creates a directory named after the role, containing the basic directory structure:

dotfiles
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml

Each  directory in the structure has a purpose, and contains a  main.yml file: this is where Ansible looks for content. In the table below we tried to summarize what they are used for (we omitted the “tests” directory, since we will talk about role testing in a dedicated tutorial):

Files Purpose
defaults/main.yml Defining role variables
handlers/main.yml Defining handlers. Handlers are tasks we executed in response to certain events, such as when a task reports a change of status.
meta/main.yml Role metadata (description, dependencies, supported Ansible versions, etc…)
tasks/main.yml Defining role tasks
vars/main.yml Defining role variables

Files in the vars and defaults directories are both used to define role variables; there is a difference however: variables defined in files under the former have an higher priority than those defined under the latter, therefore are “harder” to overwrite. We usually define variables which the user is more likely to change under defaults, and those more “static” under vars (here you can learn more about Ansible variable precedence)

Other directories can be part of the structure of a role, but are not automatically generated. Two commonly used ones are templates and files: as their names suggest, they are used to store jinja2 templates and files, respectively.

Developing our role

Let’s start developing our role. We will start by defining role variables, than the tasks which will actually perform what we want to achieve.

Defining the role variables

Let’s start by defining the variables. In the default/main.yml file we write the following:

---
dotfiles_repository: ""
dotfiles_destination: ~/.dotfiles
dotfiles_files: []

The first variable we defined is dotfiles_repository. The value of this variable must be the URL of the remote git repository hosting our dotfiles.

We also need to know where the repository should be cloned in the filesystem. We can set this path as the value of the dotfiles_destination variable. In this case the ~/.dotfiles directory is what we set as default, and what will be used if the user doesn’t explicitly override the variable value.

The last variable we defined is dotfiles_files: the value of this variable is the list of the files in the repository the user wants to be linked under its home directory.

Creating the tasks

Now, let’s create the tasks. As we saw before, the file used as entry point for tasks is tasks/main.yml. First we want to ensure git is installed on the target machine. Since we want our role to be usable on different distributions, in order to install the package we can use the ansible.builtin.package module, which works as an abstraction over the most common package managers such as apt, dnf and pacman:

--- 
- name: Ensure git is installed 
  become: true
  ansible.builtin.package: 
    name: git 
    state: present



The next tasks consists into cloning the git repository. The ansible.builtin.git module is what we need here. The modules has two required parameters: “repo” and “dest” which represent the address of the git repository, and the path where the repository should be cloned, respectively. We just need to use the corresponding variables we previously defined:

- name: Clone dotfiles repository
  ansible.builtin.git:
    repo: '{{ dotfiles_repository }}'
    dest: '{{ dotfiles_destination }}'

Finally, we add the task which which creates the symbolic links. To create links we can use the ansible.builtin.file. The “src” parameter of this modules is the path of the file to link to : we obtain it by joining the path of the cloned repository and the name of the file.

The “dest” parameters, instead, is the path where the symbolic link should be created. Here we added a little bit of logic in order to automatically prepend the name of the file with a dot in case it is stored without it in the repository. We also used the “force” parameter, setting its value to “true”, in order to force the creation of the link in case a file with the same name already exists (this doesn’t work for directories).

The task is executed in a loop, for each file in the dotfiles_files list:

- name: Create the symbolic links
  ansible.builtin.file:
    src: '{{ dotfiles_destination }}/{{ item }}'
    dest: '~/{{ item is regex("^\.") | ternary(item, "." + item) }}'
    state: link
    force: true
  loop: '{{ dotfiles_files }}'

Role metadata

Defining role metadata is mandatory if we plan to share our role on the Ansible galaxy platform. As we briefly mentioned before, metadata is defined the meta/main.yml file. Here is an example of the information which can be provided:

galaxy_info:
  author: your name
  role_name: your role name
  description: your role description
  company: your company (optional)
  license: license (GPL-2.0-or-later, MIT, etc)
  min_ansible_version: 2.1
  galaxy_tags: []
dependencies: []

Using the role

There are basically two ways we can use a role in a playbook. One is by including the role in the playbook “roles” section, the other is to import it or include it in the “tasks” section with the ansible.builtin.import_role or ansible.builtin.include_role modules.

The difference between importing and including modules (or tasks) is basically in the time the statements are parsed: import statements are processed very early, when the playbook is parsed, while “includes” are processed as they are encountered (take a look at this comparison for more info on the subject).

Roles can be installed per-project, inside a directory named “roles”, relative to the playbook which uses them, or “globally” in the roles_path, which by default is: ~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles. They can also be referenced by their absolute path in the filesystem.

Here is an example of how we can use our role in a playbook, and provide values for the variables it uses:

--- 
- name: Example playbook
  hosts: localhost
  roles:
    - role: linuxconfig.dotfiles
      dotfiles_repository: https://github.com/linuxconfig/dotfiles
      dotfiles_files:
        - vim
        - vimrc

We can also import or include the role as a task:

- name: Example playbook
  hosts: localhost
  tasks:
    - name: Clone dotfiles
      ansible.builtin.import_role:
        name: linuxconfig.dotfiles
      vars:
        dotfiles_repository: https://github.com/linuxconfig/dotfiles
        dotfiles_files:
         - vim
         - vimrc

Installing Roles

Once a role is shared on Ansible galaxy it can be installed by running:

$ ansible-galaxy install <role-name>



The command above installs the role passed as argument in the ~/.ansible/roles directory, by default. To install the role in an alternative location we can use the -p option (short for --roles-path) and provide the path where we want to install the role as argument. Supposing we are on our project directory and we want to install a role “locally”, we would run:

$ ansible-galaxy install <role-name> -p roles

Roles to be installed as part of a project can also be listed into a requirements file, using the following syntax:

roles:
  - linuxconfig.dotfiles

To install roles listed in a requirement file, we can use the -r option (--role-file) and pass the path of the file as argument. Supposing the requirements file to be called requirements.yml, we would run:

$ ansible-galaxy install -r requirements.yml

Conclusions

In this tutorial we saw how to create, use and install Ansible roles, which represent a way to organize and reuse code and tasks targeted on performing specific actions.



Comments and Discussions
Linux Forum