<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>
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>
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
# 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>
is familywhen: 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"