In this segment, you will learn how to set up and use Ansible to manage servers efficiently.
A tutorial series on building a complete home lab system from the ground up that is beginner-friendly, versatile, and maintainable.
What is Ansible
Ansible is an administrative tool that aims to make server management easier by using declarative and idempotent configuration files.
Declarative VS Imparative
Declarative means you are expressing the what without the how, this makes things easier to read and make sense of. Below is a contrived example of the difference:
Declarative
- The file
settings.conf
should include the linesome_setting=true
Imparative
- Check if
settings.conf
exists - Check if
settings.conf
includes the linesome_setting
- If not, add the line
some_setting=true
- Else check if the value of
some_setting
istrue
- If not, change the line to
some_setting=true
Idempotent
For declarative to work, the operations must be idempotent.
In simple terms, idempotent configurations don’t need to know about the current state, they must handle all possible starting states and ensure that whatever state is declared will be the final state upon completion. This means that you can define the state you want, and applying the same configuration again shouldn’t make any changes that aren’t needed.
For example, if we want to ensure that a package is installed, applying the configuration should either install the package if it isn’t already, or do nothing at all.
Note: You may see a warning in the Ansible documentation for some plugins about not being idempotent, this means you have to handle state checking yourself, this is pretty rare but worth mentioning
Benefits
Picture this; something is failing on your server, and you’ve spent hours combing over configuration files and scanning logs trying to figure out why and get it working again. In this case you’re treating your server as a pet, it’s special and irreplaceable. Wouldn’t it be better if you could treat servers as livestock - expendable and easily replaced?
For example; something is failing on your server, so you just re-apply the configuration. It’s fast and easy, and if that won’t cure it, you can put the poor thing down, spin up a new one, and copy over any mutable data. Sure, you still need to investigate, but you’re no longer under pressure because the service is up and running on a new host or virtual machine.
Another benefit is that backups will be faster, cheaper, and easier to manage if you only need to back up your data. There is no need to back up an entire server when all the configuration you need is already tucked away in a git repository. Save all that pricey off-site storage space for your mutable data instead of filling it with the entire contents of a hard drive.
Setup
First, you should install Ansible, on most distributions you can use pip
to install it, but there are some
more detailed instructions
if you need them.
Create a folder to use, I went with one called ansible
in my home directory, but you can keep it and call it whatever you like, you’ll just have to keep that in mind when creating the Ansible config file.
Now we need a configuration file called ansible.cfg
, there are a lot of configuration options for Ansible and we’re only using a few here, you should check out the Ansible documentation as well, they did a great job with that.
An enterprise automation platform for the entire IT organization, no matter where you are in your automation journey
The inventory file will list all of your hosts, we will get into that and roles in a bit, first let’s address that vault password file.
Ansible Vault is a utility for securely storing sensitive information, like passwords and such. The “vault” files are encrypted so they can be safely stored in version control, like GitHub for example.
Create the file ~/.ansible_vault
(you can call this something different if you want, just change it in ansible.cfg
to match), in this file you should enter a very long random string, save it in your password manager or write it down and keep it somewhere safe.
Once that is done, working with vaults will be seamless, you won’t be asked to enter the password when working with vault files, Ansible will read it from that file automatically.
Inventory
Create the file inventory.yml
, here we will define all hosts managed by Ansible, and optionally some global and host-specific variables. Don’t go crazy with variables here, there are better ways to store host-specific variables.
Vaults and Variables
Let’s create a global vault first, if we wanted unencrypted variables we could use the same filename without Ansible Vault, it would work the same way.
Now we can add global variables that are secure, we can do the same for other host groups like webservers
, etc. in the inventory file.
For host variables, we just put the file in a different place.
Here we can add variables for third_host
.
Playbooks
Now we can write playbooks to configure one or more of the hosts in our inventory. Let’s start with third_host
as a simple example.
We’ll start with main.yml
, I like to have one main file and import tasks from other files to keep things easy to parse, but you can also put the tasks inline under the tasks
key instead.
Note: tags
can be used to only run specific sets of tasks, e.g. ansible-playbook ~/ansible/playbooks/third_host/main.yml --tags firewall,some_other_tag
. And tasks tagged with never
will only run if another tag on that task is specified in the commands tag list.
Then create the task for configuring UFW, this is a good example of looping and list handling in Ansible; anything defined in with_items
will be looped over in the rule and each item in the list will be available as {{ item }}
.
Note: the line become: true
means “run this task with sudo
”
Files
The files
directory is for, well, files. These are static files, maybe certificates, or configuration files although it usually makes more sense to use lineinfile
to manipulate the configuration file that exists on the remote host, again check out the Ansible documentation, lots of good stuff in there.
Another example of a task, this one ensures the directory /etc/docker
exists, and ensures the file daemon.json
exists and matches the copy we have locally. The {{ inventory_dir }}
variable is one of many built-in variables in Ansible, and points to the inventory file path, in this case that would be ~/ansible/files/third_host/docker/daemon.json
.
Templates
Templates are for dynamic files, very similar to files, but you can use Ansible variables in the files, and they should have a .j2
(Jinja2) file extension.
Here is an example template for a Traefik config file that uses the variable admin_email
.
Then we can use that template in a task, and any variables will be replaced by their files in the output file.
Roles
This is where Ansible gets really powerful really quickly, roles are re-usable sets of playbooks that can be applied to multiple top-level playbooks. There is also a public repository of roles called Ansible Galaxy, where you can share your roles or use roles created by others.
Creating a Role
There is a handy command to scaffold out a new role for Ansible, let’s try that out now.
We now have a bunch of new files ready to go for our new role, these are pretty well documented by Ansible, but here’s a quick overview:
The variables in the defaults
directory can be overwritten in other places like the playbook and host or group variables, as you might expect.
You’ll see a lot of patterns repeated here because a role is kind of like a self-contained playbook of its own.
Handlers
One new directory that is significant here is handlers
, these are like tasks, but a handler is run once at the end of the role execution. This is useful for things like restarting services when configuration files change, if three configuration files are changed and they are all tied to the same handler, the service will restart once after all the changes were made.
We can now call that handler from a task using the notify
key, in this case we are using lineinfile
to make sure certain configuration lines are set the way we want them to be.
Assign Roles to a Playbook
Now all we need to do is declare any roles we want to be applied in our playbook.
A Few More Neat Tricks
I’ve only just scratched the surface of what Ansible can do here, and I’ve got a couple more necessities to share, but there is so much more! I encourage you to poke around and experiment!
Register
Register lets you assign a variable dynamically, for example if you wanted to know if a task changed or was already applied.
You can then use that variable in a later task. In this case we want to force recreate the Traefik container if the configuration was changed, otherwise only recreate it if Docker thinks it’s necessary.
You can also use these dynamic variables with the when
key.
Logging
Arguably the most important thing to know is how to log things out to the console! Fortunately, that’s pretty easy.
Summary
Now you’ve seen what Ansible is capable of, or at least a glimpse of it, and how to get started. I hope you found this useful!
If you want to see a working example I’ve set up a demo playbook that runs a Traefik instance for Docker apps, but the catch is you might have to do a little research to get it working, let me know how you do.