Skip to main content
RunBook Academy

← All break/fix scenarios in Ansible

intermediateplaybook~30 min

Break/Fix: 200 hosts reported a successful agent rollout and none of them has the agent

Reported symptoms

  • A security agent was rolled out to 200 hosts and the run reported success on every one
  • An audit three weeks later finds the agent installed on zero hosts
  • The playbook is correct and installs the agent perfectly when the task is run alone
  • Every nightly converge since has also reported success
  • The play recap shows `failed=0` on all 200 hosts, every night
  • The run does report `ok` rather than `changed` on the install task, which nobody found notable

Evidence

  • · The install task carries `ignore_errors: true`
  • · The run output shows `...ignoring` after the task result on every host
  • · The play recap reports `ignored=1` per host, in a column nobody reads
  • · Running the same task without `ignore_errors` fails immediately with a repository authentication error
  • · The vendor repository credential expired three weeks before the rollout
  • · `git blame` dates the `ignore_errors` line to eight months ago with a commit message about a flaky host
  • · No task after the install checks that the agent is present
Diagnosis and resolutionclick to reveal

Root cause

The install task carries `ignore_errors: true`, added eight months earlier because one host failed transiently during an unrelated change. From that moment the task could no longer fail the play, and when the vendor repository credential expired the install began failing on every host - silently, with the failure downgraded to an ignored result and the play continuing to a successful recap. The only visible trace is a `...ignoring` line in the output and an `ignored=1` column in the recap, neither of which any dashboard, pipeline or human was reading. Nothing else in the play notices, because there is no task after the install that checks whether the agent exists; the play asserts that Ansible ran, not that the agent is there. The result is the most expensive failure mode available in configuration management: an outcome that is wrong on every host, reported as correct on every host, for three weeks, with a green audit trail supporting it.

Remediation

Remove `ignore_errors: true` and let the task fail, then fix the underlying failure, which is an expired repository credential and not an Ansible problem at all. Where a genuinely transient failure needs tolerating, express that precisely with a retry policy or with `failed_when` naming the specific condition, rather than by discarding every possible error. Add a verification task after the install that asserts the agent is installed and running, so a future silent failure fails the run. Then treat the three-week gap as a security finding in its own right - 200 hosts were unmonitored while the compliance record said otherwise - and check the rest of the repository for the same pattern, because a line added to make a run go green is rarely added only once.

Verification

The decisive check is the verification task itself, and it has to be shown to fail: remove the agent from a scratch host, run the play with the repository credential still broken, and confirm the run reports a failure rather than success. Confirm the agent is present and running on all 200 hosts by querying the hosts, and independently by confirming the agents are reporting in to their management console. Confirm the recap shows `ignored=0` across the fleet. Then run the play once more and require `changed=0`, so the fleet is demonstrably converged rather than merely repaired.

Prevention

Never use `ignore_errors: true` on a task whose outcome matters. It converts every possible failure of that task - transient, permanent, catastrophic and as yet unimagined - into silence, in exchange for tolerating one that happened once. Where retries are genuinely appropriate, retry explicitly with a bounded count and a condition. Where a specific non-zero outcome is acceptable, name it with `failed_when` so every other outcome still fails. Assert the end state after anything that installs, configures or starts something, because a task reporting success and the outcome existing are different claims. Alert on `ignored` in the recap, and fail CI on any new `ignore_errors` that does not carry a comment explaining precisely which error is being tolerated and why.

Reported symptoms

A security agent was rolled out to 200 hosts. The rollout run was green: 200 hosts, failed=0, no errors, change record closed.

Three weeks later an audit queries the agent management console and finds 200 hosts missing. Not misconfigured, not stale - absent. The agent has never been installed on any of them.

Everything about the automation appears correct:

  • The playbook names the right package.
  • Running the install task on its own, by hand, produces a clear failure that explains itself immediately.
  • The nightly converge has run every night since and reported success every time.
  • No dashboard, alert or pipeline has ever flagged anything.

The only oddity, once somebody goes back to the log, is that the install task reports ok rather than changed - which on an idempotent task is what you would expect on a second run and is entirely unremarkable on a first.

Evidence provided

Read-only / Safethe failure is in the log, followed by the word that ends the story
$ grep -A3 'Install the security agent' logs/rollout.log | head -6
TASK [Install the security agent] **********************************************
fatal: [app001]: FAILED! => {"changed": false, "msg": "Failed to update apt cache: unauthorized"}
...ignoring
Read-only / Safefailed=0, and ignored=1 in a column nobody reads
$ tail -3 logs/rollout.log
PLAY RECAP *********************************************************************
app001                     : ok=12   changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=1
app002                     : ok=12   changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=1
Read-only / Safeone line
$ grep -n -A6 'Install the security agent' roles/security/tasks/main.yml
21:- name: Install the security agent
22:  ansible.builtin.package:
23:    name: vendor-agent
24:    state: present
25:  ignore_errors: true
Read-only / Safeeight months ago, for a reason that lasted one afternoon
$ git log -1 --format='%h %ad %s' --date=short -L21,25:roles/security/tasks/main.yml | head -2
b4419c7 2025-12-14 security: tolerate a flaky host during the December change
Read-only / Safethe actual problem, and it is not an Ansible problem
$ ansible app001 -i inventory -b -m ansible.builtin.command -a 'apt-get update' | tail -2
E: Failed to fetch https://repo.example.com/vendor/dists/stable/InRelease  401  Unauthorized
E: Some index files failed to download.
Read-only / Safenothing in the role ever checks that the agent exists
$ grep -c 'assert\|command:.*vendor-agent\|service_facts' roles/security/tasks/main.yml
0

