Skip to main content
RunBook Academy

← All break/fix scenarios in Ansible

advancedcontroller~35 min

Break/Fix: the laptop lid closed during a fleet change and nobody knows which hosts got what

Reported symptoms

  • A change across 180 hosts stopped mid-run when the operator lost their network connection
  • There is no play recap, so there is no per-host summary of what completed
  • The terminal scrollback ends partway through a task and the rest is gone
  • No retry file was produced, so there is no list of hosts to resume from
  • Some hosts have the new configuration, some have the old, and at least one is partway through
  • The obvious next step - rerun the whole thing - is being proposed and nobody can say whether it is safe

Evidence

  • · The terminal shows output up to a point and then nothing; the SSH session to the controller died
  • · `ansible-config dump | grep RETRY_FILES` shows retry files disabled, which is the default
  • · No log file exists because the run was started interactively with no logging configured
  • · The play used the default `linear` strategy, so every host completes a task before any host starts the next
  • · The last task visible in scrollback is the one that writes the configuration file
  • · `ansible all -m ansible.builtin.stat -a path=/etc/app/app.conf` returns a mixture of old and new checksums
  • · One host shows a temporary file left in the remote temporary directory
Diagnosis and resolutionclick to reveal

Root cause

The controller process was killed when its session died, which stops the run immediately and produces no recap. That is the whole mechanism, and everything difficult about the incident follows from what the run left behind rather than from why it stopped. There is no per-host summary, because the recap is printed at the end and there was no end. There is no resume list, because retry files are disabled by default in current ansible-core and nothing else records progress. There is no log, because the run was started interactively from a laptop with no logging configured and no persistent session. The fleet is therefore in a state that exists nowhere except on the fleet itself, and the only honest way to learn it is to go and measure it. The default `linear` strategy is the one piece of good news and it is easy to overlook: it synchronises hosts at every task, so the fleet is split between at most two adjacent tasks rather than scattered across the whole play, which bounds the investigation considerably.

Remediation

Measure before acting. Determine, per host, which side of the interrupted task it is on, using read-only checks against the specific artefacts the play writes - a file checksum, a package version, a service state. Then decide whether a rerun is safe, which is a question about the play rather than about the incident: a play whose every task is idempotent can be rerun, and a play containing an append, a counter, a one-shot migration or a command without a guard cannot. Clean up any host left mid-task, including temporary files in the remote temporary directory, before rerunning anything. Then rerun with a host list derived from the measurement rather than from the inventory, and do it from a session that cannot die with a laptop.

Verification

Every host in the inventory must appear in the measurement table with a definite state; the check is that the table is complete, not that a subsequent run was green. After the rerun, confirm convergence by running once more and requiring `changed=0`, which is the only evidence that the fleet has actually settled rather than that one more run completed. Prove the new logging works by killing a run deliberately on a scratch host and confirming the log contains enough per-host detail to reconstruct progress - a logging configuration that has never survived a failure has not been tested.

Prevention

Never run a fleet change from a session that can disappear. A terminal multiplexer on the controller, or a scheduler that owns the process, turns a lost connection into a reconnect rather than an incident. Configure a log path so every run leaves a durable record independent of the terminal, and keep the callback output detailed enough to reconstruct per-host progress. Design plays so that a rerun is always safe, and state in the runbook whether it is, because that question is asked under pressure and answered from memory. Prefer guarded, idempotent tasks over one-shot commands, and where a genuinely one-shot action is unavoidable, record its completion on the host so a rerun can detect it. Finally, for a change that must be resumable, capture progress deliberately rather than relying on the retry file that no longer exists by default.

Reported symptoms

An engineer starts a configuration change across 180 hosts from a laptop, over the office wireless, at 16:40 on a Friday. At 16:52 the wireless drops for eleven seconds.

The terminal shows output up to a point and then stops. There is no play recap. Reconnecting to the controller shows no ansible-playbook process running.

The immediate questions:

  • How many hosts completed?
  • Which ones?
  • Is any host in a half-changed state?
  • Can we just run it again?

Nobody can answer any of them from anything on the controller. The scrollback is gone with the terminal, there is no log file, and there is no retry file.

The proposal on the table at 16:58 is to rerun the play against the full inventory and let it converge.

Evidence provided

