Ansible · lesson 4 of 6
Loops, Conditionals, and block/rescue: Reading the Real Recap Numbers
A loop over 3 items reports changed=1, not 3 — the recap counts hosts, not items. Plus when-conditionals that skip, and block/rescue/always catching a bug.
By SkillByExample · Updated
Three cmdlets worth of playbook control flow, and one of them has a summary number that means something different from what it looks like.
Looping over a list
# loop_playbook.yml
---
- name: Loop over a list
hosts: local
tasks:
- name: Create multiple directories
file:
path: "/tmp/loopdirs/{{ item }}"
state: directory
loop:
- alpha
- beta
- gamma
ansible-playbook -i inventory.ini loop_playbook.yml
TASK [Create multiple directories] *********************************************
changed: [localhost] => (item=alpha)
changed: [localhost] => (item=beta)
changed: [localhost] => (item=gamma)
PLAY RECAP *********************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Three items, three real changed results printed per-iteration — and the PLAY RECAP at the
bottom says changed=1, not 3. That’s not a bug or an undercount: the recap summarizes
per host, not per iteration. changed=1 means “this one host had at least one changed
result this play” — for a count of individual item-level changes, the per-item lines above the
recap are the actual source of truth, not the summary line.
Conditionals with when
# when_playbook.yml
---
- name: Conditional tasks
hosts: local
vars:
deploy_env: production
tasks:
- name: Only in production
debug:
msg: "Deploying to prod"
when: deploy_env == "production"
- name: Only in staging
debug:
msg: "Deploying to staging"
when: deploy_env == "staging"
ansible-playbook -i inventory.ini when_playbook.yml
TASK [Only in production] ******************************************************
ok: [localhost] => {
"msg": "Deploying to prod"
}
TASK [Only in staging] *********************************************************
skipping: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
The staging task never ran at all — skipping, not ok and not failed. skipped=1 in the
recap tracks exactly this: a task genuinely excluded by its when: condition, a third outcome
distinct from both success and failure.
block, rescue, and always: real error recovery
# block_playbook.yml
---
- name: Block with rescue
hosts: local
tasks:
- name: Attempt risky operation
block:
- name: This will fail
command: /bin/false
- name: This never runs
debug:
msg: "unreachable"
rescue:
- name: Handle the failure
debug:
msg: "Recovered from failure"
always:
- name: Always runs
debug:
msg: "Cleanup step"
ansible-playbook -i inventory.ini block_playbook.yml
TASK [This will fail] **********************************************************
fatal: [localhost]: FAILED! => {"changed": true, "cmd": ["/bin/false"], "rc": 1, ...}
TASK [Handle the failure] ******************************************************
ok: [localhost] => {
"msg": "Recovered from failure"
}
TASK [Always runs] *************************************************************
ok: [localhost] => {
"msg": "Cleanup step"
}
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
/bin/false genuinely fails (rc: 1) — and This never runs really doesn’t; a failure inside
block: immediately jumps to rescue:, skipping any remaining tasks in the block. rescue:
catches it and the play continues; always: runs regardless, the same guarantee finally
gives in most languages. The recap’s failed=0 confirms the play didn’t actually fail overall
— rescued=1 is the honest record of what happened instead.
Practice
1. Count the real number of changed items in a loop by inspecting the task result, not the recap.
rm -rf /tmp/loopdirs
ansible-playbook -i inventory.ini loop_playbook.yml -v 2>&1 | grep -c "changed: \[localhost\] =>"
3
-v (verbose) prints the same per-item lines seen in the main run; counting them directly is
the actual way to answer “how many items changed,” since the recap’s single number can’t
express that. The rm -rf matters here: the directories already exist from the main lesson
above, and a converged file task reports ok, not changed — run this against
already-converged state and the count comes back 0, not 3.
2. Add a when: condition inside a loop, skipping specific items rather than the whole task.
- name: Create dirs except beta
file:
path: "/tmp/loopdirs2/{{ item }}"
state: directory
loop:
- alpha
- beta
- gamma
when: item != "beta"
ansible-playbook -i inventory.ini loop_when_playbook.yml
TASK [Create dirs except beta] *************************************************
changed: [localhost] => (item=alpha)
skipping: [localhost] => (item=beta)
changed: [localhost] => (item=gamma)
when: evaluates per-iteration when paired with loop: — item refers to the current
iteration’s value, so one task can selectively skip individual items instead of needing a
separate task per exception.
3. Remove the rescue: section and observe the play actually fail.
- name: Attempt risky operation
block:
- name: This will fail
command: /bin/false
ansible-playbook -i inventory.ini block_playbook.yml
TASK [This will fail] **********************************************************
fatal: [localhost]: FAILED! => {"changed": true, "cmd": ["/bin/false"], "rc": 1, ...}
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Without rescue: to catch it, the same failure now shows up as failed=1 and the play stops
for that host — confirming rescue: was the only thing standing between this failure and a
genuinely failed play.
Next: integrating Ansible into a CI/CD pipeline.