Chapter 4. Flow Control

Thus far, you’ve got most of the knowledge needed to operate Ansible correctly. You know how to define tasks, manage hosts, and execute tasks.

In the following chapters of the book, you’ll learn Ansible concepts that will enable you to create Ansible playbooks that are more compact, reusable, and readable.

In the current chapter, we’ll focus on how to change the flow of the execution of a task depending on certain situations like an error, a condition, or executing multiple times a task.

Loops

In some cases, you might want to execute the same task with minor variations multiple times in the same play or retry a failing task various times to self-correct any temporary errors.

To cite a few examples of loops include:

  • Copying multiple files.

  • Creating users.

  • Installing multiple tools.

  • Executing the same command multiple times with different arguments.

Similar to any programming language, there are special keywords to identify loops.

Loop

In Ansible, you can iterate over any variable of type list, a list of hashes, or a dictionary. Some of these variables can be explicitly created, for example, a list of programs to install, but others are implicit, like the inventory hosts list.

Ansible will set in each iteration the current value in a variable named item.

Let’s start with a simple example. Supposing you need to install Java 17 and Apache Maven into a host. With what you’ve learned so far in this book about Ansible, you could write something like:

tasks:
  - name: Install Java 17
    dnf: name=java-17-openjdk-devel
  - name: Install Maven
    dnf: name=maven

That’s one valid way, but with loops, you could rewrite the playbook to something more compact:

tasks:
  - name: Install Packages
    dnf: name={{ item }} 1
    loop: [ 'java-17-openjdk-devel', 'maven'] 2
1

Run dnf command against the value of the variable item

2

Loops through the array of elements, setting value in the item variables

Both playbooks are equivalent, but notice that the last one is more compact and maintainable; to add new packages, only add a new element to the list.

Tip

In this example, you set the list directly in the loop as constant, but a custom variable could be used and set in any of the ways explained in the previous chapter.

It’s possible to use a list of hashes referencing subkeys in the item variable by adding a dot and appending the key name.

The following example copies files from the local directory to managed hosts:

- name: Add several users
  - name: copy files
      copy:
        src: "{{item.src}}"
        dest: "{{item.dest}}"
  loop:
    - { src: 'ngnix.conf', dest: '/etc/nginx/conf.d/localhost.conf' }
    - { src: 'hosts.conf', dest: '/etc/hosts' }

To loop over a dictionary you need to transform the variable as a list of dictionaries instead of a dictionary by calling the dict2items filter:

- name: Add several users
  ansible.builtin.user:
    name: "{{ item.name }}"
    state: present
    groups: "{{ item.groups }}"
  loop: "{{ userinfo | dict2items }}" 1
  vars:
    userinfo: 2
      name: alex
      groups: root
1

Transform the userinfo dictionary into a list of dictionaries

2

Defines the variable as a dictionary

There are some cases; for example, when using nested loops, you might need to rename the item variable to another name to not overwrite the value of the inner and outer loops continuously.

To rename the variable to another name instead of item, use the loop_var directive. The following example changes the variable from item to my_item:

  loop: [ 'java-17-openjdk-devel', 'maven']
  loop_control:
    loop_var: my_item 1
1

Renames the loop variable

Registering variables

In the previous chapter, you’ve seen how to capture task output using the register keyword. As a reminder:

tasks:
  - name: Echo var
    ansible.builtin.command: /bin/echo "Hello World {{msg_name}}"
    register: response
  - name: Print result
    debug:
      var: response

But when looping, Ansible executes the task multiple times, so what’s the value of the registered variable when the loop finishes?

When using loops, the result structure changes to accommodate the multiple results. The variable will contain a results section of the type array, with the first position of the array, the result of the first iteration, and so on.

Let’s write a playbook that iterates through a list of elements, executes an echo command, and registers the result to print it in the console finally:

- name: Echoing in loops
  hosts: all
  tasks:
    - name: Echo var
      ansible.builtin.command: /bin/echo "Hello World {{item}}" 1
      loop: [ 'Ada', 'Aixa'] 2
      register: response  3
    - name: Print result
      debug:
        var: response
1

echo task definition

2

