Reported symptoms
The patch window opens at 02:00. The seven-node consensus cluster that holds service discovery and configuration for the whole estate is scheduled first.
At 02:04 the first node has been patched, rebooted and rejoined. The cluster reports all seven members healthy. The engineer watching the run notes that the canary went perfectly.
At 02:06 every dependent service in the estate starts failing. The cluster is not accepting writes. It stays that way for eleven minutes.
The play was not modified. It is the same play that patches the web tier, reused because it already had batching, health checks and a reboot loop - all the things a patch play needs.
A pre-flight check confirmed the correct seven hosts were targeted. It was correct.
Evidence provided
$ ansible-playbook -i inventory patch.yml --limit cluster --list-hostsplaybook: patch.yml
play #1 (cluster): Patch the cluster TAGS: []
pattern: ['cluster']
hosts (7):
etcd05
etcd02
etcd06
etcd07
etcd04
etcd01
etcd03$ grep -E '^PLAY \[|^ok: |^changed: ' logs/patch-0200.log | head -14PLAY [Patch the cluster] *******************************************************
changed: [etcd01]
PLAY [Patch the cluster] *******************************************************
changed: [etcd02]
changed: [etcd03]
changed: [etcd04]
changed: [etcd05]
changed: [etcd06]
PLAY [Patch the cluster] *******************************************************
changed: [etcd07]$ grep -n -A6 'hosts: cluster' patch.yml3: hosts: cluster
4: serial:
5: - 1
6: - 5
7: - 100%
8: gather_facts: true$ git log --oneline -1 --format='%h %s' -- patch.yml; git log -1 --format='%b' -- patch.yml | head -27d31a90 patch: reuse the web tier patch play for the cluster group
Same play, just a different --limit. Nothing to change.$ ssh etcd01 'journalctl -u etcd --since 02:05 --until 02:07 | grep -i quorum' Aug 11 02:06:03 etcd01 etcd[912]: lost quorum: 2 of 7 members reachable, 4 required
Aug 11 02:06:03 etcd01 etcd[912]: refusing writes$ grep -c 'cluster health\|member list\|quorum' patch.yml0Work the evidence before reading on
The play targeted the right seven hosts. The canary succeeded. Nothing was modified.
- Read the
serialvalue as data rather than as a setting. It is a list of three numbers. What does each number correspond to in the run output? - The cluster needs four of seven members. How many were unavailable during the second stage?
--list-hostswas run as the pre-flight and reported seven hosts. Was it wrong?
Before continuing: what would you have had to run, before the play, to see that the second stage takes five nodes at once?
Root cause
1. serial as a list means a ramp
serial takes either a single value, which is the size of every batch,
or a list, which is the size of each batch in turn. A list is a ramp:
start small, confirm nothing broke, then accelerate.
serial:
- 1
- 5
- 100%
Seven hosts under that policy split into batches of one, five and one -
which is exactly the shape the run output shows, once you know to read
the repeated PLAY header as a batch boundary.
On the web tier this is a good policy. One host is patched, the load balancer absorbs its absence, the operator confirms nothing is broken, and the remainder proceed quickly. Losing five of seven web servers is a capacity problem that a load balancer manages.
2. A consensus cluster fails at a threshold, not gradually
Seven members require four to be present for quorum. Removing one member is unremarkable; removing five is total failure. There is no degraded middle state - the cluster accepts writes or it does not.
That is the entire difference between the two tiers, and it is not
expressed anywhere in the play. serial: [1, 5, 100%] is not wrong in
general. It is catastrophic for this specific group and there is nothing
in the file that says so.
3. The successful canary supplied confidence about the wrong thing
The first node patched, rebooted and rejoined perfectly. That is genuine evidence: the package works, the reboot works, the rejoin works, the play is correct.
It is no evidence whatever about what happens when five nodes do the same thing simultaneously. And it arrived thirty seconds before the stage that caused the outage, at the moment when everybody watching was most reassured.
A ramp is designed to convert confidence into speed. When the risk being managed is per-host, that is sound. When the risk is about how many hosts are affected at once, the canary proves nothing about the stage that follows it, and the ramp accelerates into the danger rather than away from it.
Resolution
- Restore quorum first. Bring enough members back to reach the threshold; which members and in what order depends on the cluster technology and belongs in its own runbook.
- Let the cluster resynchronise and confirm it from the cluster tooling. A member that is running is not necessarily a member that has caught up, and a cluster at quorum with lagging replicas is still fragile.
- Do not resume the play. It would continue from the next batch, and the cluster state no longer matches what the play assumes.
- Confirm which nodes actually received the patch and which did not, from the nodes rather than from the run log, since the run was interrupted by the outage rather than by a failure.
- Change
serialto 1 for this group and add a comment stating the quorum arithmetic, so the constraint is visible to the next reader. - Add a health gate between members that requires all members present and quorum held before the next node is touched, and make it able to fail the play.
- Add an assertion at the start of the play that the batch size cannot exceed the tolerable number of unavailable members, so a future edit to
serialis caught rather than discovered. - Restart the patch cycle deliberately once the cluster is fully healthy, one node at a time, with the gate in place.
Verification
- The batch plan is one host at a time. Run the corrected play on a staging cluster and confirm the PLAY header appears once per host in the run output.
- The health gate can fail. Stop one member by hand during a staging run and confirm the play refuses to proceed to the next node. This is the check that can fail and the one the original play did not have.
- The batch-size assertion can fail. Set
serialto 2 on a scratch branch and confirm the play refuses to start. - Quorum is confirmed from the cluster between every node, using the cluster tooling. The Ansible run reporting
okis not evidence about cluster state. - Every member is present, synchronised and on the intended version after the patch cycle, checked from the cluster rather than from the package manager.
- Dependent services recovered. Confirm from the services that depend on the cluster, not only from the cluster itself.
- The runbook records the arithmetic. The number of members, the quorum threshold, and the maximum number that may be unavailable at once are written down where the next operator will read them.
Prevention
- Set
serial: 1for any tier with a quorum, a write threshold or an active-passive pair, and write the arithmetic beside it:
# 7 members, quorum 4, so at most 3 may be unavailable.
# One at a time is the only batch size that also survives an
# unplanned failure during the window.
serial: 1
- Never reuse a batching policy across tiers. Reuse the tasks; re-derive
serial,max_fail_percentageandany_errors_fatalfrom the failure semantics of the tier being patched. - Assert the batch size in the play, so an edit to
serialis caught by the play rather than by the cluster. - Put a health gate between members and require it to be able to fail. A gate that only ever passes is decoration, and on a cluster it is the only thing standing between a slow patch and an outage.
- Print the batch plan before running anything that writes.
--list-hostsshows the hosts and hides the grouping, and the grouping is what decides whether a threshold system survives. - Treat a successful canary as evidence about the change, never about the batching that follows it. On a ramp, the canary is immediately followed by the riskiest stage.
- Pause deliberately between stages of any ramp, with a check that a human or a gate has to pass. Automatic acceleration is the feature that turns a good first result into a bad second one.