Work the evidence before reading on

The failure is in the log. It has been in the log every night for three weeks. The run still ends failed=0.

  1. Read the three lines of the task output in order. What is the third one, and what does it do to the second?
  2. Look at the play recap columns. Which one is non-zero, and where in the line is it?
  3. The role installs something. What does it do afterwards to establish that the thing is there?

Before continuing: what did the commit eight months ago intend to tolerate, and what does the line actually tolerate?

Root cause

1. ignore_errors discards every error, not the one you had in mind

ignore_errors: true means the task cannot fail the play. Whatever goes wrong - a transient network blip, an expired credential, a missing package, a full disk, a repository that no longer exists - the result is downgraded to ignored and the play continues.

The commit that added it was addressing a real problem: one host failed transiently during an unrelated change in December, and the line made the run go green that afternoon. It was never removed, because nothing ever went wrong afterwards - or rather, nothing ever appeared to.

When the vendor repository credential expired three weeks ago, the task started failing on every host. The failure is not transient, it is not host-specific, and it is exactly the kind of thing a rollout must stop for. It was ignored, along with everything else.

2. The signals exist and nothing consumes them

Ansible does report this. The task output ends with ...ignoring, and the play recap carries an ignored=1 column.

Both are easy to miss for structural reasons rather than careless ones. The ...ignoring line is one line among hundreds and contains no word that anybody greps for. The ignored column is the last field of a recap line that begins with the number everybody looks at, which is failed=0.

Pipelines consume the exit code, which is 0. Dashboards count failed hosts, which is 0. The one number that is not zero is in neither.

3. Nothing asserted the outcome

The role installs the agent and then moves on. There is no task afterwards that checks whether the package is present, whether the service is running, or whether the agent has registered with its console.

Without that, the play asserts only that Ansible executed. The question the rollout actually needed answered - is the agent on this host - was never asked by anything until an auditor asked it three weeks later.

Resolution

  1. Raise the exposure first. 200 hosts have been without a security agent for three weeks while the compliance record said otherwise; that belongs with whoever owns the control, not in an Ansible ticket.
  2. Fix the underlying failure. The repository credential has expired, which is not an Ansible problem and would have been obvious on day one if the task had been allowed to fail.
  3. Remove ignore_errors: true from the task. If a transient failure genuinely needs tolerating, express it with bounded retries or with a failed_when naming the specific condition.
  4. Add a verification task after the install that asserts the package is present and the service is running, so a future silent failure fails the run.
  5. Roll out to a canary host and confirm both the install and the verification behave, before the fleet.
  6. Deploy to the fleet and confirm from the agent management console that all 200 hosts have registered, rather than from the Ansible run.
  7. Audit the repository for every other ignore_errors, and for the related suppressions - changed_when: false on tasks that change things, and conditions added to skip failing hosts.
  8. Add a CI rule that any new ignore_errors must carry a comment naming the specific error being tolerated, so the next December commit has to explain itself.

Verification

  1. The verification task can fail. Remove the agent from a scratch host, break the repository credential, and run the play: it must report a failure. This is the check that would have caught the incident on day one and it had never existed.
  2. The play fails when the install fails. With ignore_errors removed, confirm on that same scratch host that the run stops rather than continuing.
  3. The agent is present on every host. Query the package and the service state across the fleet, and confirm the count is 200.
  4. The agents are reporting in. Confirm from the management console, which is independent of both the hosts and Ansible, and is the check the auditor performed.
  5. The recap is clean. ignored=0 across the fleet, and a grep of the run log for ignoring returns nothing.
  6. The fleet is converged. Run the play once more and require changed=0, so the state is stable rather than newly repaired.
  7. The audit of other suppressions is complete, with each remaining one either removed or annotated with the specific error it tolerates and the reason it still applies.

Prevention

  • Do not use ignore_errors: true on a task whose outcome matters. It trades every future failure for one past one.
  • Use the precise tool instead. Bounded retries with until for transience; failed_when for a named acceptable outcome; block and rescue when there is something to do about the failure.
  • Assert the end state after anything that installs, configures or starts something:
- name: Confirm the security agent is installed and running
  ansible.builtin.assert:
    that:
      - ansible_facts.packages['vendor-agent'] is defined
      - ansible_facts.services['vendor-agent.service'].state == 'running'
    fail_msg: "The security agent is not installed and running on {{ inventory_hostname }}."
  • Alert on ignored and rescued in the recap, not only on failed. They are the columns that record failures the play decided to survive.
  • Fail CI on a new ignore_errors without a comment naming the specific error and the reason it is acceptable. The comment is what makes the decision reviewable later.
  • Pair every suppression added under time pressure with a ticket to remove it, in the same commit. The urgency ends the same day; the line does not.
  • Verify rollouts at an independent system where one exists. The agent console knew the truth for three weeks and nobody was comparing it against the automation.