AnsibleXL · Patch and Reboot ManagementPatch and Reboot Management
Sizing the patch run before you write it
What you'll learn
- Choose a serial shape for a patch run from the capacity the service actually needs
- Explain why max_fail_percentage is evaluated per batch and must be exceeded rather than equalled
- Distinguish forks as a throughput control from serial as a safety control
- Design the canary so that a bad patch stops after one host rather than forty
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
Patching is the highest-stakes ordinary thing automation does.
It is ordinary because it happens every month, on every host, and mostly without incident. It is high-stakes because it is the one routine change that can replace the kernel, restart every service, and take the machine down and back up again — and because the operator who wrote the play is usually asleep when it runs.
Everything Part XXXI taught about serial and failure tolerance applies
here, and it applies harder. A rolling application deploy that goes
wrong leaves you with a running host serving old code. A patch run that
goes wrong can leave you with a host that does not boot.
So the first decision is not which module to call. It is how many hosts may be in flight at once, and where the run is permitted to stop. Write that down before you write a task.
Three numbers, decided before the playbook exists
Every patch run is described by three numbers, and none of them come from Ansible.
How many hosts can the service lose simultaneously? This is a
capacity question. If a six-node service needs four nodes to carry peak
load, the answer is two, and no serial value above 2 is defensible
regardless of how long the run takes.
How long does one host take? Package download, install, reboot, reconnect, validate. On a real fleet this is rarely under four minutes and often ten. Multiply by the number of batches and compare against the change window you actually have.
How many failures make this a failed run rather than a run with
failures? A fleet of 300 will always have a host with a full /boot,
a host with a broken repository, a host that is already down. Deciding
that in advance is the difference between a judgement and a panic.
The canary is a host, not a percentage
serial: [1, 5, 25%] reads as a ramp. For a patch run, the leading 1
is doing something specific and different from the rest of the list: it
is the only batch whose purpose is to discover that the change is
fundamentally wrong.
- name: Monthly security patching
hosts: webservers
become: true
serial:
- 1
- 5
- 25%
max_fail_percentage: 0
any_errors_fatal: false
tasks:
- name: Apply security updates
ansible.builtin.include_role:
name: patching
- name: Prove the host is serving before the next batch starts
ansible.builtin.include_tasks: validate-host.ymlmax_fail_percentage: 0 is the right default for a patch run and it is
worth being explicit about why. Zero means “any failure in a batch stops
the play”, because a patch failure is almost never a property of one
host — it is a bad package, a repository that is serving a broken
update, or a service that does not survive the new version. Those
generalise. The second host will fail the same way.
The canary must be the same host every time
Batching slices the play host list positionally, in inventory order.
That is what makes serial: [1, ...] deterministic: the canary is the
first host of the resolved play host list, and it is the same host on
every run until inventory changes.
ansible-inventory -i inventories/production --list --yaml --limit webservers
ansible-playbook -i inventories/production patch.yml --limit webservers --list-hostsTwo things destroy this property, and both are worth checking for in review:
order: shuffleon the play randomises the host list before slicing. The canary becomes a lottery. There are reasons to shuffle a convergence run; there is no reason to shuffle a patch run.- A dynamic inventory whose order is not stable — a cloud plugin returning hosts in API order can silently change which host is first between runs.
A canary you cannot name is not a canary. It is just the host that happened to be unlucky.
max_fail_percentage is per batch, and must be exceeded
This is the single most misread keyword in a patch play, and it is worth executing rather than trusting.
$ ansible-playbook -i inv10.ini mfp.yml -e mfp=25 # then -e mfp=24max_fail_percentage: 25 (1 failure in a batch of 4 = 25%)
PLAY banners: 3
REACHED SECOND TASK: h01,h03,h04
REACHED SECOND TASK: h05,h06,h07,h08
REACHED SECOND TASK: h09,h10
exit=2
max_fail_percentage: 24 (1 failure in a batch of 4 = 25%)
PLAY banners: 1
NO MORE HOSTS LEFT
NO MORE HOSTS LEFT
exit=2At exactly 25 the play ran all three batches. At 24 it stopped after the first. Upstream states the rule plainly: the percentage set “must be exceeded, not equaled”.
Three consequences that matter for a patch run:
The denominator is the batch, not the fleet. max_fail_percentage: 10 over a 300-host fleet sounds like “stop after thirty failures”. With
serial: 10, one failure in a batch is 10%, which does not exceed 10,
so the run continues; two failures is 20%, which does, so it stops. The
fleet-wide number never enters the calculation.
Small batches make the percentage coarse. In a batch of one, the
only achievable failure rates are 0% and 100%. Any max_fail_percentage
below 100 stops the play on a canary failure, which is what you want,
and it means the exact value is irrelevant for that batch.
A surviving run still exits non-zero. Both runs above exited 2. A play that tolerated its failures and completed every batch is still a failed run as far as your scheduler is concerned. If you gate on exit code alone you cannot distinguish “stopped at the canary” from “finished with two casualties”, and those want very different responses.
forks is throughput. It is never safety.
forks controls how many hosts the controller talks to in parallel. It
is a controller resource setting: more forks means more SSH connections,
more Python processes, more memory on the controller.
It does not restrict which hosts are changed, or when. With no serial,
a play against 300 hosts and forks: 5 still changes all 300 hosts; it
merely takes longer to get through them, in waves of five, with no gap
in which anything is evaluated.
There is one honest interaction: forks caps parallelism inside a
batch. With serial: 25 and forks: 5, the batch of 25 is worked
through five at a time, so the batch takes five waves. If your batch is
larger than your forks, the batch duration is set by forks, and that
matters for window arithmetic. It still does not change what is patched.
$ ansible-config dump --only-changed | grep -i forksDEFAULT_FORKS(/srv/ansible/ansible.cfg) = 25If that command prints nothing, forks is at the built-in default of 5,
and a 300-host patch run is going to take considerably longer than
whoever scheduled it expects.
Putting the three numbers into a shape
Worked example. A 300-host web tier, capacity requires 80% of instances at peak, one host takes seven minutes end to end including reboot, and the window is six hours.
- Capacity ceiling: 20% of 300 is 60 hosts. That is the absolute maximum batch, and it assumes every host in the batch is down for the whole seven minutes, which is the honest assumption for a reboot.
- A batch of 60 gives five batches after the canary phases, roughly 35 minutes of hosts-down time plus soak. Comfortably inside six hours.
- But 60 is the ceiling, not the plan. A ceiling batch leaves no headroom for a host that fails to come back. Halving it to 30 costs another 35 minutes and keeps 10% of the fleet as slack.
That gives serial: [1, 5, 30] and max_fail_percentage: 0. Seventeen
batches, about two hours of moving parts, and a run that stops on the
first host that does not come back.
Knowledge check
Knowledge check · 4 questions
Q1. A patch play runs with serial: 4 and max_fail_percentage: 25. One host in a batch of four fails. What happens?
Q2. An engineer argues that setting forks: 2 makes a patch play safe because only two hosts are worked on at a time. What is wrong with this?
Q3. Which of these are legitimate reasons to keep the first batch of a patch run at exactly one host? Select all that apply.
Q4. max_fail_percentage: 10 on a 300-host fleet means the run stops once thirty hosts have failed.
Passing score: 75%. Answers are checked in this browser.