Loop with two elements

3

Registers the output of each loop

Run the playbook with a proper inventory file to get the content of the response variable registered within a loop:

ansible-playbook -i inventory playbook-loop.yaml

And you’ll get the following output:

ok: [192.168.1.115] => {
    "response": {
        "changed": true,
        "msg": "All items completed",
        "results": [
            { 1
                "ansible_loop_var": "item",
                "changed": true,
                "cmd": [
                    "/bin/echo",
                    "Hello World Ada"
                ]
                ...
            },
            { 2
                "ansible_loop_var": "item",
                "changed": true,
                "cmd": [
                    "/bin/echo",
                    "Hello World Aixa"
                ],
                ...
            }
        ],
        "skipped": false
    }
1

Result of first iteration

2

Result of second iteration

During the iteration, the result of the current item is placed in the registered variable.

Pausing

Sometimes, you might need to pause the execution between each element in a loop. There is the pause directive in the loop_control section to set the number of seconds to wait until the following element is processed:

loop: [ 'Ada', 'Aixa']
loop_control:
  pause: 5 1
1

Waits 5 seconds before process the following item of the list

Indexing

It’s possible to track the current index of iteration using the index_var directive to specify the variable name to contain the current loop index.

tasks:
  - name: Echo var
    ansible.builtin.command: /bin/echo "Hello World from element number {{idx}}" 1
    loop: [ 'Ada', 'Aixa']
    loop_control:
      index_var: idx 2
1

Prints the index of the current element in the remote node

2

Defines idx as tracking variable

Special variables

Any list variable is susceptible to being iterated in a loop.

In Ansible there is a group of special variables that represent the list of hosts. Even though you can use them in any situation, they fit perfectly in loops and conditionals to execute a task over a specific set of hosts.

Some of these variables are:

ansible_play_batch

A list of hostnames that are in scope for the current batch of the play.

ansible_play_hosts

The list of all hosts still active in the current play.

groups

A list of all the groups (and hosts) in the inventory; you can enumerate all hosts within a group using the array construction groups['all'].

The following playbook prints all hosts belonging to the therock group:

- name: Show therock the hosts in the inventory
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ groups['therock'] }}" 1
1

Returns the hosts of therock group.

Apart from host variables, some variables are only available within a loop to get extended loop information, such as its first iteration, the last iteration, or the number of elements. To access these variables, set the extended directive to true in the loop_control section.

loop_control:
  extended: true

You can then use the following variables:

ansible_loop.allitems

The list of all items in the loop.

ansible_loop.index

The current iteration of the loop 1-based.

ansible_loop.index0

The current iteration of the loop 0-based.

ansible_loop.revindex

The number of iterations from the end of the loop 1-based.

ansible_loop.revindex0

The number of iterations from the end of the loop 0-based.

ansible_loop.first

True if first iteration.

ansible_loop.last

True if last iteration.

ansible_loop.length

The number of items in the loop.

ansible_loop.previtem

The item from the previous iteration of the loop. Undefined during the first iteration.

ansible_loop.nextitem

The item from the following iteration of the loop. Undefined during the last iteration.

So far, you’ve iterated over a task looping over a list of elements; in the following section, you’ll learn how to iterate over a task until a condition is met.

Retrying Tasks until a Condition is Met

You can assume that if an Ansible task worked in the past, it will work every time if nothing has changed. That’s true until there are external factors you don’t control, such as network connectivity. For example, a task to install a package in the managed host succeeds now because the package manager is up and running, and in two days, the task can fail because, at that time, the package manager server is down.

Usually, these errors are temporal glitches; after some seconds, everything works as expected. To automatically fix these errors, Ansible lets you retry a task until a condition is met.

The following snippet retries a task until the output of the task executed on the managed host contains the word succeeded. At most, five retries with a delay of 10 seconds between each attempt are executed:

- name: Retry a task until a certain condition is met
  Ansible.builtin.command: ...
  register: result 1
  until: result.stdout.find("succeeded") != -1 2
  retries: 5 3
  delay: 10 4
1

Output is registered

2

If the result of any attempt has succeeded in its stdout, task is not retried

3

Task runs up to 5 times

