<aside> 🧭

Module 03 · Playbook Control Flow

Conditionals, loops, error handling, tags, imports and delegation — everything that turns a flat list of tasks into a program that reacts to what it finds.

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Modules 01 and 02. You already use when: and register — this module covers the full toolkit built on top of them.

</aside>


Part A · Conditionals

A1 · when: — the basics

<aside> 📖

Official docs: Conditionals · Tests

</aside>

<aside> ☂️

The analogy. Think of telling the whole family "take an umbrella, but only if it is raining". The instruction is identical for everyone — but each person looks out of their own window before deciding, and the cousin in another city quite reasonably leaves the umbrella at home. Nobody disobeyed you. They followed the instruction correctly and the answer was no.

That is when: — one condition, written once, but judged separately on every single server. Which is why skipping: in the output means the decision worked, not that something failed. Reading a wall of yellow skipping lines as a problem is one of the most common beginner mistakes.

</aside>

when: decides whether a task runs. It is evaluated per host, so the same task can run on web01 and skip on web02 in the same play.

- name: Install Apache on RedHat family only
  ansible.builtin.package:
    name: httpd
    state: present
  when: ansible_facts['os_family'] == "RedHat"

<aside> 🔑

when: is already a Jinja2 expression — do NOT wrap it in {{ }}.

The braces are redundant, ansible-lint flags them, and on complex expressions they can actively break evaluation. This is the single most common style error in beginner playbooks.

</aside>

Operators

when: app_port == 8080            # equality
when: app_port != 8080
when: app_port > 1024             # numeric comparison
when: ansible_facts['memtotal_mb'] >= 4096
when: "'nginx' in installed_packages"    # membership - note the quoting
when: "'web' in group_names"             # is this host in a group?
when: app_version is version('2.0', '>=')  # proper version comparison

Combining conditions

# A LIST is an implicit AND - all must be true. Preferred: it reads better in output.
when:
  - ansible_facts['os_family'] == "Debian"
  - ansible_facts['distribution_major_version'] is version('20', '>=')
  - deploy_enabled | bool

# Explicit and / or / not
when: ansible_facts['os_family'] == "Debian" and deploy_enabled | bool
when: env == "prod" or env == "staging"
when: not maintenance_mode | bool

# Parentheses when mixing and with or - and you MUST, precedence bites otherwise
when: (env == "prod" or env == "staging") and deploy_enabled | bool

<aside> ⚠️

and binds tighter than or. So a or b and c means a or (b and c), which is very often not what was intended. When you mix them, parenthesise — even where it is technically unnecessary, because the next person to read it will not do the precedence maths in their head.

</aside>

Tests — the is family

when: myvar is defined
when: myvar is not defined
when: myvar is none                       # explicitly null
when: result is succeeded                 # on a registered result
when: result is failed
when: result is changed
when: result is skipped
when: app_version is version('2.0', '>=')
when: path_result.stat.exists
when: some_list | length > 0
when: "'ERROR' in log_output.stdout"

🧪 Exercise A1.1 — Watch a conditional evaluate per host

---
- name: Conditionals are per-host
  hosts: all
  gather_facts: true
  tasks:
    - name: Only on Debian family
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} is Debian family"
      when: ansible_facts['os_family'] == "Debian"

    - name: Only on hosts with 4GB or more
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} has {{ ansible_facts['memtotal_mb'] }} MB"
      when: ansible_facts['memtotal_mb'] >= 4096

    - name: Only on hosts in the web group
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} is a web server"
      when: "'web' in group_names"