Tricks Of The Trades

Ansible – Playbook Concepts

Pinterest LinkedIn Tumblr

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.

  1. – hosts: webservers
  2. vars:
  3. http_port: 80
  4. max_clients: 200
  5. remote_user: root
  6. tasks:
  7. – name: ensure apache is at the latest version
  8. yum: name=httpd state=latest
  9. – name: write the apache config file
  10. template: src=/srv/httpd.j2 dest=/etc/httpd.conf
  11. notify:
  12. – restart apache
  13. – name: ensure apache is running (and enable it at boot)
  14. service: name=httpd state=started enabled=yes
  15. handlers:
  16. – name: restart apache
  17. service: name=httpd state=restarted

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.

  1. – hosts: webservers
  2. vars:
  3. http_port: 80
  4. max_clients: 200
  5. remote_user: root
  6. tasks:
  7. – name: ensure apache is at the latest version
  8. yum:
  9. name: httpd
  10. state: latest
  11. – name: write the apache config file
  12. template:
  13. src: /srv/httpd.j2
  14. dest: /etc/httpd.conf
  15. notify:
  16. – restart apache
  17. – name: ensure apache is running
  18. service:
  19. name: httpd
  20. state: started
  21. handlers:
  22. – name: restart apache
  23. service:
  24. name: httpd
  25. state: restarted

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.

  1. – hosts: webservers
  2. remote_user: root
  3. tasks:
  4. – name: ensure apache is at the latest version
  5. yum: name=httpd state=latest
  6. – name: write the apache config file
  7. template: src=/srv/httpd.j2 dest=/etc/httpd.conf
  8. – hosts: databases
  9. remote_user: root
  10. tasks:
  11. – name: ensure postgresql is at the latest version
  12. yum: name=postgresql state=latest
  13. – name: ensure that postgresql is started
  14. service: name=postgresql state=started

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).

The 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.

For example:

  1. – hosts: webservers
  2. remote_user: root

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.

  1. – hosts: webservers
  2. remote_user: root
  3. tasks:
  4. – name: test connection
  5. ping:
  6. remote_user: scarlz

The become and 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.

  1. – hosts: webservers
  2. remote_user: scarlz
  3. tasks:
  4. – service: name=nginx state=started
  5. become: yes
  6. become_method: sudo

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.

Using become_user similarly allows a change of user from within the play’s SSH user shell session.

  1. – hosts: webservers
  2. remote_user: scarlz
  3. become: yes
  4. become_user: postgres

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:

  1. – hosts: webservers
  2. vars:
  3. http_port: 80
  4. max_clients: 200
  5. remote_user: root
  6. tasks:
  7. – name: make sure apache is running
  8. service: name=httpd state=started

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.

Note: The command and 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 name is 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.

  1. tasks:
  2. – name: make sure apache is running
  3. service: name=httpd state=started

The command and 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.

  1. tasks:
  2. – name: disable selinux
  3. command: /sbin/setenforce 0

The command and 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:

  1. tasks:
  2. – name: run this command and ignore the result
  3. shell: /usr/bin/somecommand || /bin/true

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.

  1. tasks:
  2. – name: Copy ansible inventory file to client
  3. copy: src=/etc/ansible/hosts dest=/etc/ansible/hosts
  4. owner=root group=root mode=0644

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:

  1. – hosts: webservers
  2. vars:
  3. vhost: blog-site.conf
  4. remote_user: root
  5. tasks:
  6. – name: create a virtual host file for {{ vhost }}
  7. template: src=somefile.j2 dest=/etc/httpd/conf.d/{{ vhost }}

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.

  1. – name: template configuration file
  2. template: src=template.j2 dest=/etc/foo.conf
  3. notify:
  4. – restart memcached
  5. – restart apache

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:

  1. handlers:
  2. – name: restart memcached
  3. service: name=memcached state=restarted
  4. – name: restart apache
  5. service: name=apache state=restarted

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 playbook.yml.

  1. $ ansible-playbook playbook.yml -f 10

The -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”.

The --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 ansible-pull script.

Run the help option to see details on this if interested.

  1. $ ansible-pull –help

As an aside, some say that Ansible playbook output is vastly upgraded if the cowsay package is installed on your system.

  1. # Debian / Ubuntu
  2. $ sudo apt-get install cowsay
  3. # Arch Linux
  4. $ sudo pacman -S cowsay

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 /vars directory.

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 .tpl or .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:

  1. <VirtualHost *:80>
  2. ServerAdmin [email protected]
  3. DocumentRoot {{ doc_root }}

     

  4. <Directory {{ doc_root }}>
  5. AllowOverride All
  6. Require all granted
  7. </Directory>
  8. </VirtualHost>

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:

  1. – name: Alter the default Apache virtual host
  2. template: src=vhost.tpl dest=/etc/apache2/sites-available/000-default.conf

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.

  1. rolename
  2. – files
  3. – handlers
  4. – meta
  5. – templates
  6. – tasks
  7. – vars

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.

Write A Comment