// learn Β· linux Β· rhce Β· 9 min

Ansible loops & conditionals.

Two tiny keywords carry most of Ansible's logic: `loop` runs a task once per element (exposed as {{ item }}), and `when` decides β€” per host and per loop item β€” whether it runs at all. Then teach command/shell what `changed` and `failed` really mean.

Ansible loops and conditionals animated tutorial. Looping a task over a list with loop and the item variable, replacing legacy with_items, when conditionals as bare Jinja expressions, combining loop with when to skip iterations per item, registering looped tasks and reading result.results, changed_when and failed_when for non-idempotent command and shell tasks, loop_control with label and index_var, and until/retries/delay polling. RHCE EX294 ready.
// ansible Β· loops & conditionals

Two tiny keywords carry most of Ansible's logic: loop runs a task once per element, and when decides whether it runs at all. Watch a task iterate over users, get gated per host, and learn what "changed" really means.

// before you start
you should know
  • Β·You've written a basic playbook with tasks
  • Β·Comfortable with YAML lists and dictionaries
  • Β·Know what a fact / variable is in Ansible
by the end you'll

Loop a task over a list with {{ item }}, gate tasks with when:, combine the two to skip iterations, redefine changed/failed for command tasks, and poll with until/retries.

pace: 9 minutes

ansible β€” step 1 / 5 Β· loops
- name: Create usertask
loop: [alice, bob, carol]
β”‚ runs once per item β”‚
item=alice● changed
item=bob● changed
item=carol● changed

loop β€” do one task many times

Add `loop:` with a list and Ansible runs the task once per element, exposing the current element as `{{ item }}`. Modern `loop:` replaces the older `with_items` and friends. You can loop over a plain list, a variable that holds a list, or a list of dictionaries β€” then reach into each dict with `{{ item.name }}`. One task definition, many runs.

$- name: Create user\n ansible.builtin.user:\n name: "{{ item }}"\n loop: [alice, bob, carol]
$loop: "{{ users }}"
$- ansible.builtin.user:\n name: "{{ item.name }}"\n groups: "{{ item.groups }}"\n loop: "{{ user_list }}"
// key insight

Two small keywords carry most of Ansible's logic: `loop` runs a task once per element (the current one is {{ item }}), and `when` decides per host (and per loop item) whether it runs at all. And because command/shell aren't idempotent, `changed_when`/`failed_when` let you teach Ansible what "changed" and "failed" actually mean for them.

// exam-ready Β· loops, conditionals & outcomes
$loop: [a, b, c]
$loop: "{{ list }}"
${{ item }} / {{ item.key }}
$with_items: […]
$when: COND
$when: a and b
$when: x is defined
$when:\n - c1\n - c2
$register: r β†’ r.results
$changed_when: false
$failed_when: COND
$loop_control: label / index_var / pause
$until: COND retries: N delay: S
$loop: "{{ d | dict2items }}"
// check yourself
4 quick questions
Q1

Do you wrap a `when:` condition in `{{ }}`?

Q2

How do you make a one-off `command` task never report "changed"?

Q3

You loop over 3 users and `register` the result. Where are the per-item results?

Q4

What does `until` + `retries` + `delay` do?

These aren't graded β€” they're just for active recall, which is what actually makes the lesson stick.

🧬
// next: dynamic config

Ansible templates (Jinja2)

Loops and conditionals also live inside config files. Render per-host config with the `template` module and Jinja2 β€” variables, loops, and `when`-style logic, straight into /etc.

open β†’

