Skip to main content
RunBook Academy

AnsibleXV · Conditionals and LoopsConditionals

Conditionals that should have been groups

Advanced⏱ ~26 minansible-playbookansible-inventory

What you'll learn

  • Recognise a when: chain that encodes group membership
  • Refactor branching task logic into groups plus group_vars
  • Explain why --limit can target a group and cannot target a conditional
  • Choose between task when, inventory group and separate play for a given requirement

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Part XV closes with a design argument rather than a feature.

Here is a task from a real-shaped repository:

Service impact possiblethe thing this lesson is about
- name: Restart the collector on hosts that need the new agent
ansible.builtin.systemd_service:
  name: metrics-collector
  state: restarted
when:
  - ansible_facts.os_family == 'Debian'
  - ansible_facts.distribution_major_version | int >= 12
  - "'legacy' not in group_names"
  - inventory_hostname not in metrics_collector_exclusions
  - metrics_agent_version is version('2.0', '>=')

Five conditions. It parses, it reviews plausibly, and it restarts a service.

Now answer a question an operator must be able to answer before running it: which hosts does this restart?

You cannot. Not from reading it, not from --list-hosts, not from ansible-inventory. The only way to find out is to run it and read what happened afterwards — which is the wrong order for a task that restarts things.

The problem stated precisely

--limit and host patterns operate on inventory. They can express “the webservers group”, “everything in prod except db”, “hosts matching web*”.

when: operates on evaluation at task time, per host, after the play has already targeted that host, gathered its facts, and started running tasks against it.

So a decision written as when: is invisible to every targeting tool the course has taught you. ansible-playbook --list-hosts shows the hosts the play targets, not the hosts the task will act on. Your pre-flight blast-radius check reports a number that is not the number.

The refactor

Every condition in that chain answers a question about the host, not about the moment. Which means every one of them can be answered when the inventory is written.

Read-only / Safeinventory/production.yml
all:
children:
  metrics_collectors_v2:
    hosts:
      app01.example.com:
      app02.example.com:
      app03.example.com:
  metrics_collectors_legacy:
    hosts:
      app04.example.com:
      app05.example.com:
Service impact possiblethe task afterwards
- name: Restart the collector
hosts: metrics_collectors_v2
serial: 2
tasks:
  - name: Restart the collector
    ansible.builtin.systemd_service:
      name: metrics-collector
      state: restarted

The task now has no when: at all, and every tool works again:

Read-only / Safethe blast radius, before running anything
$ ansible-playbook --list-hosts restart-collectors.yml
playbook: restart-collectors.yml

play #1 (metrics_collectors_v2): Restart the collector	TAGS: []
  pattern: ['metrics_collectors_v2']
  hosts (3):
    app01.example.com
    app02.example.com
    app03.example.com
Read-only / Safeand the same answer from the inventory itself
$ ansible-inventory -i inventory/production.yml --graph metrics_collectors_v2
@metrics_collectors_v2:
|--app01.example.com
|--app02.example.com
|--app03.example.com

Three hosts. Stated before the run, verifiable by two independent tools, and available to --limit and serial.

Where the policy goes

Moving membership to groups leaves the values to group_vars, which is the layering rule from Part XIII doing exactly what it was designed for:

Read-only / Safegroup_vars/metrics_collectors_v2.yml
metrics_agent_version: '2.4.1'
metrics_collector_interval: 15
metrics_collector_endpoint: 'https://metrics.example.com/ingest'
Read-only / Safegroup_vars/metrics_collectors_legacy.yml
metrics_agent_version: '1.8.9'
metrics_collector_interval: 60
metrics_collector_endpoint: 'https://metrics.example.com/ingest-v1'

The role becomes unconditional. It reads its variables and applies them, and it does not know or care which environment it is in. That is the property that makes a role reusable, and it is why Part XXII treats role interfaces as a design question rather than a filing one.

When when: is still right

This is not an argument against conditionals. It is an argument about which kind of decision belongs where.

Keep when: for genuine runtime state. Something only discoverable on the host, at that moment:

- name: Reboot to activate the new kernel
  ansible.builtin.reboot:
  when: reboot_required.stat.exists

Whether a reboot flag file exists is not knowable when the inventory is written. That is a real conditional.

Keep when: for a result you just registered. Lesson 2’s territory. The value came from a task in this run.

Keep when: for a single, stable, mechanical fact. One os_family branch inside a role that must work on both families is fine, and pushing it into inventory would mean every consumer of the role maintaining a debian_hosts group, which is worse.

The test is the same as Part XIV lesson 7’s, applied to conditions rather than values:

Could you have known, before the run started, whether this task applies to this host?

If yes, it is group membership written in the wrong place.

The decision table

Three ways to express “this applies to some hosts and not others”:

Task when:Inventory groupSeparate play
Where the decision is visibleIn the task body, at run timeIn the inventory, before the runIn the playbook hosts: line
Can --list-hosts show the affected set?NoYesYes
Can --limit target it?NoYesYes
Does serial batch the right population?NoYesYes
Cost of adding a variantA branch in shared codeA file and a groupA play
Right forRuntime state, registered resultsHost properties and policyDifferent work, different targets

The third column deserves a word. A separate play is the right answer when the work differs, not just the values — when the Debian path and the RHEL path are genuinely different sequences rather than the same sequence with different parameters. Two plays with clear hosts: lines read better than one play with parallel branches throughout, and each gets its own honest blast radius.

Doing the refactor safely

The refactor is behaviour-preserving in intent and it is still a change to which hosts get touched, so prove it:

  1. Enumerate the current behaviour. Run the existing play with the conditional replaced by a debug that prints inventory_hostname when the condition is true. That gives you the real affected set, from the current logic, without changing anything.
  2. Build the group from that list. Not from your reading of the condition — from the output of step 1. The two differ more often than you expect, and the difference is the bug you are removing.
  3. Compare. ansible-inventory --graph <newgroup> against the list from step 1. They must match exactly.
  4. Check-mode the new play against a staging inventory and compare the changed set to a check-mode run of the old one.
  5. Delete the conditional and the exclusion variable. Leaving them in place “just in case” gives you two sources of truth, which is the condition you started with.

Step 1 is the one people skip and the one that finds things. A five-condition chain evaluated across 400 hosts frequently turns out to match a different set than anyone believed, and discovering that during a refactor is enormously cheaper than discovering it during an incident.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A task restarting a service is guarded by five conditions on facts and group membership. Why can an operator not determine the blast radius before running it?

  2. Q2. Which of these is a genuine runtime conditional that should stay as a when: rather than becoming a group?

  3. Q3. What does moving a targeting decision from a when: chain into an inventory group make possible? Select all that apply.

  4. Q4. Because the refactor is behaviour-preserving, it is safe to build the new group directly from reading the conditions rather than from observing which hosts they currently match.

Passing score: 75%. Answers are checked in this browser.