Playbooks are written in YAML like the configuration files and are the basis for Ansible’s configuration management and en-masse multi-machine deployment.
These are very powerful not only for declaring server configurations but also to orchestrate steps of any manual ordered process, even when the different steps must bounce back and forth between sets of machines in any order, as playbooks can launch tasks synchronously or asynchronously as required.
While it’s suitable to use the
/usr/bin/ansible program for ad-hoc commands and tasks. Playbooks are better kept in source control and used to push out larger configurations, or assure the configurations of your remote systems are still in check.
1 – Playbook Examples
Here are some initial examples of Ansible playbooks. A playbook by definition is composed of one or more plays in a list.
The goal of each play is to map a group of hosts to some well-defined role, through actions within the play Ansible calls tasks. Put simply though, a task is nothing more than a call to a preset Ansible module.
By composing a playbook of multiple ‘plays’, it is possible to orchestrate multi-machine deployments. Running certain steps on all machines in the “webservers” group, then certain steps on the “databases” server group, and then more commands back on the “webservers” group, etc.
The first snippet and playbook sets up Apache webserver on hosts in the
webservers group. Notice the variables declared, remote user in use, and handlers towards the end.
This second example uses tasks that have really long parameters, and modules that take many parameters. When this is the case you can break tasks items over multiple lines to improve the playbook readability and structure. Breaking up tasks is achieved by using YAML dictionaries to supply the modules with their key=value arguments.
This final example contains not just one but two plays. The first targets the
webservers host group and the second targets the
databases host group.
See the ansible/ansible-examples GitHub repository for some fully-fledged and properly structured Playbook configuration examples.
2 – Playbook Hosts and Users
For each “play” in a playbook, you get to choose the servers/hosts in your infrastructure you want to target, and which remote Linux user to complete the steps in the play as (remember the steps are called tasks).
hosts line at the start of a play is a list of one or more groups, or if needed a host pattern. The
remote_user holds the name of the target Linux/Unix user account.
Remote users can also be set per task instead of just for the entire play. Such as in this example where the task
test connection uses a different user to the overall play.
become_method items allow a change of privileges from within the SSH user’s session. This is useful for using shell programs like
sudo for tasks that need higher privileges.
In these cases you must provide the necessary password using
--ask-become-pass when running the playbook itself with
ansible-playbook from the command line – covered later on.
become_user similarly allows a change of user from within the play’s SSH user shell session.
Let’s look more at the tasks themselves and what they can do.
3 – Playbook Tasks
As seen each play contains its own list of tasks. Tasks are executed in order, one at a time, against all machines matched by the host group or pattern. So keep in mind that all hosts are going to receive and process the same task directives issued.
Also when running a playbook, any hosts that end up with failed tasks are taken out of the rotation for the entire playbook run. If things fail, simply correct the playbook file (or host connectivity issue) and rerun it.
As seen in the first section of this post, the goal of each task is to execute a module, with set variables passed to that module if needed.
Here is an omitted example:
Importantly though module use should also be “idempotent”. That is to say, when running a module multiple times in a sequence, it should have the same end result as if you had run it only once. One way to achieve this state of idem-potency in playbooks is to have a module check whether its desired final state has already been achieved, and if that state has been achieved, to instead exit without performing any actions.
So if all the modules a playbook uses are idempotent, then the playbook itself is likely to be idempotent too (overall), so re-running the playbook multiple times should be safe and not create any issues.
shell modules will typically rerun the same command issued again, which is fine if the command is something that does not change the outcome of its initial execution when run multiple times. There is also a “creates” flag available here which can be used to make these two modules idempotent.
Furthermore, every task should have a
name, which is included in the output from running the playbook, and serves as more of a description than a moniker. Due to this, It is useful to provide good descriptions for each task step.
Note: If a
nameis not provided, the string fed to ‘action’ will be used for output.
As with most modules, the
service the module takes arguments in the key=value format.
shell modules are the only modules that take a list of arguments and don’t use the key=value form as normal.
This makes them work in the same way as you would expect them to on the command line e.g.
shell module care about return codes, so if you have a command where the successful exit code is not zero, you can alter it to this:
If the action line of the module is getting too long, you can break it on a space character and indent any continued lines to improve readability.
Variables declared can be explicitly used in task action lines, not just in templates (templates not covered yet).
The variable is called using the word
vhost inside of the curly braces, in the example below:
To carry on the concept and practice introduced here of idem-potency in Ansible. The next section talks about handlers.
4 – Playbook Handlers
Modules should be idempotent as described in the last section, so they can relay back whether they have made a change on the remote system or not. Playbooks have the ability to recognize these changes and also have a basic event system that can be used to respond to change.
notify actions are triggered at the end of each block of tasks in a play, and will only be triggered once. Even if notified and triggered by multiple different tasks. For instance, multiple resources may indicate that Apache needs to be restarted because they have changed a config file, but Apache will only be restarted once to avoid multiple unnecessary restarts.
Here’s an example of restarting two services in a task when the contents of a file change, but only if the file changes. The items listed in the notify section of the task are called handlers.
Handlers are lists of tasks (not really any different from regular tasks) that are referenced by a globally unique name, and are notified by notifiers. If a handler is never referenced/notified it will not run. As inferred earlier, regardless of how many tasks notify a handler, it will run only once, and after all of the tasks complete in a particular play.
Here’s an example of a
handlers section with some defined tasks:
5 – Running Playbooks
Now that you’ve learned much of the groundwork for playbook syntax, we should look at how to run a completed playbook.
To run a playbook use the
anisble-playbook command followed by the path to the playbook file. In this scenario it’s in the current working directory and named
-f 10 specifies the usage of 10 simultaneous Ansible processes at once.
When executing a playbook there will always be a summary of the nodes/hosts that were targeted and how they fared with the instructions. General failures and fatal unreachable attempts by the playbook execution are kept separate in the “counts”.
--verbose flag tagged onto the
ansible-playbook command results in a more detailed account of the results of a playbook run. Furthermore the
--list-hosts flag shows which hosts are to be affected by a playbook before you run it.
It’s possible to invert the architecture of Ansible, forcing nodes/hosts check in to a central location instead of pushing configuration out externally, by using the
Run the help option to see details on this if interested.
As an aside, some say that Ansible playbook output is vastly upgraded if the
cowsay package is installed on your system.
7 – Ansible Templating
Templates in Ansible are constructed using the Jinja2 Python templating language and reside in their own files. They are capable of referencing variables from outside sources, such as from within the playbook using the template and the outer Ansible file inventory e.g. a
main.yml file within an upper
Templates are typically useful for setting up configuration files, to make them and other files more versatile or reusable among playbooks. They can have any file name but the
.j2 extensions are common.
Below is a template example for setting up an Apache virtual host. It uses a variable for setting up the document root on each Ansible host:
The inbuilt module
template: is used to apply a template file in a task. For example, if you named a template file
vhost.tpl and you placed it inside the same directory as your playbook, this is how you would make use of the template to replace the default Apache virtual host, on your Ansible hosts:
8 – Roles in Playbooks
Roles work best for more complex playbooks that have many multiple but related tasks. Each role contains only the relevant data and information needed to carry out said tasks. As there are usually multiple stages to a task or set of tasks, playbooks can get very lengthy and congested with all their operations. So confining tasks to roles helps alleviate this problem.
As an example, a playbook that installs Nginx, likely involves adding the stable package repository, then installing the package itself, as well as setting up all the configuration for the webserver. The final configuration step no doubt requires extra data such as variables, files, dynamic templates, and more. These are bet kept in their own role’s area.
The file-system for a role looks like the below, where
rolename is the root directory, and the options are the subsequent directory areas for each piece of information.
Within each of the individual directory areas above, Ansible will search for a
main.yml file automatically. Any every piece of configuration for the role in question is contained within these locations.
After understanding how each of these different concepts work together to create a playbook. You can go on to make your own from scratch, from other examples, or by converting your older setup scripts.