What is Ansible?
Well, I’m gonna steal this directly from the Ansible Homepage:
“Ansible is a universal language, unraveling the mystery of how work gets done. Turn tough tasks into repeatable playbooks. Roll out enterprise-wide protocols with the push of a button.”
Brilliant. Ansible allows for the idea of Infrastructure-as-Code. This concept basically says that you can write a few text files with what you want a machine to do, then run those text files. The end result should be a correctly configured machine.
How it differs from Bash scripts
Human readability
Ansible can be read by a human being with no scripting experience, and should provide them with some measure of insight into what the Ansible playbook achieves. For instance:
---
- name: Update ubuntu device
hosts: ubuntu
become: True
tasks:
- name: Carry out complete 'apt upgrade'
apt: upgrade=full update_cache=yes
When you read this, you are able to identify some key points:
- This playbook updates Ubuntu Devices
- It will only run on hosts in the ‘ubuntu’ group
- The only task in this playbook is to carry out the ‘apt upgrade’ command.
You didn’t need any real technical know-how to identify those points, which is a good thing.
It is true that Bash scripts can be commented, but I for one often forget to comment my code, unless I know for a fact someone else is going to read it!
Syntax and error checking
Ansible uses YAML (YAML Ain’t Markup language), which is fairly easy to check for errors and inconsistencies. This grammar check is particularly useful if you are the one writing a playbook, as you don’t have to execute your playbook after each change. Ansible will highlight the most glaring errors you have, without you ever having to execute your playbook.
Dry-Runs
Ansible provides what they refer to as ‘Check Mode’. This mode allows Ansible tasks to report on what changes they would have made, without ever actually making changes to a remote system. This comes in handy when you need to make sure that your playbook won’t have any unintended consequences when being run across a large number of hosts.
The real Difference
Applications such as ShellCheck can be used to check Bash syntax and best practices, good commenting can describe what the code itself does.
So what is the real difference between the two?
As redditor “tremblane” puts it:
- Bash (or another scripting language) describes actions. Do this thing. Now do this other thing.
- Ansible (and other configuration management systems) describe the desired state. This file should exist here, owned by this user, with these permissions. This package should be installed. This line in this config file should appear like this.
Ansible doesn’t need you to know the exact command in bash, or python, or the obscure Linux command to do something. You tell Ansible what you want the end result to be, and it handles the rest.
This is where Ansible shines.
Ansible Concepts
Ansible uses a few key concepts that I’m just going to summarise here:
Inventory
An inventory stores all of the devices that you want Ansible to know about. For example:
[ubuntu]
chat.int.seantodd.co.uk
friend-server.int.seantodd.co.uk
funkwhale.int.seantodd.co.uk
libertas.int.seantodd.co.uk
mastodon.int.seantodd.co.uk
mediaget.int.seantodd.co.uk
mysql.int.seantodd.co.uk
nut.int.seantodd.co.uk
plex.int.seantodd.co.uk
secret-tunnel.int.seantodd.co.uk
ubiquiti-controller.int.seantodd.co.uk
jitsi.int.seantodd.co.uk
[centos]
gitea.int.seantodd.co.uk
ansible.int.seantodd.co.uk
[jitsi]
jitsi.int.seantodd.co.uk
[mysql]
mysql.int.seantodd.co.uk
[friend_site]
friend-server.int.seantodd.co.uk
In this inventory I have several groups, each one denoted by the square brackets.
From this file, you can tell that I have both Ubuntu and CentOS devices separated into groups. I use this when I carry out updates via ansible.
You can also see that there are three other groups.
These denote a particular role, whether it be the name of an application, or something else. You may have also noticed that you can have the same machine listed under many groups.
Modules
Modules are how Ansible can carry out actions on your machines. These modules mean that you don’t need to know the correct script syntax to carry out an action.
For example:
The following bash script is used to update the Apt package manager’s cache, then install the latest version of Nginx available to Apt.
$ sudo apt update
$ sudo apt install nginx
This can also be expressed using the apt
module in Ansible.
- name: Update apt cache and install nginx
apt:
name: nginx
update_cache: yes
Ansible has a wide range of modules available for people to use.
Tasks
Tasks are Ansible’s way of defining a single action. An example of a task can be seen in the ‘Module’ example above.
The task itself is the entire code block, providing a description for the task, defined by the name
variable in the task.
The task then states which module the task uses, then defines the variables required by the module.
Playbooks
Playbooks describe the end-state of a host (or multiple hosts), and the steps required to reach that state. They can hold a series of tasks, or can just define a set role (more on Roles below).
An example playbook that just uses tasks can be found below.
---
- name: Update all CentOS hosts
hosts: centos
become: True
tasks:
- name: Running yum update
yum:
name: "*"
state: latest
In order, this defines:
- The description of the playbook.
- Which hosts in the inventory to run this playbook on. In this case, the
centos
group. - Whether this needs to be run with root privileges.
- The list of Tasks to carry out.
An example of a Playbook that uses a pre-defined role is below.
---
- hosts: mysql
roles:
- mysqlserver
This would apply the role mysqlserver
to all hosts in the mysql
group.
Roles
While it is possible to write a giant playbook that does everything you want, you’ll eventually want to re-use sets of tasks in other playbooks, leading to lots of large files with identical code. Roles allow you to cut down you playbooks to smaller subsets of tasks that are easier to manage.
For example, you could write a playbook to install Wordpress on a server.
---
- hosts: wordpress
roles:
- harden_os
- mysqlserver
- nginx
- wordpress
- letsencrypt
In the above example, 5 separate roles are called. Each of these will contain a list of playbooks that allow them to carry out their function.
This appraoch means that any time you need to install a webserver on a machine, you call the nginx role, etc. You don’t need to copy all of the tasks into a new playbook.
This lets you keep your eye on the big picture, which is the aim of the game when orchestrating your infrastructure.
How I’m using Ansible
I’m using Ansible both at home and work, with some crossover in functionality between them.
At Home
In the homelab, I have a dedicated Ansible host. This virtual machine contains all of the playbooks and roles that are required for the day-to-day running an maintenance of my infrastructure. These activities can be broken down into the following areas:
Patching
I have playbooks that are run every day to ensure all of my Linux boxes are kept up-to-date with their particular package manager.
An example of one of these scripts is below:
---
- name: Update all CentOS hosts
hosts: centos
become: True
tasks:
- name: Running yum update
yum:
name: "*"
state: latest
Configuration and Content management
Configuration
I also host an implementation of Jitsi Meet, an open-source videoconferencing application. I use Ansible to make sure that the Jitsi configuration files have certain variables set to values that I provide. If I play around with the configuration files, or make a mistake that breaks the functionality of Jitsi, I just re-run the playbook that describes how Jitsi should be configured. Once this is done, I have a working configuration again.
I’m also able to spin up my another version of my Jitsi server, confident that it is configured exactly like my original. From here, I can play around with configuration options, and if I want to keep the changes, I tell Ansible what those changes are.
The next time Ansible runs over my production Jitsi install, it changes the relevant configuration options, then restarts the service. This is really useful after a large update to an application that would require mass changes to a configuration file. I can do it in a development environment, then push to production with no effort at all.
Content
I provide web hosting to one of my friends, and he manages his site through GitHub. By leveraging Ansible, I’m able to make sure that every 15 mins, the webserver picks up the latest master release of his site, deploying it automatically. If anything were to happen to his hosting virual machine, I can wipe it, then retarget it with Ansible.
Ansible is smart enough (because I told it) to know that this box is going to be a web server, so it needs apache installed. It knows my the correct domain names my friend uses for his sites (again, because it’s in the Ansible playbook I wrote), so it configures apache to use those domains. Finally, it reaches out to his GitHubpage, pulling the latest master of his site and deploying it.
The top-level playbook is here:
- name: Deploy friend's site with apache.
hosts: friend_site
vars:
webserver_vhosts_root: /var/www
server_domain_name: friendsite.co.uk
apache_vhosts_filename: friendsite.co.uk.conf
roles:
- role: friend-site
- name: Deploy other site with apache.
hosts: friend_site
vars:
webserver_vhosts_root: /var/www
server_domain_name: website.friendsite.co.uk
apache_vhosts_filename: website.friendsite.co.uk.conf
roles:
- role: other-site
I can even share my Ansible code with him, and he can use it to deploy his web environment in his own development box. He could even use it to deploy his site to another hosting provider!
At work
At work, I have four Linux boxes that I maintain. These virtual machines are primarily used for Penetration testing, so I needed a way to manage them all effectively.
Tool availability
Using Ansible, I can guarantee that the same software is installed on all four of my boxes. I can also guarantee that they are configured the same way. Or, I can ensure that specific configuration options are passed to the appropriate box (such as regional IP addressing). If a new tool gets added to a box, it’s a simple task to add it to an Ansible playbook. After 15 minutes, all the boxes have the tool.
Command execution
This is a functionality that doesn’t really match the state-driven ethos of Ansible.
It is entirely possible to run Bash commands on devices via Ansible. It’s not recommended to use this to manage configuration files, or to install/remove software though. I use it to carry out activities that need to be run concurrently across all boxes.
An example of this is to carry out a network scan of the IP addresses reachable from each box. The results of these scans are filtered down according to what services are up, then another scanner checks for default usernames and passwords for each box running a set service.
This happens on every box, at the same time. This can then be scheduled, with the Ansible host reporting any errors, if they occur. This could be handled via Bash scripting, but if you’re already using Ansible, you may as well keep using it!
Future implementation plans
Ideally, I’d like to take each of the services in my Homelab, and create an Ansible playbook or role for them. This would give me peace of mind that if one of my servers dies, I can be up and running in a much shorter time than if I had to restore everything from scratch manually.
Managing my services with Ansible also provides me with the opportunity to play around with redundancy and load-balancing, as getting a new server configured and added to a pool can be carried out automatically. This is how companies like Google and Amazon work, so why should I be any different?
And who knows? The next Ansible playbook I work on might be the one that deploys this blog and keeps it up-to-date!