Skip to main content
RunBook Academy

← All break/fix scenarios in Ansible

advancedrollout~40 min

Break/Fix: the canary node patched perfectly and then the cluster lost quorum

Reported symptoms

  • A seven-node consensus cluster lost quorum during a routine patch window and stayed down for eleven minutes
  • The first node patched and rejoined cleanly, which was taken as evidence that the patch was safe
  • The next stage of the run took several nodes out simultaneously
  • Every dependent service failed at the same moment because the cluster stopped accepting writes
  • The play was not changed; it was reused from the web tier where it has always been safe
  • A pre-flight check confirmed the correct seven hosts were targeted, and it was right

Evidence

  • · `ansible-playbook ... --list-hosts` reports seven hosts and gives no indication of how they will be grouped
  • · The run output shows the PLAY header three times, with one host in the first group and five in the second
  • · The play sets `serial` to a list rather than to a single value
  • · The list was copied from a web-tier playbook where the same values are appropriate
  • · The cluster requires four of seven members for quorum, so five simultaneously unavailable members breaks it
  • · The cluster log records the loss of quorum within seconds of the second stage starting
  • · No task in the play checks cluster health between stages
Diagnosis and resolutionclick to reveal

Root cause

`serial` accepts a list, and a list means a ramp: the first value is the size of the first batch, the second value the size of the second, and so on. The value was copied from a web-tier playbook where a ramp is exactly right - patch one host, confirm nothing broke, then move faster. On a stateless tier behind a load balancer the ramp is a safety feature. On a seven-node consensus cluster that needs four members to hold quorum, the second stage takes five nodes at once and the cluster stops. Nothing in the play describes that constraint, and nothing checks for it: `--list-hosts` reports the seven hosts correctly and says nothing about batching, so the pre-flight check that everybody trusts is silent on the only thing that mattered. The successful canary made it worse rather than better, because it supplied genuine evidence about the patch and no evidence at all about the batch size that followed it - and it arrived immediately before the stage that caused the outage, at the moment when confidence was highest.

Remediation

Set `serial: 1` for any cluster whose members must agree with each other, and say why in a comment next to it. One at a time is not a performance compromise on a consensus cluster; it is the only batch size that preserves the invariant. Add a health gate between members that requires the cluster to be fully healthy - all members present, quorum held, no unsynchronised replicas - before the next node is touched, and make that gate able to stop the play. Recover the current outage first by restoring enough members to reach quorum, then let the cluster resynchronise and confirm it before continuing any patching. Do not resume the play from where it stopped; restart the patch cycle deliberately once the cluster is healthy.

Verification

The decisive check is behavioural: on a staging cluster, run the corrected play and confirm the run output shows one host per batch, then stop one member by hand mid-run and confirm the health gate refuses to proceed to the next. A gate that has never blocked anything has not been shown to work. Confirm quorum and full member health from the cluster itself between every node, using the cluster tooling rather than inferring from the Ansible run. After the incident, confirm every member is present, synchronised and on the intended version before declaring the patch complete.

Prevention

Never reuse a batching policy across tiers with different failure semantics. A stateless tier behind a load balancer degrades gracefully as members are removed; a consensus cluster fails completely below a threshold, and the correct batch size for the second is one. Record the quorum arithmetic in the play as a comment and as an assertion, so the constraint is visible to the next person who reads it. Put a health gate between every member and require it to be able to fail. Treat a successful canary as evidence about the change only, never about the batching that follows, and pause deliberately between stages of any ramp. Finally, print the batch plan before running anything that writes, because `--list-hosts` shows you the hosts and hides the grouping, and the grouping is what decides whether a cluster survives.

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

Read-only / Safeseven hosts, and not one word about how they will be grouped
$ ansible-playbook -i inventory patch.yml --limit cluster --list-hosts
playbook: patch.yml

play #1 (cluster): Patch the cluster	TAGS: []
  pattern: ['cluster']
  hosts (7):
    etcd05
    etcd02
    etcd06
    etcd07
    etcd04
    etcd01
    etcd03
Read-only / Safeone, then five, then one
$ grep -E '^PLAY \[|^ok: |^changed: ' logs/patch-0200.log | head -14
PLAY [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]
Read-only / Safeserial as a list is a ramp, not a batch size
$ grep -n -A6 'hosts: cluster' patch.yml
3:  hosts: cluster
4:  serial:
5:    - 1
6:    - 5
7:    - 100%
8:  gather_facts: true
Read-only / Safethe commit message says exactly what everybody believed
$ git log --oneline -1 --format='%h %s' -- patch.yml; git log -1 --format='%b' -- patch.yml | head -2
7d31a90 patch: reuse the web tier patch play for the cluster group
Same play, just a different --limit. Nothing to change.
Read-only / Safethe cluster describes the fault precisely
$ 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
Read-only / Safethe play has no idea it is patching a cluster
$ grep -c 'cluster health\|member list\|quorum' patch.yml
0

Work the evidence before reading on

The play targeted the right seven hosts. The canary succeeded. Nothing was modified.

  1. Read the serial value as data rather than as a setting. It is a list of three numbers. What does each number correspond to in the run output?
  2. The cluster needs four of seven members. How many were unavailable during the second stage?
  3. --list-hosts was 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

  1. 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.
  2. 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.
  3. Do not resume the play. It would continue from the next batch, and the cluster state no longer matches what the play assumes.
  4. 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.
  5. Change serial to 1 for this group and add a comment stating the quorum arithmetic, so the constraint is visible to the next reader.
  6. 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.
  7. 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 serial is caught rather than discovered.
  8. Restart the patch cycle deliberately once the cluster is fully healthy, one node at a time, with the gate in place.

Verification

  1. 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.
  2. 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.
  3. The batch-size assertion can fail. Set serial to 2 on a scratch branch and confirm the play refuses to start.
  4. Quorum is confirmed from the cluster between every node, using the cluster tooling. The Ansible run reporting ok is not evidence about cluster state.
  5. Every member is present, synchronised and on the intended version after the patch cycle, checked from the cluster rather than from the package manager.
  6. Dependent services recovered. Confirm from the services that depend on the cluster, not only from the cluster itself.
  7. 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: 1 for 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_percentage and any_errors_fatal from the failure semantics of the tier being patched.
  • Assert the batch size in the play, so an edit to serial is 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-hosts shows 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.