Bookmark this page

Remediating Issues with Ansible Playbooks

Objectives

After completing this section, students should be able to read and interpret an existing Ansible Playbook, and run it to configure hosts as specified by the plays and the current Ansible inventory.

Ansible Playbooks

While you can perform a simple, one-off task with an ad hoc command, the true power of Ansible automation is in using Ansible Playbooks. A playbook is a text file, written in YAML syntax, that is used to automate something: the initial configuration of a system, the deployment of a simple or complex application, the remediation of a security issue or misconfiguration. You use the ansible-playbook command to run the playbook and perform the automated tasks.

Each playbook contains one or more plays. A play is a list of tasks that should be performed on particular hosts. That list is executed in order, and the tasks generally ensure that the hosts are in a particular configuration. There may be other settings associated with a play, such as the values of variables or whether or not to escalate privileges to root for these tasks.

Complex automation of something may require that you perform a list of tasks on one system, and another list of tasks on another set of systems. These separate lists can be executed on different hosts in a number of ways, but one easy way is to use two separate plays in the same playbook. You run the playbook, which performs both plays to meet the playbook's overall automation goal.

Each play and each task can be given a name to help document what it is meant to accomplish. This can make the intent of the playbook and final configuration clearer, and if a task fails during playbook execution make it clearer what went wrong.

Important

It's best not to think of an Ansible Playbook as a script. Think of each play in a playbook as a checklist, and the playbook itself being a list of checklists to perform in order.

Likewise, it can be misleading to think of tasks as "Install this software" or "Configure that thing" unconditionally, in the way a shell script would. Instead, a task is supposed to make sure the system is in a certain state: "Is this software installed?", or "Does that file have the right configuration?" If it is not, Ansible is supposed to change the system so that it is in that state. Otherwise, Ansible notes that the task is complete already and move on.

The advantage of this is that Ansible makes it easier to create a well-written playbook that can safely be run a second time against a list of hosts. If the hosts have already been correctly configured, Ansible should do nothing.

Reading Ansible Playbooks

A playbook is a text file written in YAML format, and is normally saved with the extension .yml. It usually starts with the characters --- on the first line, although Ansible does not enforce this. The playbook primarily uses indentation with space characters to indicate the structure of its data. YAML doesn't place strict requirements on how many spaces are used for the indentation, but there are two basic rules.

  • Data elements at the same level in the hierarchy (such as items in the same list) must have the same indentation.

  • Items that are children of another item must be indented more than their parents.

A playbook is a list of plays. In YAML, a list is a set of items with the same indentation that start with a single dash (-) followed by a space.

- apple
- orange
- grape

A playbook is simply a YAML list of plays. But unlike this example, each play's item in the list is more than a simple string. Instead, each play is a collection of key-value pairs. A simplified play in a playbook might be structured like the following example:

- name: A simplified play
  hosts: webservers
  tasks:
    - first
    - second
    - third

This is a playbook which has one play in its list.

The play has three keys: name, hosts, and tasks. The keys have the same indentation because they're all play-level directives. The name is a label for the play. The hosts are the managed hosts on which to run the play. The tasks are a list of tasks to run for the play in order.

This isn't yet a valid playbook. You must replace "first", "second", or "third" with some valid task definitions. Here is a simple example of a real playbook, containing a play with one task.

Example 2.1. A Simple Playbook

---
- name: Configure important user consistently
  hosts: server1.example.com
  tasks:
    - name: newbie exists with UID 4000
      user:
        name: newbie
        uid: 4000
        state: present

The first line of the preceding play starts with a dash and a space (indicating the play is the first item of a list), and then the first key, the name attribute. The name associates an arbitrary string with the play as a label. This identifies what the play is for. The name key is optional, but is recommended because it helps to document your playbook. This is especially useful when a playbook contains multiple plays.

- name: Configure important user consistently

The second key in the play is a hosts attribute, which specifies the hosts against which the play's tasks should be run. Like the argument for the ansible command, the hosts attribute takes a host pattern as a value, such as the names of managed hosts or groups in the inventory.

  hosts: server1.example.com

Finally, the last key in the play is the tasks attribute, whose value specifies a list of the tasks to run for this play. This example has a single task which runs the user module with specific arguments (to ensure user newbie exists and has UID 4000).

  tasks:
    - name: newbie exists with UID 4000
      user:
        name: newbie
        uid: 4000
        state: present

