AnsibleXLIV · Failure Modes and Partial Fleet FailureFailure Modes and Partial Fleet Failure
Stopping a bad change at host twelve
What you'll learn
- Combine serial, max_fail_percentage and any_errors_fatal into a bounded blast radius
- State the exact evaluation rule for max_fail_percentage and why equalling it does not stop the play
- Explain what force_handlers changes when a play aborts
- Design a canary ramp that limits the first failure to a countable number of 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
Everything in this part so far has been about a failure that already happened. This lesson is about the four keywords that decide how big it is allowed to get.
The 300/30/50 scenario is a failure of scope control before it is
anything else. A play with no serial, no failure threshold and no
abort criteria hits all 300 hosts before anyone can react, because
Ansible works through the fleet at forks hosts at a time and does not
pause to ask. The same play with a canary batch fails at host 12,
someone reads the error, and the other 288 never run.
The four controls, and the question each answers
| Keyword | Question it answers | Scope of its effect |
|---|---|---|
serial | How many hosts change at once? | Defines the batch. Everything else is relative to it |
max_fail_percentage | How much failure within a batch is tolerable? | Aborts the play after the current batch |
any_errors_fatal | Is one failure enough to stop everything? | Aborts the play and the playbook |
force_handlers | Do notified handlers still run when a play aborts? | Changes what state the aborted hosts are left in |
They compose, and the composition is the point. serial alone limits
concurrency but not total reach — without a threshold, a play with
serial: 20 and a failure in every batch still visits all 300 hosts,
twenty at a time, failing all the way.
serial is the unit of damage
serial decides the largest number of hosts that can be in a broken
state before anything can stop the run, because thresholds are evaluated
between batches.
That gives a rule of thumb worth stating directly: your first batch is your maximum acceptable loss. If losing twelve hosts simultaneously is survivable and losing thirty is not, the first batch is twelve, whatever the rest of the ramp looks like.
- name: Application rollout
hosts: appservers
become: true
serial:
- 1 # one host. If this fails, the damage is one host
- 11 # a dozen total. Enough to see a pattern
- '10%' # then ramp
- '25%'
max_fail_percentage: 0
tasks:
- name: Deploy the release
ansible.builtin.include_role:
name: app_releaseThe first two entries are the whole safety mechanism. Everything after them is throughput.
max_fail_percentage must be exceeded
This is the semantic that catches people, and it is worth stating with its arithmetic.
The play aborts when the percentage of failed hosts in the batch is greater than the configured value. Equal is not greater.
| Batch size | max_fail_percentage | Failures | Percentage | Aborts? |
|---|---|---|---|---|
| 20 | 10 | 2 | 10% | No — equal, not exceeded |
| 20 | 10 | 3 | 15% | Yes |
| 20 | 0 | 1 | 5% | Yes |
| 3 | 30 | 1 | 33.3% | Yes |
| 4 | 30 | 1 | 25% | No |
Two consequences follow.
max_fail_percentage: 0 means “any failure stops the play”, because
any non-zero percentage exceeds zero. It does not mean “tolerate no
failures” in some special way; it is the ordinary rule applied to zero.
This is the most useful setting for a change where consistency matters,
and it is more precise than any_errors_fatal because it stops the
play without stopping the whole playbook.
Small batches make the percentage coarse. With serial: 3, the
available percentages are 0, 33, 67 and 100. A threshold of 30 and a
threshold of 10 behave identically. During the canary phase of a ramp,
you are effectively choosing between “stop on any failure” and “do not
stop”, so choose deliberately rather than tuning a number that cannot
express what you meant.
any_errors_fatal stops the playbook, not just the play
Covered in full in the failure-tolerance part; recalled here because its reach is the part that produces incidents.
Verified on 2.21.3: with any_errors_fatal: true, a failure in batch
two of five means batches three, four and five never run and produce no
recap lines at all, and a later play in the same playbook does not
run either.
$ ansible-playbook -i inv6.ini aef.ymlTASK [Health check] ************************************************************
fatal: [h03]: FAILED! => {"msg": "health check failed"}
ok: [h04] => { "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=0The operational consequence is the one to carry into a design review: a
patching playbook whose first play silences monitoring and whose last
play re-enables it will, on a single failure, leave the entire fleet
unmonitored. Anything that must happen regardless of outcome belongs in
a block/always, never in a later play.
Neither of them sees unreachable hosts
Both keywords react to task failures. A host that cannot be connected to never returns a task result, so neither threshold counts it.
Verified on 2.21.3 with any_errors_fatal: true, serial: 3 and one
documentation-range address in batch one: the batch containing the
unreachable host was cut, and then later batches ran to completion and
the subsequent play ran too, exiting 4. Compare the failed-host run
above, where batch two never started.
force_handlers, and the hosts left drained
When a play aborts, notified handlers that have not yet run are discarded. That is usually right — you do not want a service restart firing on a fleet you just decided to stop changing.
It is wrong in one specific shape, and it is the shape rolling deployments have. If the play drains a host from the load balancer, changes it, and returns it to service via a handler, then aborting between those steps leaves the host drained. The abort protected the fleet from the change and left the canary out of rotation.
- name: Application rollout
hosts: appservers
become: true
serial: [1, 11, '25%']
max_fail_percentage: 0
force_handlers: true
tasks:
- name: Deploy configuration
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
mode: '0640'
validate: '/usr/sbin/appd -t -c %s'
notify: Restart app
handlers:
- name: Restart app
ansible.builtin.systemd_service:
name: appd
state: restartedforce_handlers is not a general safety improvement — it means handlers
run on a play you decided to abort, which is sometimes precisely what
you did not want. The right question is the same one as everywhere else
in this part: if this play stops here, what state is each host left
in, and does a handler need to run to make that state coherent?
The general answer for cleanup work is block/always, which runs
regardless of outcome and is not subject to the notification mechanism
at all. force_handlers is for the case where the coherence-restoring
action is genuinely a handler and you cannot restructure it.
The design, assembled
For the 300-host scenario, the play that would have limited it:
- name: Pre-flight — the fleet must actually be there
hosts: appservers
gather_facts: false
tasks:
- name: Prove every target is reachable
ansible.builtin.ping:
- name: Refuse to start if the reachable count is short
ansible.builtin.assert:
that: ansible_play_hosts | length == ansible_play_hosts_all | length
fail_msg: >-
{{ ansible_play_hosts_all | difference(ansible_play_hosts) | join(', ') }}
did not answer. Resolve or re-scope before changing anything.
run_once: true
- name: Rollout
hosts: appservers
become: true
serial: [1, 11, '10%', '25%']
max_fail_percentage: 0
force_handlers: true
tasks:
- name: Apply the change
ansible.builtin.include_role:
name: patch_and_configureUnder that play, the same underlying problems produce a very different night: the 50 unreachable hosts stop the run before it starts, and if they had appeared later, the first task failure ends the rollout inside the canary batch.
Knowledge check
Knowledge check · 4 questions
Q1. A play has serial: 20 and max_fail_percentage: 10 over 300 hosts. Exactly two hosts fail in each of the fifteen batches. What happens?
Q2. Setting any_errors_fatal: true protects a rollout against hosts that become unreachable partway through.
Q3. Which statements about serial in a rolling change are accurate? Select all that apply.
Q4. A rolling play drains each host from the load balancer, changes it, and returns it to service through a notified handler. The play aborts on a failure. What is the risk, and what addresses it?
Passing score: 75%. Answers are checked in this browser.