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
}}
loop
:
[
'
java-17-openjdk-devel
'
,
'
maven
'
]
Run
dnf
command against the value of the variableitem
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
}}
"
vars
:
userinfo
:
name
:
alex
groups
:
root
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
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}}"
loop
:
[
'
Ada
'
,
'
Aixa
'
]
register
:
response
-
name
:
result
debug
:
var
:
response
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"
:
[
{
"ansible_loop_var"
:
"item"
,
"changed"
:
true,
"cmd"
:
[
"/bin/echo"
,
"Hello World Ada"
]
...
}
,
{
"ansible_loop_var"
:
"item"
,
"changed"
:
true,
"cmd"
:
[
"/bin/echo"
,
"Hello World Aixa"
]
,
...
}
]
,
"skipped"
:
false
}
During the iteration, the result of the current item is placed in the registered variable.
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}}"
loop
:
[
'
Ada
'
,
'
Aixa
'
]
loop_control
:
index_var
:
idx
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']
}}
"
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
until
:
result.stdout.find("succeeded")
!=
-1
retries
:
5
delay
:
10
Output is registered
If the result of any attempt has succeeded in its stdout, task is not retried
Task runs up to 5 times
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
tasks
:
-
name
:
install
nginx
in
Fedora
dnf
:
name
:
nginx
when
:
ansible_facts['distribution']
==
"Fedora"
-
name
:
install
nginx
in
Debian
apt
:
name
:
ngnix
when
:
ansible_facts['distribution']
==
"Debian"
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
]
TASK
[
install
nginx
in
Debian
]
skipping:
[
192
.168.1.115
]
...
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
:
distribution
:
"
{{
ansible_facts['distribution']
}}
"
-
name
:
install
nginx
in
Fedora
dnf
:
name=nginx
when
:
distribution
==
"Fedora"
-
name
:
install
nginx
in
Debian
apt
:
name
:
ngnix
when
:
distribution
==
"Debian"
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
-
name
:
Copy
conf
file
for
the
app
copy
:
src
:
application.properties
dest
:
/home/alex/application.properties
when
:
file_exists
is
failed
Ansible interrupts a play if the command fails. With this option, Ansible continues executing the rest of the tasks
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
TASK
[
Copy
conf
file
for
the
app
]
changed:
[
192
.168.1.115
]
The task fails because the file doesn’t exist
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
when
:
host_type
==
'db'
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
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
:
-
Restart
nginx
-
debug
:
msg
:
'
Page
updated
'
handlers
:
-
name
:
Restart
nginx
ansible.builtin.service
:
name
:
nginx
state
:
restarted
Notify a handler in case the task succeeds
Name of the handler to invoke
Registration of handlers
Registration of handler with a name
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
]
TASK
[
debug
]
ok:
[
192
.168.1.115
]
=
>
{
"msg"
:
"Page updated"
}
RUNNING
HANDLER
[
Restart
nginx
]
changed:
[
192
.168.1.115
]
Copie the file and notify to handler
Handlers are executed after all tasks
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
:
-
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
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
"
handlers
:
-
name
:
Restart
nginx
ansible.builtin.service
:
name
:
nginx
state
:
restarted
listen
:
"
restart
web
services
"
-
name
:
Restart
Infinispan
ansible.builtin.service
:
name
:
infinispan
state
:
restarted
listen
:
"
restart
web
services
"
Task notifies to the group
This handler listens for the restart web services trigger
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
-
name
:
Copy
configuration
copy
:
...
All handlers triggered in previous tasks are executed
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
ignore_errors
:
true
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
:
url
:
http://mysite.com
return_content
:
true
register
:
output
failed_when
:
"
'Bye'
in
output.content
"
Uses
uri
module to fetch web contentRegisters response in
output
varSets 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
:
-
result.rc
>
0
-
"
'No
such'
not
in
result.stdout
"
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:
PLAYRECAP
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
"
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
tasks
:
-
name
:
Publish
under
maintenance
page
copy
:
...
.
-
hosts
:
backends
tasks
:
-
name
:
Stop
service
service
:
...
.
-
name
:
Update
backend
copy
:
...
.
-
name
:
Start
Service
service
:
...
.
If any any task fails in frontends host, the playbook execution is aborted
Copies maintenance page to frontend hosts
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
:
-
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
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
{
insertIntoDb
(
)
;
}
catch
(
Exception
e
)
{
exceptionLogic
(
)
;
}
finally
{
closeConnection
(
)
;
}
Executes business logic
In case of an error executes this logic
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
]
fatal:
[
192
.168.1.115
]
:
FAILED!
=
>
{
"changed"
:
false,
"msg"
:
"This command has to be run under the root user."
,
"results"
:
[
]
}
TASK
[
debug
]
ok:
[
192
.168.1.115
]
=
>
{
"msg"
:
"Oh there is an error"
}
TASK
[
debug
]
ok:
[
192
.168.1.115
]
=
>
{
"msg"
:
"This always executes"
}
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.