The tasks attribute is the part of the play that actually lists, in order, the tasks to be run on the managed hosts. Each task in the list is itself a collection of key-value pairs.

In this example, the only task in the play has two keys:

  • name is an optional label documenting the purpose of the task. It's a good idea to name all of your tasks to help document the purpose of each step of the automation process.

  • user is the module to run for this task. Its arguments are passed as a collection of key-value pairs, which are children of the module (name, uid, and state).

Interpreting Tasks

Ansible playbooks are meant to be human-readable, meaning that you should be able to determine what a module will do when executed. Ansible comes with a command called ansible-doc that can be used to list all modules on a system and display documentation for each.

The following example shows an excerpt of the output of ansible-doc user, which looks up documentation for the user module.

[user@demo ~]$ ansible-doc user
> USER    (/usr/lib/python2.7/site-packages/ansible/modules/system/user.py)

        Manage user accounts and user attributes. For Windows targets,
        use the [win_user] module instead.

OPTIONS (= is mandatory):

...output omitted...

= name
        Name of the user to create, remove or modify.
        (Aliases: user)

...output omitted...

- state
        Whether the account should exist or not, taking action if the
        state is different from what is stated.
        (Choices: absent, present)[Default: present]

...output omitted...

- uid
        Optionally sets the `UID' of the user.
        [Default: (null)]

...output omitted...

You can get a list of all modules installed on your system with ansible-doc -l.

The Ansible documentation website also has online documentation of all the modules Ansible currently ships.

Executing Ansible Playbooks

The ansible-playbook command is used to run a playbook. The command is executed on the control node and the name of the playbook to be run is passed as an argument.

The following example executes the simple playbook shown earlier in this section that ensures the newbie user exists and has UID 4000.

[user@demo project1]$ ansible-playbook playbook.yml
SSH password: password
SUDO password[defaults to SSH password]: 

PLAY [Configure important user consistently] *******************************

TASK [Gathering Facts] *****************************************************
ok: [server1.example.com]

TASK [newbie exists with UID 4000] *****************************************
changed: [server1.example.com]

PLAY RECAP *****************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0   

Note that the name set for each of your plays and tasks is displayed when the playbook is run. (The Gathering Facts task is a special task that usually runs automatically at the start of a play.) For playbooks with multiple plays and tasks, name attributes make it easier to monitor the progress of a playbook's execution.

The newbie exists with UID 4000 task reports it "changed" for server1.example.com. This means that the task changed something on that host to ensure its specification was met. In this case, it means that the user probably did not exist or had the wrong UID.

In general, tasks in Ansible playbooks are idempotent, and it is safe to run the playbook multiple times. If the targeted managed hosts are already in the correct state, no changes should be made. For example, assume that the playbook from the previous example is immediately run again:

[user@demo project1]$ ansible-playbook playbook.yml
SSH password: password
SUDO password[defaults to SSH password]: 

PLAY [Configure important user consistently] *******************************

TASK [Gathering Facts] *****************************************************
ok: [localhost]

TASK [newbie exists with UID 4000] *****************************************
ok: [localhost]

PLAY RECAP *****************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0   

This time, no changes were made to server1.example.com.

Note

Before running a playbook, it is a good practice to test it with the --syntax-check option to check it for syntax errors:

[user@demo project1]$ ansible-playbook --syntax-check playbook.yml

playbook: playbook.yml

If the command does not find any errors, it prints the playbook's file name as shown in the preceding example. Otherwise, it reports the errors.

You can also use the --check option to actually connect to the managed hosts and attempt a dry run of the playbook. It won't make any changes, but attempts to identify changes that would be made. (This may not be accurate, since any changes that an actual run would make but the check run did not make could affect the behavior of subsequent tasks.)

Plays with Multiple Tasks

Most plays have more than one task. For example, in the preceding section we looked at the following ad hoc command to enable the http predefined service group for the default zone for firewalld:

[user@demo project1]$ ansible webservers \
> -m firewalld -a "service=http permanent=true state=enabled immediate=true"

As a single task play, it might look like this:

---
- name: Apache HTTPD is set up and running
  hosts: webservers
  tasks:
    - name: firewall allows HTTP service
      firewalld:
        service: http
        permanent: true
        state: enabled
        immediate: true

It would not be difficult to add additional tasks to the play to make sure the httpd package is installed and the httpd service is started and enabled.

That longer three-task playbook might look like this:

---
- name: Apache HTTPD is set up and running
  hosts: webservers
  vars:
    httpdsoftware: httpd
  tasks:
    - name: webserver software is installed and up to date
      package:
        name: "{{ httpdsoftware }}"
        state: latest 

    - name: firewall allows HTTP traffic 
      firewalld:
        service: http
        permanent: true
        state: enabled
        immediate: yes

    - name: Apache HTTPD is started and enabled
      service:
        name: httpd
        state: started
        enabled: true

In the preceding example, a vars section for the play that sets a httpdsoftware variable to the value httpd. There are many ways Ansible may set variables, including in the inventory on a host-by-host basis, and in general in the play itself.

The first task of the play uses the package module that a particular RPM package is installed. Its name attribute uses a Jinja2 template to replace the variable httpdsoftware with its value as the name of the package, httpd.

The second task is the original task to make sure firewalld is configured to accept traffic for the HTTP service.

The third task uses the service module to ensure the httpd service is started and enabled to start at boot time.

Using Handlers to Trigger Tasks on Changes

One advanced feature that you should know about when reading playbooks is handlers. A handler is a special task in a play that only runs if it is notified. A regular task can be configured to notify a handler by name if it changes something on the system. If multiple tasks notify the same handler, it still only runs once, at the end of the play.

This is useful when configuring network services. For example, you may have a playbook that ensures a certain line in a network service's configuration file is set. If the task to do that makes a change, you may want to restart the service. A regular task to check that the service is running may find that the service is already running (with the wrong configuration) and do nothing. If you instead used a regular task to always restart the service, any time you run the play the service will be restarted, even if nothing changed! A handler lets you avoid this situation by only restarting the service on a change.

The following modification of the playbook above ensures that the Apache HTTPD configuration replaces any default DirectoryIndex index.html line in /etc/httpd/conf/httpd.conf with DirectoryIndex custom.html. It also notifies the handler Restart HTTPD to restart the service if it had to change the configuration file.

---
- name: Apache HTTPD is set up and running
  hosts: webservers
  vars:
    httpdsoftware: httpd
  tasks:
    - name: webserver software is installed and up to date
      package:
        name: "{{ httpdsoftware }}"
        state: latest 

    - name: DirectoryIndex is custom.html
      lineinfile:
        path: /etc/httpd/conf/httpd.conf
        regexp: 'DirectoryIndex index.html'
        backrefs: yes
        line: 'DirectoryIndex custom.html'
      notify:
        - Restart HTTPD

    - name: firewall allows HTTP traffic 
      firewalld:
        service: http
        permanent: true
        state: enabled
        immediate: yes

    - name: Apache HTTPD is started and enabled
      service:
        name: httpd
        state: started
        enabled: true

  handlers: 
    - name: Restart HTTPD
      service:
        name: httpd
        state: restarted

Playbooks with Multiple Plays

A playbook may contain more than one play. This can be very useful when orchestrating a complex deployment which may involve different tasks on different hosts. A playbook can be written that runs one play against one set of hosts, and when that finishes runs another play against another set of hosts.

The following example shows a simple playbook with two one-task plays, one which adds a user to server1.example.com and a second that configures a firewall for the systems in the webservers host group.

---
- name: Configure important user consistently
  hosts: server1.example.com
  tasks:
    - name: newbie exists with UID 4000
      user:
        name: newbie
        uid: 4000
        state: present

- name: Apache HTTPD is set up and running
  hosts: webservers
  tasks:
    - name: firewall allows HTTP service
      firewalld:
        service: http
        permanent: true
        state: enabled
        immediate: true

Important

This section provided you with an overview of how to read simple Ansible Playbooks. But there's a lot more to learn about things like conditional features, various kinds of task loops, error handling and recovery, variables and facts, and much more.

Later in this course you will find out about some of the tools that provide existing Ansible Playbooks to help with mitigating and resolving security issues. This section has prepared you to analyze and use those Playbooks to help you better manage the security of your operational environments.

If you are interested in learning more about Ansible, there are many other resources available, including various Red Hat Training courses and online resources at https://docs.ansible.com.

References

ansible(1), ansible-playbook(1), ansible-doc(1) man pages

For more information, refer to the Working With Playbooks chapter in the Ansible user guide at: https://docs.ansible.com/ansible/latest/user_guide/playbooks.html

Revision: rh415-7.5-b847083