Lesson 5 of 9 · 22m
Loops, Handlers, and Conditionals Without the Traps
loop vs the deprecated with_* forms, when clauses that actually evaluate the way you think, and the handler lifecycle that decides whether your service reload happens.
The two questions that expose Ansible experience: "why did your handler not run?" and "why is your when never true?" Both are lifecycle misunderstandings, not syntax errors — the code parses, it just doesn't mean what you think.
loop: one syntax to learn, the rest to forget
- name: Create service accounts
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: deploy, groups: "docker,sudo" }
- { name: backup, groups: "backup" }loop replaced the with_items/with_dict/with_fileglob zoo (they're lookup-plugin calls in disguise — they still work, but new code should use loop). loop_control: label: "{{ item.name }}" keeps the log readable when item is a fat dict, and until/retries/delay handle retry loops:
- name: Wait for health endpoint
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}:8080/healthz"
status_code: 200
register: health
retries: 12
delay: 10
until: health.status == 200when: Jinja truthiness, not Python
- name: Only on Debian family
ansible.builtin.apt: { name: htop, state: present }
when: ansible_facts['os_family'] == 'Debian'
- name: Restart if config drifted
ansible.builtin.service: { name: app, state: restarted }
when: config_result is changedwhen evaluates a Jinja2 expression per task, per host — a conditional applies to each host independently, which is how "only reboot hosts that need it" works in one task. Traps: never wrap when in {{ }} (it's already Jinja); prefer the is changed / is failed tests on registered results; and a string "false" from -e is truthy — pipe it through | bool.
Handlers: the notify lifecycle
Handlers run once, at the end of the play, only if notified by a task that reported changed — notify five times, it still runs once, in definition order rather than notification order.
handlers:
- name: Reload nginx
ansible.builtin.service: { name: nginx, state: reloaded }
listen: web config changed # tasks can notify the topic instead of the nameThe failure mode that bites: if a later task fails on that host, pending handlers never fire — the config changed but the service still runs the old one, and the next run sees no change so it never reloads. Fixes: meta: flush_handlers where you need the reload mid-play, or force_handlers: true (--force-handlers) so notified handlers run despite failures.
Field notes
changedoncommand/shellis a lie unless you setchanged_when— fix it or accept noisy runs.listen:topics let several handlers respond to one notification — "web config changed" can reload nginx and purge the CDN.include_tasksis dynamic and can be looped;import_tasksis resolved at parse time and can't takeloopat all.