Skip to main content
RunBook Academy

AnsibleXLVIII · Maintenance Windows and RollbackMaintenance windows and rollback

Reconciling a fleet after a partial rollback

Expert⏱ ~30 minansible-playbookansible-inventory

What you'll learn

  • Enumerate the populations an aborted run leaves and build the host list for each
  • Use check mode against each population to prove convergence without changing anything
  • Choose a reference state deliberately rather than defaulting to the newest revision
  • Produce a reconciliation report that accounts for every host in the resolved list

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.

The window aborted, the rollback ran on some hosts, and now somebody has to say what state the fleet is in. The instinctive model is two populations: rolled back, and not.

It is four.

PopulationHow it got thereWhat is true of it
OldNever entered the playPre-change state, and nobody looked at it
NewChanged and validatedPost-change state, confirmed
PartialIn the failing batch when the play endedSome tasks applied, some not — per host
Rolled backThe reverse procedure ranClaims to be pre-change state

The last two are the work, and the fourth is not as safe as it sounds: “the reverse procedure ran” is a claim about a playbook exit code, not about a host.

Building the host list for each population

Start from the denominator. The recap is not it.

Read-only / Safeestablish the population sets
# The denominator: what the window was supposed to touch.
ansible-playbook -i inventories/prod site.yml \
--limit 'appservers:!maintenance' --list-hosts > window-hosts.txt

# The full inventory, for cross-checking that nothing has been forgotten.
ansible-inventory -i inventories/prod --graph appservers

# The recap gives you only the hosts that entered the play. Everything in
# window-hosts.txt with no recap line is population "old".

The set arithmetic is the whole exercise, and it is worth doing in a file rather than in your head at 03:20:

  • old = window hosts − hosts appearing in the recap
  • new = hosts with failed=0 that reached the final task
  • partial = hosts in the recap that did not reach the final task
  • rolled back = hosts the reverse procedure was run against

Two of those definitions are unsatisfying, and that is honest. “Reached the final task” is not something the recap reports directly — the ok count is a per-host task count, and comparing it against an expected number is fragile because skipped tasks vary by host. This is the argument for the change play writing a completion marker as its last task, which turns population membership into a fact you can query rather than a number you infer.

Choosing the reference state

The decision that must be made before any reconciliation runs, and the one that gets made by default.

The default is “converge everything to the newest revision”, because that is what running the playbook does. If the newest revision is the change that just failed, the default is to re-apply the failed change to the whole fleet, including the hosts that were successfully rolled back.

Three candidate references, and the criteria for choosing:

ReferenceChoose it whenWhat it costs
Pre-changeThe change failed for a reason not yet understoodThe rolled-forward hosts must be rolled back too, including any that hit the point of no return
Post-changeThe failures were environmental and the change is soundThe old and partial hosts must complete the change, in a window you may not have
A third stateThe change was sound but needs a fixA new change, planned properly, rather than a reconciliation

The third row is the one to take seriously. “Fix it and re-run against the stragglers” during the same window is how a controlled abort turns into an uncontrolled change. The abort criteria fired; the correct response is usually to reconcile to a known state and go home.

Proving convergence without changing anything

Once the reference is chosen, check mode against each population tells you the distance to it.

Read-only / Safemeasure the gap before closing it
# Population "partial" - the important one. Expect a mixed picture.
ansible-playbook -i inventories/prod site.yml \
--limit @partial-hosts.txt --check --diff

# Population "rolled back" - expect changed=0 if the reverse worked.
ansible-playbook -i inventories/prod site.yml \
--limit @rolledback-hosts.txt --check --diff

# Population "old" - expect a uniform diff, identical on every host.
ansible-playbook -i inventories/prod site.yml \
--limit @old-hosts.txt --check --diff

Reading each result:

Rolled back, changed=0: the reverse procedure converged those hosts to what the reference revision declares. That is real evidence and it is bounded by what the role manages — the previous lesson on rollback patterns explains what it does not cover.

Rolled back, changed>0: the reverse did not fully land. Worth stopping on, because the plan currently believes those hosts are safe.

Old, uniform diff: expected and reassuring. Every host in that population shows the same diff, because they are all in the same untouched state.

Old, non-uniform diff: those hosts were not identical before the window either. This is pre-existing drift, discovered by the incident, and it should be recorded as a separate finding rather than fixed silently during a reconciliation.

Partial, anything: read it host by host. This population has no uniform state by definition.

The report the change board is owed

Not a narrative. A table with one row per host in the resolved window list, and no host missing.

FieldContent
HostFrom the window host list, not the recap
Populationold / new / partial / rolled back
EvidenceThe fact that establishes it — package version, change-state file, check-mode result
Action takenReconciled to X, or deferred
OutstandingWhat is still true and should not be

Three properties make it useful:

It is complete. Every host in the window list appears. A host with no information appears with “unknown” in the population column, which is a finding rather than an omission.

It states evidence, not belief. “Rolled back” is a belief. “acme-app 2.4.1-1 per package_facts, changed=0 in check mode against the pre-change revision” is evidence.

It names what is still outstanding. Hosts deferred, drift discovered, the point-of-no-return hosts that cannot be returned. These become work items, and an item that is not in the report is an item nobody will do.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A change play aborted mid-batch. A host in the failing batch was changed by task 4, which notified a service restart handler, then the play ended at task 6. What is that host state?

  2. Q2. Running --check --diff against the rolled-back population returns changed=0 on every host. What does that establish? Select all that apply.

  3. Q3. Re-running the change play against a partially changed host is safe whenever the role is idempotent, because idempotence guarantees convergence regardless of the starting state.

  4. Q4. A fleet will stay deliberately diverged for a week while the failed change is investigated. Which risk most often goes unnoticed?

Passing score: 75%. Answers are checked in this browser.