Read-only / Safethe run died with the session
$ pgrep -af ansible-playbook
Read-only / Safeno log, no retry file, and one useful default
$ ansible-config dump | grep -E 'RETRY_FILES|LOG_PATH|DEFAULT_STRATEGY'
DEFAULT_LOG_PATH(default) = None
DEFAULT_STRATEGY(default) = linear
RETRY_FILES_ENABLED(default) = False
RETRY_FILES_SAVE_PATH(default) = None
Read-only / Safenothing was recorded anywhere
$ ls -la ~/*.retry /var/log/ansible/ 2>&1 | head -3
ls: cannot access '/home/deploy/*.retry': No such file or directory
ls: cannot access '/var/log/ansible/': No such file or directory
Read-only / Safethe only source of truth left is the fleet itself
$ ansible all -i inventory -b -m ansible.builtin.stat -a 'path=/etc/app/app.conf checksum_algorithm=sha256' -o | awk '{print $1}' | head -3
app001
app002
app003
Read-only / Safe112 done, 67 not, and 180 hosts in the inventory
$ ansible all -i inventory -b -m ansible.builtin.stat -a 'path=/etc/app/app.conf checksum_algorithm=sha256' 2>/dev/null | grep -o '"checksum": "[a-f0-9]*"' | sort | uniq -c
    112 "checksum": "9f2c...new"
67 "checksum": "41ab...old"
Read-only / Safeone host was interrupted mid-task
$ ansible all -i inventory -b -m ansible.builtin.find -a 'paths=/var/tmp patterns=ansible-tmp-* file_type=directory' -o 2>/dev/null | grep -v '"matched": 0'
app144 | SUCCESS => {"matched": 1, "files": [{"path": "/var/tmp/ansible-tmp-1786553521.4-2210-88"}]}

Work the evidence before reading on

112 plus 67 is 179. The inventory has 180 hosts.

  1. Which host is missing from the checksum tally, and what does the temporary directory on app144 tell you about it?
  2. The play used the linear strategy. What does that guarantee about how far apart two hosts can be in the play?
  3. There is no recap and no retry file. Is there any artefact on the controller that records which hosts completed?

Before continuing: before deciding whether to rerun, what property of the playbook do you need to establish, and where would you look to establish it?

Root cause

1. The run died with the terminal

The controller process was a child of the SSH session. When the session went, the process went, and everything the run held in memory went with it.

There is no partial recap because the recap is printed once, at the end. There is no partial log because no log path was configured. There is no resume list because retry files are disabled by default in current ansible-core - a .retry file is something older documentation promises and modern installations do not produce unless asked.

None of that is a malfunction. It is the shape of an interactive run, and it is only visible when a run does not finish.

2. The fleet state exists only on the fleet

This is the part worth internalising, because it changes what the first action should be.

After an interrupted run there is no authoritative record on the controller. The record is the fleet: which hosts have the new file, which have the old, and which are in neither state. It has to be measured, and measuring it is cheap - read-only modules against the specific artefacts the play writes.

The measurement is also the only thing that makes the rerun decision defensible. Without it, “run it again” is a guess about 180 machines.

3. linear bounds the problem, and that is genuinely helpful

Under the default linear strategy every host completes a task before any host begins the next. So at any instant the fleet is split between at most two adjacent tasks: those that finished task N and those still on it.

That is why the checksum tally splits cleanly into two groups plus one straggler. The investigation is not “which of 40 tasks did each host reach” but “which side of one task is each host on”, which is a question you can answer with a single read-only command across the fleet.

Resolution

  1. Do not rerun yet. Say so explicitly, because the proposal is already on the table and it is the natural instinct.
  2. Measure the fleet. Use read-only modules against the specific artefacts the play writes - file checksums, package versions, service states - and build a table with one row per inventory host.
  3. Account for every host. In this incident 179 of 180 fall into two clean groups; the missing one is the host that was interrupted mid-task and it needs individual attention.
  4. Inspect the interrupted host directly. Establish whether the module completed its work before the process died, using the artefact rather than the leftover temporary directory, which cannot tell you.
  5. Clean up leftover temporary directories on any host that has them, so the next investigation is not confused by evidence from this one.
  6. Read the play and decide whether a rerun is safe. Every task idempotent means yes; anything that appends, fires once, or runs unguarded means no, and those tasks need to be excluded or handled individually.
  7. Rerun against the hosts that need it, derived from the measurement rather than from the inventory, from a session that cannot die - a multiplexer on the controller or a scheduler that owns the process.
  8. Converge once more afterwards and require changed=0, which is the evidence that the fleet has settled rather than that another run happened to finish.

Verification

  1. The measurement table is complete. One row per inventory host, each with a definite state. A table with 179 rows for 180 hosts is the incident continuing.
  2. The interrupted host is resolved individually, on the evidence of the artefact rather than on an assumption about how far the task got.
  3. No leftover temporary directories remain. A find across the fleet for the temporary directory pattern returns nothing.
  4. The fleet has converged. A final run reports changed=0 on every host; a run that completes is not the same as a fleet that agrees.
  5. The new logging works under failure. Start a run on a scratch host, kill it deliberately, and confirm the log contains enough per-host detail to reconstruct progress. This is the check that can fail, and a logging setup that has never survived a failure is untested.
  6. The run survives a lost connection. Disconnect from the controller mid-run and confirm the run continues and can be reattached.
  7. The runbook answers the rerun question. It states, for this play, whether a rerun from an unknown state is safe and which tasks are the exceptions.

Prevention

  • Run fleet changes from a session that cannot disappear. A terminal multiplexer on the controller, or a scheduler that owns the process, turns a dropped connection into a reconnect.
  • Configure a log path so every run leaves a durable record independent of the terminal, and keep enough per-host detail in it to reconstruct progress from a partial file.
  • Design plays to be rerunnable, and say so in the runbook. The answer to “is a rerun safe” should be written down before it is needed.
  • Guard every unguarded command. creates and removes turn a one-shot command into an idempotent task and make the whole play resumable.
  • Where an action genuinely can only happen once, record its completion on the host so a rerun can detect it. A marker file is unglamorous and it converts the hardest case into an ordinary one.
  • If a change must be resumable, capture progress deliberately rather than relying on retry files, which are disabled by default and are not a resume mechanism anyone should be depending on.
  • Prefer smaller batches for changes that write. A serial value bounds how many hosts can be mid-change when something interrupts the run.