AnsibleXXXI · Serial Execution and Failure ToleranceFailure Tolerance
any_errors_fatal and what fatal means
What you'll learn
- Describe exactly how far execution proceeds after any_errors_fatal triggers
- Explain why a later play in the same playbook does not run
- Apply any_errors_fatal at block level to scope it to a critical section
- State what any_errors_fatal does not do about unreachable hosts
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
max_fail_percentage expresses a tolerance: some failures are
acceptable, past a threshold they are not. any_errors_fatal expresses
the opposite position — one failure anywhere means the change is
wrong, so stop touching things.
Upstream describes it precisely, and the precision is the whole lesson:
If you set
any_errors_fataland a task returns an error, Ansible finishes the fatal task on all hosts in the current batch and then stops executing the play on all hosts.
Two clauses. The first is a courtesy; the second is much bigger than it sounds.
Finishing the fatal task on all hosts in the batch
The failing task is not abandoned partway. Every host in the batch runs it, and the results all appear, before anything stops.
$ ansible-playbook -i inv6.ini aef.ymlPLAY [Rollout] *****************************************************************
TASK [Deploy] ******************************************************************
ok: [h01] => { "msg": "DEPLOYED h01" }
ok: [h02] => { "msg": "DEPLOYED h02" }
TASK [Health check] ************************************************************
ok: [h01] => { "msg": "HEALTHY h01" }
ok: [h02] => { "msg": "HEALTHY h02" }
TASK [Return to service] *******************************************************
ok: [h01] => { "msg": "INSERVICE h01" }
ok: [h02] => { "msg": "INSERVICE h02" }
PLAY [Rollout] *****************************************************************
TASK [Deploy] ******************************************************************
ok: [h03] => { "msg": "DEPLOYED h03" }
ok: [h04] => { "msg": "DEPLOYED h04" }
TASK [Health check] ************************************************************
fatal: [h03]: FAILED! => {"assertion": "inventory_hostname not in ['h03']",
"changed": false, "evaluated_to": false,
"msg": "health check failed"}
ok: [h04] => { "changed": false, "msg": "HEALTHY h04" }
PLAY RECAP *********************************************************************
h01 : ok=3 changed=0 unreachable=0 failed=0
h02 : ok=3 changed=0 unreachable=0 failed=0
h03 : ok=1 changed=0 unreachable=0 failed=1
h04 : ok=2 changed=0 unreachable=0 failed=0h04 ran the health check and passed it, after h03 was already fatal.
That is the first clause working: the task completes across the batch.
h04 then did not run “Return to service”. That is the second
clause, and it produces the same stranded-host problem the previous
lesson described for max_fail_percentage: a healthy host, drained,
never returned.
Stopping the play on all hosts
Read the recap again. h05 and h06 are not in it.
They were in batch three. Batch three never ran. any_errors_fatal did
not merely end the batch that contained the failure — it ended the play,
and every host that had not yet been reached was never reached.
That is the intended behaviour and it is the reason to choose this keyword. But it extends further than most people expect.
Scoping it to a block
any_errors_fatal is settable at play level and at block level. Block
level is usually the better instrument, because most playbooks contain
one genuinely critical section and a lot of ordinary work around it.
- name: Apply the release
hosts: appservers
become: true
serial: 2
tasks:
- name: Stage the release artefact
ansible.builtin.copy:
src: 'app-{{ release_version }}.tar.gz'
dest: /var/tmp/app-release.tar.gz
mode: '0640'
- name: Schema-affecting migration
any_errors_fatal: true
block:
- name: Verify the migration is reversible before applying it
ansible.builtin.command: /usr/local/bin/app-migrate --dry-run
changed_when: false
- name: Apply the migration
ansible.builtin.command: /usr/local/bin/app-migrate --apply
register: migration
changed_when: "'applied' in migration.stdout"
- name: Record the deployed version
ansible.builtin.copy:
content: '{{ release_version }}'
dest: /etc/app/deployed-version
mode: '0644'Verified on 2.21.3 with the block form: serial: 2 over six hosts, a
failure on h03 inside the block. h01 and h02 completed the play,
h04 did not run the task after the block, and h05 and h06 never
ran at all — identical to the play-level behaviour, scoped to the block.
It does not make unreachable hosts fatal
This is the finding that surprised me most while preparing this lesson, and it is worth stating as an exception because the keyword name promises otherwise.
The upstream sentence says “if a task returns an error”. A host that
cannot be connected to does not return an error from a task — it never
ran one. Ansible marks it UNREACHABLE and, per the documentation,
“removes it from the list of active hosts for the run”. That is a
different path, and any_errors_fatal does not intercept it.
$ ansible-playbook -i inv-unreach.ini rollout.ymlPLAY [Rollout] *****************************************************************
TASK [Touch the host] **********************************************************
ok: [h02]
ok: [h03]
fatal: [u01]: UNREACHABLE! => {"changed": false, "msg": "Task failed: Failed to
connect to the host via ssh: ssh: connect to host 192.0.2.11 port 22: Connection
timed out", "unreachable": true}
PLAY [Rollout] *****************************************************************
TASK [Touch the host] **********************************************************
ok: [h04]
ok: [h05]
ok: [h06]
TASK [Return to service] *******************************************************
ok: [h04] => { "msg": "INSERVICE h04" }
ok: [h05] => { "msg": "INSERVICE h05" }
ok: [h06] => { "msg": "INSERVICE h06" }
PLAY [Second play] *************************************************************
TASK [Monitoring] **************************************************************
ok: [h04] => { "msg": "MONITORING h04" }
ok: [h05] => { "msg": "MONITORING h05" }
ok: [h06] => { "msg": "MONITORING h06" }
PLAY RECAP *********************************************************************
h02 : ok=1 changed=0 unreachable=0 failed=0
h03 : ok=1 changed=0 unreachable=0 failed=0
h04 : ok=3 changed=0 unreachable=0 failed=0
h05 : ok=3 changed=0 unreachable=0 failed=0
h06 : ok=3 changed=0 unreachable=0 failed=0
u01 : ok=0 changed=0 unreachable=1 failed=0
exit=4Compare the two runs directly. Same keyword, same serial, same
playbook shape, failure in batch one:
| Host failed a task | Host was unreachable | |
|---|---|---|
| Rest of the failing batch | stops after the fatal task | stops after the fatal task |
| Later batches | never run | run to completion |
| Later play in the playbook | never runs | runs, for the later-batch hosts |
| Exit code | 2 | 4 |
The batch containing the unreachable host was cut — h02 and h03
never returned to service, and they are absent from the second play, so
they were dropped from the run. But the rollout itself continued.
any_errors_fatal is not a defence against a fleet that is
disappearing. Neither is max_fail_percentage. If the risk you are
managing is connectivity rather than correctness, the mechanism is a
reachability precondition in an unbatched play before the rollout
starts — a subject the next lesson takes up in full.
Knowledge check
Knowledge check · 4 questions
Q1. A play has any_errors_fatal: true and serial: 2 over six hosts. Host h03 fails a task in batch two. What does h04, its batch-mate, do?
Q2. A playbook has a rolling play with any_errors_fatal: true, followed by a second play that re-enables monitoring. A host fails in batch two of five. What is true afterwards? Select all that apply.
Q3. With any_errors_fatal: true, a host that is unreachable rather than failed will stop the whole run in the same way a failed host does.
Q4. Which change is the best candidate for any_errors_fatal rather than max_fail_percentage?
Passing score: 75%. Answers are checked in this browser.