4

A delay of 10 seconds between each attempt

Tip

To see the results of individual retries, run the play with -vv.

This approach is valid in idempotent modules, in the case of none-idempotent modules a retry might leave the system in an unstable state. Check module documentation to know if it is idepotent or not, and which side effects might have the reexecution of the module.

In this section, you’ve learned how to execute a task multiple times; in the following section, you’ll see how to control whether Ansible executes a task.

Conditionals

Ansible provides conditionals to evaluate whether a task, role, or import should be executed.

Suppose you need to install a new package. For example, if you are managing multiple hosts with different operative systems, Fedora and Debian, you might need two different tasks, one to execute dnf and another one to execute apt-get. The first task is executed only in hosts with Fedora installed, while the later task is executed only in hosts with Debian installed. How do you filter the execution of a task by the operative system?

Ansible offers the when directive to evaluate the task execution. It supports all Jinja2 standard tests and filters in conditionals. Conditional expressions can contain Ansible facts, registered variables, or Ansible variables to compare with other values.

First Example with a Conditional

Let’s take the problem introduced at the beginning of this section and write a playbook that installs a package using the correct package manager:

- name: Install ngnix
  hosts: all
  become: true 1
  tasks:
  - name: install nginx in Fedora
    dnf:
      name: nginx
    when: ansible_facts['distribution'] == "Fedora" 2
  - name: install nginx in Debian
    apt:
      name: ngnix
    when: ansible_facts['distribution'] == "Debian"
1

Becoms root to install package

2

Gets Ansible fact and compares with a string

Execute the following command to execute the playbook:

ansible-playbook -i inventory -K playbook-facts.yaml

The execution of the playbook shows the execution of the Fedora task:

...
TASK [Gathering Facts]
ok: [192.168.1.115]

TASK [install nginx in Fedora]
ok: [192.168.1.115] 1

TASK [install nginx in Debian]
skipping: [192.168.1.115] 2
...
1

Runs task in Fedora

2

Skips Debian

You can store Ansible facts as variables to use for conditional logic. This is useful for creating readable playbooks, especially when a lot of facts are used. Let’s rewrite the previous example setting a fact as a variable:

- name: Install ngnix
  hosts: all
  become: true
  tasks:
  - name: Get the distribution
    set_fact: 1
      distribution: "{{ ansible_facts['distribution'] }}" 2
  - name: install nginx in Fedora
    dnf:
      name=nginx
    when: distribution == "Fedora"
  - name: install nginx in Debian
    apt:
      name: ngnix
    when: distribution == "Debian"
1

Sets facts as variables

2

Get distribution fact and set it as var

Conditionals are very intuitive, and you can use them in multiple situations. In this section, you’ll explore some examples.

Using Conditionals

So far, you’ve seen conditionals with Ansible facts. Let’s learn how to use conditionals with registered variables. Then, we’ll show you how to use conditionals with standard variables.

Conditionals with registered variables

Often, in a playbook, you want to execute or skip a task based on the outcome of an earlier task. For example, you may want to check if a configuration file exists, and if not, copy it, or depending on the content of a file, execute or not a task.

The following playbook verifies if a file is present and, if not, copies from the local directory:

- name: Check file
  hosts: all
  tasks:
  - name: Check if file already exists
    command: ls /home/alex/application.properties
    register: file_exists
    ignore_errors: yes 1
  - name: Copy conf file for the app
    copy:
      src: application.properties
      dest: /home/alex/application.properties
    when: file_exists is failed 2
1

Ansible interrupts a play if the command fails. With this option, Ansible continues executing the rest of the tasks

2

If the previous task fails, it executes this one

Executing the playbook, you get the following output:

TASK [Gathering Facts]
ok: [192.168.1.115]

