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

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.