Automate All the Things With Ansible: Part Two


This is part two of a two-part tutorial on Ansible. Part one is here. In this part, you will learn about roles (Ansible's building blocks), variables, loops, how to use roles in playbooks, and how to organize roles into a directory structure.


When you manage tens, hundreds or more servers, probably many of them need to be configured similarly. Different groups of servers like web servers or database servers will require their own special configuration, but also may share some other common functionality. It is of course possible to just copy tasks around, but this gets old really fast when dealing with a complicated infrastructure.

Ansible roles are the ticket. Playbooks can include roles. Roles can depend on other roles, and Ansible best practices recommend grouping hosts in your inventory file based on their roles. Roles are the backbone of serious Ansible-managed infrastructure. As usual, I'll start with an example and introduce many of the capabilities of roles through the example.

I like aliases and shell functions a lot because I can't remember all the arcane switches and options for each command, and also because it saves a lot of typing. I also like to have some tools like htop and tmux on every server I log in to.

Here is a file that contains some of my favorite aliases and functions. I'll call it '.gigirc'. By the way, if you ever wondered what the 'rc' suffix stands for in all those rc files, then it stands for 'Run Commands'.

Let's define a role called 'common' that creates a user called 'gigi', adds a public ssh key, copies the '.gigirc' file and adds a line at the end of '~/.bashrc' that runs this file and finally installs the common packages vim, htop and tmux (defined in the 'vars/main.yml file'). 

I will introduce a lot of new stuff here: four different modules, variables, and loops. Also, roles are typically spread across multiple files in a standard directory structure. I'll show you a couple of files and then explain about the directory structure. Here is the 'tasks/main.yml' file:

And here is the vars/main.yml file that contains the definition of the 'COMMON_PACKAGES' variable used to specify which common packages to install.


The user module can manage user accounts. Here I use it to create the user 'gigi'.

The authorized_key module is for adding/removing SSH authorized keys. Here I use it to add my public key for the 'gigi' user.

The lineinfile module can be used to replace or add single lines to a file. In this case, I use it to source the '.gigirc file' from '.bashrc', so all the cool aliases and functions in '.gigirc' are always available in any interactive session.

Finally, the apt module has tons of options for managing apt packages. Here I just install some common packages.


The COMMON_PACKAGES you see in the last task for installing common packages is a variable. Ansible lets you use variables defined almost anywhere: playbooks, inventory, roles, dedicated files, and even environment variables. There is a lot more information about variables in the documentation.


Ansible is declarative, so it doesn't support explicit loops. But there is a plethora of with_xxx that allows you to perform repeated operations on some structure like a list of users, packages. or lines in a file. You can also repeat operations until some condition is true or get the index of the current item. Additional information can be found in the documentation

Role Directory Structure

Here is what a typical role directory structure may look like:


├── handlers

│   └── main.yml

├── meta

│   └── main.yml

├── tasks

│   └── main.yml

├── templates

└── vars

    ├── Debian.yml

    ├── Ubuntu.yml

    └── main.yml

The 'tasks/main.yml' file is the where all the tasks are defined. Each task corresponds to an Ansible command that typically uses a module.

The 'meta/main.yml' file will contain a list of other roles that the current role depends on. Those roles' tasks will be executed before the current role, so it can be sure all its prerequisites are met.

The 'handlers/main.yml' file is where you keep your handlers, like the handler you saw earlier that starts Nginx after installation.

The templates directory is where you keep Jinja2 templates of configuration and other files that you want to populate and copy to the target system.

The vars directory contains various variables and can conditionally contain different values for different operating systems (very common use case).

It's important to note that Ansible is very flexible and you can put anything almost anywhere. This is just one possible structure that makes sense to me. If you look at other people's directory structures, you may see something completely different. That's totally fine. Don't be alarmed. Ansible is not prescriptive, although it does provide guidance for best practices.

Using Roles

Roles do the heavy lifting, but playbooks are how you actually do work. The playbooks marry the inventory and the roles and specify what roles to play on which host. Here is what a playbook with roles looks like:

Running the playbook produces the following output:


Ansible is a great tool. It is lightweight. It can be used interactively with ad-hoc commands, and it scales very well to massive systems. It also has a lot of momentum and a great community. If you manage or even just work with remote servers, you want Ansible.



Related Articles