Read this lesson as text5 chapters Β· quiz answers

  1. 01loop β€” do one task many times

    Add loop: with a list and Ansible runs the task once per element, exposing the current element as {{ item }}. Modern loop: replaces the older with_items and friends. You can loop over a plain list, a variable that holds a list, or a list of dictionaries β€” then reach into each dict with {{ item.name }}. One task definition, many runs.

    • $ - name: Create user\n ansible.builtin.user:\n name: "{{ item }}"\n loop: [alice, bob, carol] β€” one task, three runs
    • $ loop: "{{ users }}" β€” loop a variable that holds a list
    • $ - ansible.builtin.user:\n name: "{{ item.name }}"\n groups: "{{ item.groups }}"\n loop: "{{ user_list }}" β€” list of dicts β†’ item.name
  2. 02when β€” run only if a condition holds

    when: takes a RAW Jinja expression β€” you do NOT wrap the whole condition in {{ }} (braces are only for inserting a value into a string). Combine tests with and/or, or give a YAML list, which is an implicit AND. You can test facts, variables, is defined, and comparisons. Here the gate os_family == "RedHat" decides whether the task runs at all on a given host.

    • $ when: ansible_facts['os_family'] == "RedHat" β€” bare expression β€” no {{ }}
    • $ when: my_var is defined β€” test that a var exists
    • $ when:\n - ansible_facts['os_family'] == "RedHat"\n - install_web | bool β€” a YAML list = implicit AND
  3. 03loop + when together

    Put when: on a looped task and the condition is evaluated PER ITEM β€” so some iterations run and others are skipped, all from one task. Looping over a list of dicts lets each element carry its own flag, e.g. when: item.enabled. If you register: a looped task you don't get a single result β€” you get result.results, a list with one entry per item that you can iterate later.

    • $ - ansible.builtin.user:\n name: "{{ item.name }}"\n loop: "{{ users }}"\n when: item.enabled β€” skip per item
    • $ - ansible.builtin.command: "id {{ item }}"\n loop: [alice, bob, carol]\n register: result β€” register a looped task
    • $ - debug:\n var: item.stdout\n loop: "{{ result.results }}" β€” result.results = one per item
  4. 04changed_when / failed_when β€” redefine the outcome

    command and shell aren't idempotent, so Ansible can't tell if they really changed anything β€” by default they always report changed. Use changed_when: to override that (often changed_when: false for a read-only check) and failed_when: to decide what counts as a failure based on the registered result. These take the same bare Jinja expressions as when:.

    • $ - ansible.builtin.command: systemctl is-active sshd\n changed_when: false β€” a check never "changes"
    • $ - ansible.builtin.command: /opt/build.sh\n register: result\n failed_when: "'ERROR' in result.stderr" β€” fail on a string in output
    • $ failed_when: result.rc != 0 and result.rc != 2 β€” rc 2 is fine here
  5. 05loop controls & retries

    loop_control: cleans up looped tasks: label shows a friendly string instead of the whole dict in output, index_var exposes the position, and pause waits between items. To poll until something is ready, add until: with retries: and delay: β€” Ansible re-runs the task until the condition is true or it runs out of attempts. Use dict2items to loop over a dictionary as key/value pairs.

    • $ - ansible.builtin.user:\n name: "{{ item.name }}"\n loop: "{{ users }}"\n loop_control:\n label: "{{ item.name }}" β€” tidy output via label
    • $ - ansible.builtin.command: curl -sf http://localhost\n register: result\n until: result.rc == 0\n retries: 5\n delay: 3 β€” poll until rc 0
    • $ loop: "{{ my_dict | dict2items }}" β€” iterate a dict as key/value

Quiz β€” answers

Do you wrap a when: condition in {{ }}?
β†’No β€” when takes a bare Jinja expression; the {{ }} braces are only for inserting a value into a string.when: is already evaluated as a Jinja expression, so you write when: ansible_facts['os_family'] == "RedHat" with no braces. Braces are for templating a value into a string, e.g. name: "{{ item }}".
How do you make a one-off command task never report "changed"?
β†’Set changed_when: false.command/shell aren't idempotent, so they default to reporting changed every run. changed_when: false tells Ansible this task is a read-only check that never changes the system.
You loop over 3 users and register the result. Where are the per-item results?
β†’In result.results β€” a list with one entry per loop item.A registered looped task gives you result.results, a list with one entry per iteration (each holding that item's own rc, stdout, item value, etc). You loop over result.results to use them.
What does until + retries + delay do?
β†’Retries the task until the until condition is true, up to retries times, waiting delay seconds between attempts β€” polling.It's a polling loop: Ansible re-runs the task, checks until, and if it's still false waits delay seconds and tries again, giving up after retries attempts. Great for waiting on a service to come up.
// more in systems

keep going β€” these pair well with what you just learned.

see all systems β†’

See also

back to RHCSA / RHCE trackall lessons