Ansible is all about playbooks. In the "Ansible - Getting Started" article, we already had a very brief look at our first playbook. In this article, I will explain the structure of playbooks and provide some best practices in writing playbooks.
First things first. The term "playbook" comes from coaches and trainers in American Football. These playbooks often contain playing strategies, play moves and general step-by-step instructions. An Ansible playbook is basically the same, but meant for servers, switches, cloud providers, etc.
For this tutorial, I am using a single Fedora 34 machine, which will act as the Ansible node and the managed node at the same time. In an upcoming article I will demonstrate more complex setups for Ansible.
You can spin up a VM for this tutorial with Vagrant and the below Vagrantfile. If you don't know about Vagrant, you can check out "Vagrant - Getting Started". Every other Fedora 34 machine will work, too.
After saving the above code as a new Vagrantfile, you can spin up the machine with
vagrant up and enter it with
Vagrant ssh. You can install Ansible in the Vagrant machine as described in the article "Ansible - Getting Started" via
sudo dnf install ansible.
I will provide an automated solution in a future article, too. But let's focus on playbooks for now.
Now let's see how a playbook is structured. I will examine each of the sections and provide some meaningful example code. We will end up with a playbook, that can be used to install a fully working Apache httpd server and an example website.
If you just want to get started with Ansible, I recommend to start simple. Yes, there are very exhaustive and sophisticated examples out there and even the Ansible documentation recommends some layouts. In fact, you only need an inventory and a playbook. Since we are using Ansible on "localhost", we can also skip the inventory for now.
I recommend adding 2 more directories for this example and start with a boilerplate like described below.
In some future articles, I will show more complex examples. In case you want to dig deeper right now, please have a look here.
Every playbook starts with a header. The most simple example can be something like this.
This simple statement is called a pattern in Ansible, and it can be used to indicate where the playbook will be executed. The pattern can be a hostname, a group of the inventory and much more.
For our example, I will add a name for the playbook. So that it looks like this.
You can adjust all kinds of stuff in the header section like the execution strategy, the remote user, the fact gathering behavior, etc. Please have a look at the possible playbook keywords in the documentation.
Tasks, Pre-Tasks, Post-Tasks
The most important section of a play are the task sections. In the below example, you can see how you can facilitate them.
In many cases, you will only need the
tasks: section, but it's good to know, that you can have a lot of control over the execution order. This come in very handy for tasks like enabling or disabling the monitoring, sending notification mails or putting something in/out of a load balancer.
The tasks sections contain a list of tasks, which are executed in the given order. The below example is an update to our Playbook, so it is easier to understand how tasks are working.
The above example contains three tasks in the task section. The first task uses the
name: task keyword, whereas the second and third task also have another keyword
become:, which is used to control if a task needs privileges. You can adjust every task on its own with task keywords, which are documented here.
The second line of each task is calling a module. In the above example, we are using 3 modules, namely ansible.builtin.debug, ansible.builtin.package and ansible.builtin.service. Each of them behaves differently and does different things. Currently, there are thousands of modules available to control all kind of things. You can manage packages, render template files, spin up an AWS instance and much more.
Modules are maintained in collections, and you can get an overview of all officially maintained collections here. The collections beginning with (the namespace) "ansible" or "community" are somewhat special. These are either maintained directly from the Ansible developers or the Ansible community developers. In our example above we used tasks from the "ansible.builtin" collection.
To get a better understanding of modules, tasks and playbooks you can think of:
- modules are simple tools to do one thing, like a hammer
- tasks are instruction steps how the hammer must be used
- playbooks are the step-by-step instruction how to build a furniture
Finally, lets run the playbook. To see how it is working.
# Execute the playbook ansible-playbook playbook.yml
Handlers are something, you may need to run event driven tasks. They are working mostly like tasks, but will not be executed in a regular order, but based on notifiers.
For example, it is good practice to restart a service/application if it was updated. Let's refactor our example slightly.
In the above example, I added the handlers section and changed the task "Manage httpd Package". The package module supports the update of packages (
state: "latest"), which will ensure that the package is installed and up-to-date. With the task keyword
notify: "some handler name", you can trigger a handler. This handler will be executed only if the task has the state "changed".
You can execute the playbook again, and you will see that the handler is not triggered. On a fresh installation or if there is an update available, you will see an additional task.
# Execute the playbook ansible-playbook playbook.yml
You may wonder why I am providing an example where the enablement and starting of a service is not a handler, too. Let's assume our playbook looks like the below example.
You can find this kind of examples all over the place. The problems here are:
- httpd will not be restarted after updates
- if httpd is installed, but the service crashed, it will not be started or enabled
There is another section possible in Ansible playbooks, which is not used very often recently, but I want to show it anyway.
Roles will be executed before tasks and after pre_tasks. In a future article, I will cover the role development and usage of roles in more details. For now, please feel free to read about roles in the official Ansible documentation.
Ansible provides a lot of documentation for the usage of plays and playbooks. Please check out the below links.
Writing playbooks is an easy way to automate common tasks and structure them in a meaningful way. You can use pre_tasks, post_tasks and handlers to provide even more logic to your playbooks and react to events or trigger some mandatory tasks at the beginning.
Now that this topic is out of the way, we can have a look at variables, conditionals, roles and ways of integration in future articles.