When you execute an Ansible playbook, all actions are run top-down, in the order they are written. There are cases when you need more control: You want to check a host that a certain software is installed, or you want to validate a certain condition and stop following actions when this condition is not fulfilled.
In this article, I will cover the following options of influencing action execution: defining when to execute an action, defining when to stop an action, branching the control flow, and controlling action results.
This article originally appeared at my blog.
Ansible Concepts
This article assumes you are familiar with the Ansible concepts. If you are not, here is a short definition:
- Playbook: A collection of plays
- Play: A sequence of actions or roles that are applied to a host
- Action: A concrete command to the host (installing packages, setting config file values)
- Role: A reusable collection of actions
Run Tasks on Conditions
To express conditions, any action can be augmented with the keyword when
. Conditions are written in Jinja2 templating language. A common use case is to check for the hosts OS distribution or architecture.
Here is an example from my infrastructure at home project. When installing Consul, I check if the host has an arm architecture (the raspberry Pi) or if it’s an x86_64 architecture. Based on this, I determine the correct path to download the required binary.
- name: Set vars when architecture is armv7l
set_fact:
consul_src_file: "consul_{{ consul_version }}_linux_armhfv6.zip"
...
when: ansible_facts.architecture == 'armv7l'
- name: Set vars when architecture is x86_64
set_fact:
consul_src_file: "consul_{{ consul_version }}_linux_amd64.zip"
...
when: ansible_facts.architecture == 'x86_64'
In some cases, you need to wait for a condition to be true. This can be expressed with the action wait_for
. For example, you have a playbook that orchestrated starting some software, and one task needs to wait until the software is running. You can check for the applications PID file to be present on the host - like this:
- name: wait for Nginx server to be running
command: /usr/sbin/sync_web_content
wait_for:
path: /var/logs/nginx.pid
state: present
Stop Tasks on Conditions
There are three commands to stop plays when certain conditions are not met: Validations, assertations and explicit fails. Think of them like checklist-points that need to be fulfilled.
Validations
Validations are commands specific to some modules, like templates or lineinfile. They invoke a command that receives the new file. If the return code of that command is anything other than 0, the validation is not successful.
In the following example, I'm adding a new entry to /etc/fstab
, and want to prevent that there is a mistake in the file. Consequences could be terrible, like a non-bootable system. With mount -f
, the system will try to mount all file systems, and I'm passing the new file with the flag -T %s
.
- name: Create fstab entry
lineinfile:
path: /etc/fstab
line: "{{ nfs_dir_server_ip }}:{{ nfs_dir_mnt_path }} {{ nfs_dir_mnt_path }} nfs defaults,soft,bg,noauto,rsize=32768,wsize=32768,noatime 0 0"
regex: "{{ nfs_dir_server_ip }}:{{ nfs_dir_mnt_path }}"
state: present
validate: /usr/sbin/mount -fa -T %s
Assertation
Assertations are actions expressed with assert
. You define one or multiple conditions with that
, and you can define a custom success_msg
or fail_msg
. In the following example, I'm checking that the OS family of the host is a Debian system, giving an error message if that is not the case.
- name: Check OS family
assert:
that:
- ansible_facts[inventory_hostname].ansible_os_family == 'Debian'
fail_msg: Host is not Debian, stopping installation
Explicit Fail
Finally, you can explicitly fail
when a condition is not met. Here is an example from my infrastructure at home project: I'm checking if the Consul program in a certain version is already installed, or stop the play immediately.
- name:
command: consul --version
register: result
failed_when: false
- name: Check if current consul version is installed
fail:
msg: Consul v{{consul_version}} already installed - only updating config files
when: result is search("Consul v{{consul_version}}")
Branching Control Flow with Blocks
So far, we have seen examples of plays that execute tasks based on conditions, or stop executing any other task. This is a good start, but sometimes you need even more control.
Blocks are the answer. In its basic form, a block encompasses a set of tasks. You can attach a when
clause to a block, and therefore provide a simple branching form similar to if-else
in programming languages.
Here is an example to install and configure the text editor nano if the variable software_package
is defined and when it equals nano.
- name: Install text editor
block:
- name: Install nano
apt:
name: nano
state: present
- name: Configure nano
template:
src: .nanorc.js
dest: {{user_home}}.nanorc
state: present
when: software_package is defined and software_package == 'nano'
- name: Install zsh
...
Blocks provide even more features: They can catch errors, and execute tasks in the error case or in any case. The first behavior is achieved with the rescue
clause: When an error occurs, tasks inside this block-like structure are executed. The second behavior is expressed with the always
clause: These tasks are always executed, independent of whether an error was raised or not.
An example from my infrastructure at home project is to check that a certain program and program version is installed. If it is, the installation actions, grouped as a block
, are not executed, but the configuration part, expressed in the always
part, is applied. I'm using this for the installation of Consul. Here is the relevant excerpt.
- block:
- name:
command: consul --version
register: result
failed_when: false
- name: Check if current consul version is installed
fail:
msg: Consul v{{consul_version}} already installed - only updating config files
when: result is search("Consul v{{consul_version}}")
- name: Get consul binary
...
always:
- name: Copy consul config
template:
src: consul.config.hcl.j2
dest: "{{ consul_base_directory }}/consul.config.hcl"
mode: 0644
notify: Restart consul
Controlling Task Results
Any action that is executed will have one of the following results: ok, changed, failed, skipped. You see these results at the end of executing an ansible playbook.
The changed and failed results can be defined with the changed_when
and failed_when
clause, a condition that is evaluated. I'm using the failed_when
clause when installing Consul: At the beginning, I execute the consul -version
command, an action that fails when Consul is not installed. However, I want the play to continue, so I explicitly set the failed_when
condition to false.
- block:
- name:
command: consul --version
register: result
failed_when: false
Conclusion
When you use Ansible, actions are executed linearly, from top to bottom. This article showed you how to influence this order. You can define explicit conditions when actions are executed. You can set validations and assertations that stop the play when they are not fulfilled. With blocks, you can group multiple actions together with a condition, and react on any errors that are thrown. And finally, you can determine when an action is considered to have changed or failed. With these options, you gain more influence when executing actions in Ansible.