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.
Population
How it got there
What is true of it
Old
Never entered the play
Pre-change state, and nobody looked at it
New
Changed and validated
Post-change state, confirmed
Partial
In the failing batch when the play ended
Some tasks applied, some not — per host
Rolled back
The reverse procedure ran
Claims 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— ansible-inventory and --list-hosts resolve patterns without connecting to anything. Everything here is read-only.
# 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:
Reference
Choose it when
What it costs
Pre-change
The change failed for a reason not yet understood
The rolled-forward hosts must be rolled back too, including any that hit the point of no return
Post-change
The failures were environmental and the change is sound
The old and partial hosts must complete the change, in a window you may not have
A third state
The change was sound but needs a fix
A 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— --check --diff changes nothing. Run it per population, because the interesting output is different for each one.
# 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.
Field
Content
Host
From the window host list, not the recap
Population
old / new / partial / rolled back
Evidence
The fact that establishes it — package version, change-state file, check-mode result
Action taken
Reconciled to X, or deferred
Outstanding
What 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
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?
Q2. Running --check --diff against the rolled-back population returns changed=0 on every host. What does that establish? Select all that apply.
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.
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.