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, such as defining when to execute an action, defining when to stop an action, branching the control flow, and controlling action results.
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 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__linux_armhfv6.zip" ... when: ansible_facts.architecture == 'armv7l' - name: Set vars when architecture is x86_64 set_fact: consul_src_file: "consul__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 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
- name: Create fstab entry lineinfile: path: /etc/fstab line: ": nfs defaults,soft,bg,noauto,rsize=32768,wsize=32768,noatime 0 0" regex: ":" state: present validate: /usr/sbin/mount -fa -T %s
Assertations are actions expressed with
assert. You define one or multiple conditions with
that, and you can define a custom
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
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 already installed - only updating config files when: result is search("Consul v")
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: .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 already installed - only updating config files when: result is search("Consul v") - name: Get consul binary ... always: - name: Copy consul config template: src: consul.config.hcl.j2 dest: "/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
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
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.