TASK [Check if file already exists]
fatal: [192.168.1.115]: FAILED! => {"changed": true, "cmd": ["ls", "/home/alex/application.properties"], "delta": "0:00:00.011441", "end": "2024-01-25 13:39:25.941884", "msg": "non-zero return code", "rc": 2, ...
...ignoring 1

TASK [Copy conf file for the app]
changed: [192.168.1.115] 2
1

The task fails because the file doesn’t exist

2

But the playbook is not terminated, and the file is copied

Apart from registered variables, you use standard variables in conditionals, too.

Conditionals with standard variables

As with registered variables, you can also use variables you define on the playbooks in conditionals. In the following example, Ansible executes a task depending on the boolean value set in a variable:

- name: Check file
  hosts: all
  vars:
    special: false
  tasks:
  - name: Run the command if not special
    ansible.builtin.shell: echo "Not special"
    when: not special

The task is executed by default as the variable value matches the conditional expression. You can skip the execution of the task by setting the special variable to true, for example, overriding from the command line with the --extra-vars option:

ansible-playbook -i inventory --extra-vars="special=true" playbook-var.yaml

So far, you’ve used conditions to control task execution, but you can use them in imports, includes, or rules.

Conditionals with reusable tasks

You can use conditionals with reusable task files, playbooks, or roles.

Conditions in imports

When you add a conditional to an import statement, Ansible applies the condition to all tasks defined in the imported file.

For example, to import a playbook only if the host_type variable is set to db, you can write the following playbook:

- hosts: all
  tasks:
    - import_tasks: db_tasks.yaml
      when: host_type == 'db'

Notice that all tasks defined in the db_tasks.yaml will implicitly have the when: host_type == 'db' condition.

Conditions in includes

When you add a conditional to an include statement, the condition is applied only to the include task itself and not to any other tasks, in contrast to what you’ve seen in import.

The same example rewritten with include looks like:

- hosts: all
  tasks:
    - include_tasks: db_tasks.yaml
      when: host_type == 'db'

Conditions in roles

You’ve not seen roles in depth yet, but it’s an introduction. In chapter 8, you’ll learn more about roles, but for now, remember that you can add conditionals to decide whether to apply for a role or not.

For example, applying a role only when the host type is db:

- hosts: all
  roles:
     - role: geerlingguy.mysql 1
       when: host_type == 'db'
1

Role fully qualified name

Jinja2

As mentioned at the beginning of this section, Ansible uses Jinja2 expressions in conditionals. Let’s explore some of the most used operators when using conditionals.

Comparisons

==

Compares two objects for equality.

!=

Compares two objects for inequality.

>

True if the left-hand side is greater than the right-hand side.

>=

True if the left-hand side is greater or equal to the right-hand side.

<

True if the left-hand side is less than the right-hand side.

True if the left-hand side is less or equal to the right-hand side.

Some examples:

name == 'Natale'
age >=18
country != 'Unknown'

Logic

and

Return true if the left and the right operand are true.

or

Return true if the left or the right operand is true.

not

Negate a statement (see below).

(expr)

Parentheses group an expression.

Some examples:

name == 'Elder' and age >=18

Special Operators

in

Perform a sequence/mapping containment test.

is

Performs a test.

|

Applies a filter.

Some examples:

num_hosts in [1, 2, 3]

x is defined

test_list = ['192.24.2.1', 'host.fqdn', '192.168.32.0/24', ...]
test_list | ansible.netcommon.ipaddr 1
1

Only shows IP addresses (192.24.2.1, 192.168.32.0/24)

In the following section, let’s explore the concept of handlers and why they are important in the Ansible ecosystem.

Handlers

So far, tasks are always executed in the defined order. You can skip executing tasks by using conditionals. But there are also special tasks are executed only if triggered by another task at the end of a play.

These special tasks are handlers and typically are used to start, reload, restart, and stop services, among any other use cases you might need.

For example, you may need to restart a service if a task updates the configuration of the service but not if nothing changes.

The handlers section in the YAML document sets the tasks that should be executed if triggered. The notify directive is used to trigger the handlers.

Let’s create a simple playbook for updating a webpage hosted in Nginx and restarting the service:

- name: Update page
  hosts: all
  become: true
  tasks:
    - name: copy index.html
      copy:
        src: index.html
        dest: /usr/share/nginx/html/index.html
        mode: 0644
      notify: 1
        - Restart nginx 2
    - debug:
        msg: 'Page updated'
  handlers: 3
    - name: Restart nginx 4
      ansible.builtin.service: 5
        name: nginx
        state: restarted
1

Notify a handler in case the task succeeds

2

Name of the handler to invoke

3

Registration of handlers

4

Registration of handler with a name

5

The task to execute

Run the playbook with the following command:

ansible-playbook -i inventory -K playbook-handler.yaml
BECOME password:

PLAY [Update page]
...

TASK [copy index.html]
changed: [192.168.1.115] 1

TASK [debug]
ok: [192.168.1.115] => {
    "msg": "Page updated"
}

RUNNING HANDLER [Restart nginx] 2 3
changed: [192.168.1.115]
1

Copie the file and notify to handler

2

Handlers are executed after all tasks

3

The Copy task changed the host, invoking the handler

It’s important to note that if the task triggering the handler doesn’t change the host, there is no invocation of the handler.

You can specify a list of handlers in the notify directive so all of them are notified in case of a change.

Handlers are executed in the order defined in the handlers section, not in the order listed in the notify statement. This is something important to be aware especially if handlers need to be executed in order.

Tip

Our advice is not to have inter-dependencies between handlers.

Notifying the same handler multiple times will result in executing it only once, regardless of how many tasks notify it.

Grouping handlers

As you’ve seen previously, you named the handlers, and you refer to them by its name. This is a good strategy when there are not many handlers, but when you have a large list of handlers, and some of them are notified together, it’s a good idea to keep them together in a group.

To create a group, use the listen directive when registering a handler. All handlers having the same name in the listen section belong to the same group and can be triggered together.

Let’s see a simple example of notifying multiple handlers without and with using listen.

This first example doesn’t use the listen directive:

  tasks:
    - name: copy index.html
      copy:
        src=index.html
        dest=/usr/share/nginx/html/index.html
        mode=0644
      notify: 1
        - Restart nginx
        - Restart Infinispan
  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted
    - name: Restart Infinispan
      ansible.builtin.service:
        name: infinispan
        state: restarted
1

Two handlers are triggered

In this second example, we create a “group” named restart web services using the listen directive:

  tasks:
    - name: copy index.html
      copy:
        src=index.html
        dest=/usr/share/nginx/html/index.html
        mode=0644
      notify: "restart web services" 1
  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted
      listen: "restart web services" 2
    - name: Restart Infinispan
      ansible.builtin.service:
        name: infinispan
        state: restarted
      listen: "restart web services" 3
1

Task notifies to the group

2

This handler listens for the restart web services trigger

3

This handler is executed together with the previous one

When having multiple handlers triggered together, it’s a good idea to use listen to keep them grouped.

Flushing handlers

Ansible executes handlers after task execution. This approach is perfect because the handler only runs once, regardless of how many tasks notify it.

But in some situations, you might want to flush handlers in the middle of a play.

Ansible has meta tasks, a special task that can influence Ansible’s internal execution or state. One of these meta tasks is flush_handlers, which executes all triggered handler tasks. Moreover, it empties the queue of handlers to be executed; hence, if any other task triggers an already executed handler, it will be executed again.

To execute a meta task, use the meta directive:

tasks:
  - name: Copy index.html
    copy: ...

  - name: Flush handlers
    meta: flush_handlers 1

  - name: Copy configuration
    copy: ...
2
1

All handlers triggered in previous tasks are executed

2

All handlers triggered after the flush are executed

Important

Avoid using variables in the handler name. If a variable is not available, the entire play fails.

So far, we’ve seen how to control the flow of Ansible in typical cases. In the next section, you’ll see how to handle error situations?

Error Management

So far, we’ve run Ansible in a controlled way, with no failures and tasks completed correctly, but this is not always the case. Tasks might fail because the executed command fails, the managed host is unavailable, or the playbook definition contains a runtime error (i.e., variable not set).

In these cases, Ansible stops executing on that host and continues on other hosts. But this behavior might not fit in all situations; sometimes, a command failure is the expected result, or a failure in a host should stop executions to all hosts.

Ignoring Failed commands

When a command fails, Ansible stops the execution of further tasks on the failing host.

Using the ignore_errors directive in a task will make Ansible continue executing tasks despite the error.

Remember that the ignore errors directive only affects when the task returns a failure. In the case of Ansible, errors like undefined variables, connection issues, and so on will stop tasks regardless of the value of ignore_errors.

- name: Try to show content of not existing file
  ansible.builtin.command: cat /etc/a.txt
  ignore_errors: true
- name: Show the content of an existing file
  ansible.builtin.command: cat /etc/b.txt 1
  ignore_errors: true
1

Even though the first command fails, the second command runs too.

Tip

If you want to ignore the unreachable hosts error, use ignore_unreachable at the task or playbook level.

Defining Failure

From the point of view of Ansible, a task fails when the command it executes fails, and finishes with an error code.

Sometimes a command might notify the failure through a console message instead of the returning an error code (an integer with value greater than 0). Or the failure is based on a business logic output instead of a command failure. For example, a task should fail if the background color of a webpage is red; in this specific case, the curl command succeeds as the webpage is up and running, but the task should fail as the content is not.

The failed_when directive lets you define when a task should fail based on a registered variable containing the command result.

Usually, a variable with a command result contains:

  • The exit code (rc field)

  • If the host changed (changed field)

  • The executed command (cmd field)

  • The standard error output (stderr field)

  • The standard output (stdout field)

  • Many more …​

Let’s code a playbook failing when the webpage body content contains the word bye. To fetch the page, you’ll use the uri module, used to interact with HTTP and HTTPS web pages.

- name: Checks Site
  hosts: all
  tasks:
    - name: Fetch webpage
      uri: 1
        url: http://mysite.com
        return_content: true
      register: output 2
      failed_when: "'Bye' in output.content" 3
1

Uses uri module to fetch web content

2

Registers response in output var

3

Sets to fail the task if the result is an error or the page content contains the word bye

You can use any conditional expression in the failed_when directive, for example:

failed_when: output.rc == 0 or output.changed == true

failed_when: 1
  - result.rc > 0
  - "'No such' not in result.stdout"
1

By default and is used when multiple conditions

Defining Change

When a playbook finishes its execution, Ansible provides a report with statistics like executed tasks and failed tasks, …​

You can see the previous execution report in the following snippet:

PLAY RECAP 192.168.1.115              : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

What we are focused on in this section is the changed report. When an action (module/task) changes something, like installing a package on the managed host, it reports changed status to Ansible. On the other hand, if there are no changes, and this is left to the module to decide if there are changes or not, then it reports ok status.

But let’s think about our previous example; since the URL request returns a 200 status code, Ansible registers the task as an ok, but what if you know that calling this service, even though it returns a 200 status code, has changed the host?

Ansible lets you define when a particular task has changed a remote host using the changed_when conditional.

Redefine the previous example to report the call as changed instead of ok:

- name: Checks Site
  hosts: all
  tasks:
    - name: Fetch webpage
      uri:
        url: http://mysite.com
        return_content: true
      register: output
      changed_when: "output.status == 200" 1
1

Changes when status code is 200

And the report will show one task as changed:

Aborting a Play

When a task fails on a single host, all functions of other hosts continue their execution. But sometimes, you might want to ensure that all tasks are successful in all hosts, and if there is any problem in any of them, you want to stop the execution and not continue on other hosts.

In such cases, setting any_errors_fatal to true the task returns an error, then Ansible finishes the errored task on all hosts in the current batch and then stops executing the play on all hosts.

One of the use cases of this flag is creating a barrier between the execution of groups of hosts. For example, you could create a task executed against some hosts. Only if the execution of the task succeeds for all hosts, only then it can continue the execution with the rest of the tasks and hosts.

Let’s see this in a playbook:

- hosts: frontends
  any_errors_fatal: true 1

  tasks:
    - name: Publish under maintenance page 2
      copy: ....

- hosts: backends 3

  tasks:
    - name: Stop service
      service: ....

    - name: Update backend
      copy: ....

    - name: Start Service
      service: ....
1

If any any task fails in frontends host, the playbook execution is aborted

2

Copies maintenance page to frontend hosts

3

If all frontends have the maintenance page, then the update of the backend is executed

By default, Ansible will execute backend tasks even though a failure in frontend tasks is thrown. In this example, we used the any_error_fatal: true directive on frontend hosts to abort the process if there is an error. So, the process of rolling updates cannot start until the frontend has been updated with the maintenance page.

You can also abort the play when a certain threshold of failures has been reached using the max_fail_percentage directive. For example, setting ` max_fail_percentage: 50` will make Ansible stop execution if 50% of the tasks fail.

Blocks

Ansible playbooks can contain as many tasks as required for maintaining your infrastructure. However, maintaining playbooks with a huge amount of tasks might be challenging.

One of the options Ansible offers is grouping tasks in blocks.

Blocks create logical groups of tasks where all tasks inherit directives set at the block level. Any directive set at the block level does not affect the block itself; it is only inherited by the tasks enclosed by a block.

Blocks also offer a way to handle task errors at the block level and fix these errors in a single place.

To define a block, wrap the list of tasks belonging to a block under the block directive. All directives at the same level of block are inherited for all tasks defined within the block. For example, setting the become_user directive at the block level makes all tasks enclosed in the block run as the user set there.

Grouping Tasks with Blocks

Let’s create a block containing two simple tasks to update a webpage deployed in Ngnix. The first task copies the page into the Ngnix shared directory, and the second restarts the Ngnix service.

Moreover, the playbook contains a condition affecting the block execution. Only when the update_page variable is set to true the task is within the block will be executed.

- name: Web Servers tasks
  hosts: webservers
  tasks:
   - name: Update pages
     block: 1
       - name: copy index.html
         copy:
           src=index.html
           dest=/usr/share/nginx/html/index.html
           mode=0644

       - name: restart nginx
         service:
           name: nginx
           state: restarted
     when: update_page == true 2
1

Block defining two tasks

2

Condition is evaluated before Ansible executes tasks in the block

Let’s explore how to handle errors with blocks.

Error Handling with Blocks

You’ve already seen how to handle error situations in Ansible in the previous section.

Using blocks, you can control how Ansible responds to task errors by providing custom execution in case of errors.

Let’s abstract from Ansible (and YAML) to talk about how programming languages handle errors, and let’s pick Java.

Error handling syntax in Java uses the try/catch/finally form:

try { 1
    insertIntoDb();
} catch (Exception e) {
    exceptionLogic(); 2
} finally {
    closeConnection(); 3
}
1

Executes business logic

2

In case of an error executes this logic

3

Always executes this code after a try or after the catch

Ansible blocks let you reproduce similar construction but execute tasks instead of code. It uses two directives, rescue, equivalent to the catch clause in Java, and always, equivalent to the finally clause.

In the following snippet, you can see the structure of an Ansible block with error handling:

tasks:
   - name: Update pages
     block:
       - name: Task 1
         ...
       - name: Task 2
         ...
     rescue:
       - name: Exception logic
         ...
     always:
       - name: Executes after all
         ...
Tip

rescue and always are optional and can be used alone or together.

Let’s create a straightforward playbook that installs an invalid package, catches the error and finally executes some logic.

- name: Block example
  hosts: all
  tasks:
  - name: Attempt to install a package
    block:
      - name: install an invalid package
        dnf:
          name=qwehdjikn
    rescue:
      - debug:
          msg: 'Oh there is an error'
    always:
      - debug:
          msg: 'This always executes'

If you run this playbook, you get the following output:

ASK [install an invalid package] 1
fatal: [192.168.1.115]: FAILED! => {"changed": false, "msg": "This command has to be run under the root user.", "results": []}

TASK [debug] 2
ok: [192.168.1.115] => {
    "msg": "Oh there is an error"
}

TASK [debug] 3
ok: [192.168.1.115] => {
    "msg": "This always executes"
}
1

Executes a failing task

2

The rescue task catches the error

3

Task executed at the end of the block

Blocks offer a convenient way of dealing with errors in a similar way as programming languages do.

In this chapter, you’ve learned about flow control, executing a task multiple times, changing the input parameters, or the concept of blocks. The next chapter will cover how to manage files and resources, with a focus on templating.

Get Red Hat Certified Engineer (RHCE) Ansible Automation Study Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.