Skip to main content
RunBook Academy

AnsibleXL · Patch and Reboot ManagementPatch and Reboot Management

Sizing the patch run before you write it

Advanced⏱ ~24 minansible-playbookansible-inventory

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

Not yet marked complete on this device.

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.

Service impact possiblethe shape of a fleet patch play
- 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.yml

max_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.

Read-only / Safefinding out which host is the canary before you run
ansible-inventory -i inventories/production --list --yaml --limit webservers
ansible-playbook -i inventories/production patch.yml --limit webservers --list-hosts

Two things destroy this property, and both are worth checking for in review:

  • order: shuffle on 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.

Read-only / Safethe boundary, executed on ansible-core 2.21.3
$ ansible-playbook -i inv10.ini mfp.yml -e mfp=25   # then -e mfp=24
max_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=2

At 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.

Read-only / Safewhat the controller is configured to do
$ ansible-config dump --only-changed | grep -i forks
DEFAULT_FORKS(/srv/ansible/ansible.cfg) = 25

If 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

  1. Q1. A patch play runs with serial: 4 and max_fail_percentage: 25. One host in a batch of four fails. What happens?

  2. 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?

  3. Q3. Which of these are legitimate reasons to keep the first batch of a patch run at exactly one host? Select all that apply.

  4. 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.