Lesson 8 of 9 · 26m
Failure Handling, Execution Strategies, and Speed
block/rescue/always, failed_when, serial and strategies, async/poll — and the tuning knobs (forks, pipelining, fact caching) that turn a 40-minute run into a 6-minute one.
Two production scars taught me this lesson's material. One: a deploy playbook that kept going after a database migration failed on the primary — ignore_errors: true someone left behind, so every replica ran migrations against a broken schema. Two: a "why does this take 45 minutes" audit that found forks: 5, no pipelining, and SSH multiplexing off. Failure semantics and execution speed are the same skill: controlling when tasks run and what happens when they don't.
block/rescue/always: structured failure
- name: Apply migration safely
block:
- name: Stop writes
ansible.builtin.service: { name: app, state: stopped }
- name: Run migration
ansible.builtin.command: /opt/app/bin/migrate
rescue:
- name: Roll back
ansible.builtin.command: /opt/app/bin/migrate --rollback
- name: Start app back up
ansible.builtin.service: { name: app, state: started }
always:
- name: Tell the on-call
ansible.builtin.command: /opt/bin/notify-oncall "migration finished"rescue: runs when anything in block: fails (with ansible_failed_task and ansible_failed_result available); always: runs regardless — it's finally. This replaces "the playbook died mid-flight and left the app stopped."
failed_when, ignore_errors, and honesty
- name: Check cert expiry
ansible.builtin.command: openssl x509 -checkend 604800 -noout -in /etc/ssl/acme.crt
register: cert_check
failed_when: false # non-zero is data, not failure
changed_when: false
# ...then a later task does: when: cert_check.rc != 0ignore_errors: true means "I refuse to know this failed" — a code smell unless you immediately inspect the registered result. failed_when/changed_when let you teach Ansible what failure means for commands it can't introspect. any_errors_fatal: true on a play kills the whole run on first failure — the opposite extreme, correct for migrations, wrong for "patch a thousand web servers."
serial, strategies, and async
For rolling deploys, serial: "30%" on the play batches hosts and max_fail_percentage: 10 aborts the rollout when a batch goes bad. strategy: free lets fast hosts run ahead instead of lockstep per task — great for patching, wrong for anything order-dependent. And for genuinely slow work, async: 1800 + poll: 0 fires the task and moves on; a later async_status task with until: job.finished collects it.
The speed knobs, in order of payoff
# ansible.cfg
[defaults]
forks = 50 # parallel hosts — the default of 5 is why you're slow
gathering = smart # only gather facts when the cache is stale
fact_caching = jsonfile
fact_caching_connection = ~/.ansible/facts
[ssh_connection]
pipelining = True # pipe modules via stdin, skip the temp-file round trips
ssh_args = -o ControlMaster=auto -o ControlPersist=60sControlPersist amortizes the SSH handshake across a play; pipelining (needs requiretty off in sudoers) cuts per-task round trips. Together they're routinely a 3-5x win before anything clever. Profile with profile_tasks before reaching for Mitogen or pull mode.
Field notes
serialbatches are per-play — a playbook with three plays re-batches each one.delegate_to: localhost+run_once: truefor "do this once total" (e.g., create the LB pool), not once per host.poll: 0without a laterasync_statusmeans you fired and forgot — always pair them, or the task is a rumor.