Skip to content

Expert · Ansible in depth · topic 50 of 57 · 35 min

Loops, conditions and error handling

Explanation

loop repeats a task for each item in a list; the current item is {{ item }}. when runs a task only if a condition is true, often using facts or registered results. block groups tasks, and rescue and always handle failures like try/catch/finally. failed_when and changed_when let you define what success means.

Loops

yaml
- name: Install tools
  ansible.builtin.dnf:
    name: "{{ item }}"
    state: present
  loop:
    - vim-enhanced
    - tmux
    - git
For packages, passing the whole list to name: is faster (one transaction). Loops are for modules that take one item at a time.
yaml
# vars
users:
  - name: asha
    groups: wheel
  - name: ravi
    groups: developers
  - name: meena
    groups: developers

# task
- name: Create users
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
    append: true
  loop: "{{ users }}"
  loop_control:
    label: "{{ item.name }}"
Looping over a list of dictionaries: item.name, item.groups. label keeps the output short.

Conditions with when

when takes a Jinja2 expression WITHOUT the {{ }} braces. If it is false, the task is skipped for that host (shown as "skipping"). Combine with and, or, not and brackets; a list under when means all must be true.

yaml
- name: Install Apache only on RHEL 9
  ansible.builtin.dnf:
    name: httpd
    state: present
  when:
    - ansible_facts['distribution'] == "RedHat"
    - ansible_facts['distribution_major_version'] == "9"

- name: Warn about small servers
  ansible.builtin.debug:
    msg: "{{ inventory_hostname }} has only {{ ansible_facts['memtotal_mb'] }} MB RAM"
  when: ansible_facts['memtotal_mb'] < 2048

- name: Only for hosts in the dbservers group
  ansible.builtin.dnf:
    name: mariadb-server
  when: "'dbservers' in group_names"

With a loop, when is checked for each item: when: item.groups == "wheel" creates only the admins. To test a registered result: when: result is failed, when: result.rc != 0, or when: conf.stat.exists.

Deciding what failed and what changed

yaml
- name: Check the app health page
  ansible.builtin.uri:
    url: http://localhost:8080/healthz
    status_code: [200, 503]
  register: health

- name: Is the database reachable?
  ansible.builtin.command: /opt/notes/bin/check-db
  register: dbcheck
  changed_when: false
  failed_when: "'unreachable' in dbcheck.stdout"

block, rescue and always

yaml
- name: Upgrade the app safely
  block:
    - name: Stop the app
      ansible.builtin.service: { name: notes, state: stopped }
    - name: Install the new version
      ansible.builtin.dnf: { name: notes-2.1, state: present }
  rescue:
    - name: Roll back to the old version
      ansible.builtin.dnf: { name: notes-2.0, state: present, allow_downgrade: true }
  always:
    - name: Start the app again
      ansible.builtin.service: { name: notes, state: started }
If any task in block fails, rescue runs; always runs in every case. The { } form is a one-line YAML dictionary.

ignore_errors: true on a task lets the play continue even if that task fails. Use it sparingly; it hides real problems. A block with rescue is usually the cleaner way.

Retrying until something is ready

Sometimes a task must wait, for example until a freshly restarted app answers on its port. until repeats the task until a condition is true, trying up to retries times with delay seconds in between. If the condition never becomes true, the task fails with a clear message instead of the next task failing for a confusing reason.

yaml
- name: Wait until the app answers
  ansible.builtin.uri:
    url: http://localhost:8080/healthz
  register: health
  until: health.status == 200
  retries: 10
  delay: 5
Up to 10 tries, 5 seconds apart: at most about 50 seconds of waiting.

Common mistakes

  • Putting braces in when: when: "{{ x }} == 1". Ansible warns; write when: x == 1.
  • Comparing version facts as numbers without | int.
  • Using the old with_items syntax in new code. loop is the current way (with_* still works).
  • A loop over a big list of packages with dnf, which is slow; pass the list to name instead.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Loops, conditions and error handling · 1/6

loop:
  - vim-enhanced
  - tmux
name: "{{ item }}"

loop repeats a task for each item. The current item is item.

Prefer a video by a teacher? Find free videos on “ansible loops when conditionals block rescue always tutorial” on YouTube ↗

In real life: Mixed fleet at an Indian railway IT vendor

An IT vendor maintaining ticketing kiosks for a railway zone has RHEL 8 and RHEL 9 machines in the field. Their playbook uses when on distribution_major_version to pick the right package names for each, loops over a list of kiosk users, and wraps the kiosk software upgrade in a block with a rescue that reinstalls the previous version, so a failed upgrade never leaves a station without a working kiosk.

yaml
- name: Kiosk browser for RHEL 8
  ansible.builtin.dnf:
    name: kiosk-browser-1.8
  when: ansible_facts['distribution_major_version'] == "8"
- name: Kiosk browser for RHEL 9
  ansible.builtin.dnf:
    name: kiosk-browser-2.3
  when: ansible_facts['distribution_major_version'] == "9"

Practice (0/3 done)

Task 1 · Question

Which when line is written correctly?

Task 2 · Code · Python

Model a loop with a condition. users is a list of dicts with name, state ("present" or "absent") and optional groups. Write plan(users, only_group=None) that returns, in order, one string per user that a task with loop + when would act on: "create NAME" for present users and "remove NAME" for absent ones. If only_group is given, include only users whose groups list contains it.

Task 3 · Practice

Write users.yml: a variable list of four users (name, groups, state). Create the groups first with a loop (unique group names), then create or remove users with one loop task using when to pick present/absent. Add a block that installs a package that does not exist, with a rescue that prints a friendly message and an always that writes "done" to /tmp/users.log.

Quick quiz

Question 1

1.In a loop, what variable holds the current element?

Question 2

2.What does Ansible show for a task whose when is false?

Question 3

3.When does the always section of a block run?

Question 4

4.Why is name: "{{ packages }}" (a list) better than a loop for dnf?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

In a loop, what variable holds the current element?

Answer: item

item (rename with loop_control: loop_var).

What does Ansible show for a task whose when is false?

Answer: skipping

It is skipped for that host.

When does the always section of a block run?

Answer: Every time, after block (and rescue if it ran)

Like finally.

Why is name: "{{ packages }}" (a list) better than a loop for dnf?

Answer: One dnf transaction is faster than one per package

dnf accepts a list.

Which loop is best to visit every item of an array?Related

Answer: for...of

for...of gives you each item in turn, with no index to manage.

From Loops: repeating work

Which loop fits best when you repeat until something happens, without knowing how many rounds?Related

Answer: while

A while loop keeps going until its condition becomes False, however many rounds that takes.

From while loops

Why add changed_when: false to a command that only reads data?Related

Answer: So it does not report "changed" every run

Reading is not a change; this keeps the recap honest.

From Variables, facts and registered results

Which loop is safest for going over every item of an array?Related

Answer: for item in array

for cannot go past the end, so there are no index mistakes.

From Control flow: if, loop, while and for

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Ansible in depth” before its exam: Jinja2 templates, Roles, collections and ansible-galaxy, RHEL System Roles and Ansible Vault.
  2. Module 10 exam: Ansible in depth · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Certifications and career. 57 topics left in the course before the final exam.

← Previous: Variables, facts and registered results