Skip to main content
RunBook Academy

AnsibleLII · Anti-PatternsAnti-patterns of execution

Anti-pattern: ignore_errors as a green-build button

Intermediate⏱ ~26 minbash

What you'll learn

  • Read a recap and identify a run that ignored failures
  • Explain how ignore_unreachable makes a fleet-wide outage report as success
  • Replace a blanket ignore with failed_when, rescue, or letting the task fail
  • Audit a repository for the pattern and rank the findings

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.

ignore_errors: true has a legitimate use and almost nobody reaches for it for that reason. It gets added because a task failed, the run stopped, somebody was in a hurry, and the keyword made the problem go away.

Then it gets added again. And the pipeline goes green, and stays green, and the estate underneath it stops being observable.

What it looks like in a repository

Service impact possiblethe anti-pattern - accumulated, not designed
- name: Stop the application
ansible.builtin.systemd_service:
  name: app
  state: stopped
ignore_errors: true

- name: Deploy the new artefact
ansible.builtin.unarchive:
  src: 'https://artifacts.example.com/app-{{ app_version }}.tar.gz'
  dest: /srv/app
  remote_src: true
ignore_errors: true

- name: Run the database migration
ansible.builtin.command:
  cmd: /srv/app/bin/migrate
ignore_errors: true

- name: Start the application
ansible.builtin.systemd_service:
  name: app
  state: started
ignore_errors: true

Read the sequence. The artefact fails to download; the migration runs against the old code; the start succeeds because the old version is still installed. The run is green, the deployment did not happen, and the database has been migrated for a version that is not deployed.

Each ignore_errors was added for a defensible local reason. The first one, most likely, because stopping an already-stopped service failed once.

The recap, and the exit code

Read-only / Safea failing task, ignored
$ ansible-playbook -i inventory/hosts.yml deploy.yml; echo exit=$?
TASK [Deploy the application] **************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "artefact checksum mismatch"}
...ignoring

TASK [Report success] **********************************************************
ok: [localhost] => {
  "msg": "deployment complete"
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1

exit=0

Two details worth having in mind at 03:00.

failed=0 and ignored=1. The recap does record it, in the last column, which almost nobody reads. Anything summarising runs by failed, or by exit code, sees a clean run.

Exit code 0. This is what CI reads, what a wrapper script reads, and what the monitoring check that watches for failed runs reads. The deployment failed and every automated observer was told it succeeded.

The cousin: ignore_unreachable

This one is worse, and its output is the most alarming in this part.

Read-only / Safea play with ignore_unreachable at play level
- name: Restart the fleet
hosts: web01.example.com,web02.example.com
gather_facts: false
ignore_unreachable: true
tasks:
  - name: Restart the application
    ansible.builtin.ping:
Read-only / Safetwo hosts that were never contacted
$ ansible-playbook -i inventory/hosts.yml unreach.yml -T 3; echo exit=$?
TASK [Restart the application] *************************************************
fatal: [web02.example.com]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: connect to host 192.0.2.10 port 22: Connection timed out", "unreachable": true}
...ignoring
fatal: [web01.example.com]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: connect to host 192.0.2.10 port 22: Connection timed out", "unreachable": true}
...ignoring

PLAY RECAP *********************************************************************
web01.example.com          : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1
web02.example.com          : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1

exit=0

The three honest replacements

1. failed_when — be precise about what failure means

Most ignore_errors are really “this exit code is not a failure for my purposes”. Say that instead.

Configuration changea non-zero exit that is not an error
- name: Check whether the ruleset would load
ansible.builtin.command:
  cmd: /usr/sbin/nft -c -f /etc/nftables.conf
register: nft_check
changed_when: false
failed_when: nft_check.rc not in [0, 1]

The difference is total: ignore_errors discards every failure including the ones you have not thought of, while failed_when names the acceptable outcomes and leaves everything else fatal.

For the “stop an already-stopped service” case that starts most of these, the answer is usually neither keyword — a module with proper state semantics does not fail when the desired state already holds.

2. block/rescue — compensate, then decide

Service impact possiblea failure that is handled rather than hidden
- name: Deploy with a compensating action
block:
  - name: Deploy the new artefact
    ansible.builtin.unarchive:
      src: 'https://artifacts.example.com/app-{{ app_version }}.tar.gz'
      dest: /srv/app
      remote_src: true

  - name: Start the application
    ansible.builtin.systemd_service:
      name: app
      state: started
rescue:
  - name: Restore the previous artefact
    ansible.builtin.command:
      cmd: /srv/app/bin/rollback-to-previous
    changed_when: true

  - name: Fail the run deliberately, having compensated
    ansible.builtin.fail:
      msg: >-
        Deploy of {{ app_version }} failed on {{ inventory_hostname }};
        previous version restored. Run not marked successful.

The final fail is the part that matters and the part usually omitted. A rescue block that ends without failing marks the task as rescued and the play continues — which is correct when the compensation genuinely resolved the situation, and a quieter version of this anti-pattern when it did not.

3. Let it fail

Often the right answer. If the task failing means the change did not happen, the run should stop and the operator should know. Part XXIII’s argument in one line: a run that stops halfway is recoverable; a run that reports success having done half the work is not, because nobody knows to recover it.

Finding them

Read-only / Safeaudit the repository
cd /srv/ansible

grep -rn 'ignore_errors:\s*\(true\|yes\|True\)'    --include='*.yml' --include='*.yaml' .

grep -rn 'ignore_unreachable:\s*\(true\|yes\|True\)'    --include='*.yml' --include='*.yaml' .

# Recaps from previous runs, if you log them.
grep -h 'ignored=[1-9]' /var/log/ansible/*.log | tail -20
  1. Rank by what the ignored task does. An ignored service restart or migration is a different order of problem from an ignored optional file deletion.
  2. For each one, ask what specific failure it was added to suppress. If nobody knows, that is the finding.
  3. Replace with failed_when where the answer is "this exit code is fine", which is most of them.
  4. Replace with block/rescue where a compensating action exists - and check the rescue ends in a deliberate fail unless it genuinely resolved the problem.
  5. Delete the rest and let the tasks fail. Then fix what fails, which is the work that was deferred when the keyword was added.
  6. Add a CI check that fails on new ignore_errors outside an allowlist, so the count cannot silently grow again.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A nightly play runs with `ignore_unreachable: true` at play level. A network partition makes 40 hosts unreachable. What does the recap and exit code show?

  2. Q2. Which are honest replacements for a blanket ignore_errors? Select all that apply.

  3. Q3. A task with ignore_errors: true has its result discarded, so a registered variable from it cannot be tested afterwards.

  4. Q4. Removing ignore_errors from a repository makes the nightly pipeline fail every night. What is the right response?

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