loading...
Cover image for Ansible: Influencing Action Execution with Conditions and Branches

Ansible: Influencing Action Execution with Conditions and Branches

admantium profile image Sebastian ・5 min read

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.

Discussion

pic
Editor guide