Ansible - Playbooks

Ansible May 11, 2021

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.

Prerequisites

Hint
The guide is tested on Fedora 34 with Ansible 3.2.0.

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.

Optional:

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.

Vagrant.configure("2") do |config|

  # libvirt
  config.vm.provider "libvirt" do |libvirt|
    libvirt.cpus = 2
    libvirt.memory = 2048
  end

  # Fedora
  config.vm.define "node" do |node|
    node.vm.box = "generic/fedora34"
    node.vm.hostname = "node"
  end

end
Vagrantfile

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.

Playbook

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.

Directory Layout

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.

project/
|- files/
|- templates/
|- playbook.yml
directory layout

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.

---
- hosts: "localhost"
playbook.yml

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.

---
- name: "My first Playbook"
  hosts: "localhost"
playbook.yml

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.

---
- name: "My first Playbook"
  hosts: "localhost"
  
  pre_tasks:
  
    - name: "Output pre_tasks message"
      ansible.builtin.debug:
        msg: "Pre_tasks will run at the beginning."
        
  tasks:
  
    - name: "Output tasks message"
      ansible.builtin.debug:
        msg: "Tasks will run after pre_tasks and before post_tasks."
  
  post_tasks:
  
    - name: "Output post_tasks message"
      ansible.builtin.debug:
        msg: "Post_tasks will run at the end."
playbook.yml

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.

...

  tasks:
  
    - name: "Output tasks message"
      ansible.builtin.debug:
        msg: "Tasks will run after pre_tasks and before post_tasks."

    - name: "Manage httpd Package"
      ansible.builtin.package:
        name: "httpd"
        state: "present"
      become: true
      
    - name: "Manage httpd Service"
      ansible.builtin.service:
        name: "httpd.service"
        state: "started"
        enabled: true
      become: true
...
playbook.yml

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

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.

...

  handlers:
  
    - name: "Restart httpd Service"
      ansible.builtin.service:
        name: "http.service"
        state: "restarted"
      become: true

  tasks:
  
    - name: "Output tasks message"
      ansible.builtin.debug:
        msg: "Tasks will run after pre_tasks and before post_tasks."

    - name: "Manage httpd Package"
      ansible.builtin.package:
        name: "httpd"
        state: "latest"
      become: true
      notify: "Restart httpd Service"
      
    - name: "Manage httpd Service"
      ansible.builtin.service:
        name: "httpd.service"
        state: "started"
        enabled: true
      become: true
...
playbook.yml

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.

...

  handlers:
  
    - name: "Start & Enable httpd Service"
      ansible.builtin.service:
        name: "http.service"
        state: "started"
        enabled: true
      become: true

  tasks:
  
    - name: "Output tasks message"
      ansible.builtin.debug:
        msg: "Tasks will run after pre_tasks and before post_tasks."

    - name: "Manage httpd Package"
      ansible.builtin.package:
        name: "httpd"
        state: "latest"
      become: true
      notify: "Start & Enable httpd Service"
...
playbook.yml

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

Roles

There is another section possible in Ansible playbooks, which is not used very often recently, but I want to show it anyway.

---
- name: "My first Playbook"
  hosts: "localhost"
  
  pre_tasks:
  
    - name: "Output pre_tasks message"
      ansible.builtin.debug:
        msg: "Pre_tasks will run at the beginning."
        
  roles:
  
    - role: "rolename"
        
  tasks:
  
    - name: "Output tasks message"
      ansible.builtin.debug:
        msg: "Tasks will run after pre_tasks and before post_tasks."
  
  post_tasks:
  
    - name: "Output post_tasks message"
      ansible.builtin.debug:
        msg: "Post_tasks will run at the end."
playbook.yml

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.

Documentation

Ansible provides a lot of documentation for the usage of plays and playbooks. Please check out the below links.

Tips and tricks — Ansible Documentation
Working with playbooks — Ansible Documentation
Playbook Example: Continuous Delivery and Rolling Upgrades — Ansible Documentation
User Guide — Ansible Documentation

Conclusion

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.

Tags

Daniel Schier

Just a guy doing stuff. Mostly #FLOSS like #Linux, #Ansible, #Podman, #K8s, #Python, #Nextcloud or whatever comes next.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.