Skip to main content
RunBook Academy

AnsibleLII · Anti-PatternsAnti-patterns of execution

Anti-pattern: no limit, no canary

Advanced⏱ ~26 minbash

What you'll learn

  • Recognise the anti-pattern from a playbook header and an invocation
  • Explain why simultaneity destroys both diagnosis and rollback, not just uptime
  • Apply the corrected form: a canary group, a serial ramp and a guardrail that refuses
  • State what a rollback plan must contain to be one

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.
Service impact possiblethe anti-pattern, in full
- name: Configure the application
hosts: all
become: true
roles:
  - app

Invoked as ansible-playbook site.yml. No --limit, no serial, no canary group, no --check first.

There is nothing syntactically wrong with it, it is what every tutorial shows, and it is how most repositories start.

What it looks like when it fires

The template has a defect — a variable that is undefined on some hosts, a value with the wrong unit, a directive removed in the newer package version. The role renders it, writes it, and notifies a handler.

With the default forks = 5 and a fast play, three hundred hosts take a couple of minutes. With forks raised for a large fleet, as it usually is, ninety seconds is realistic.

Read-only / Safewhat the operator could have known first
$ ansible-playbook -i inventory/hosts.yml site.yml --list-hosts
playbook: site.yml

play #1 (all): Configure the application	TAGS: []
  pattern: ['all']
  hosts (16):
    web10.example.com
    web01.example.com
    db03.example.com
    ...

The failure is not the outage

An outage is bad and recoverable. The specific damage here is that simultaneity removes the two things you need in order to recover.

1. There is no healthy population to diagnose against

Diagnosis is comparison. “This host is broken, that host is fine, what differs” is how these are solved, and it takes minutes.

When all three hundred hosts received the change at the same time, every host is broken. There is nothing to compare against. The investigation becomes reading the diff and reasoning from first principles, at 03:00, with the service down — and the change is now the prime suspect and the only evidence.

2. There is nothing to roll back from

Rollback needs a known-good state to return to. Part XLVIII establishes that Ansible has no universal rollback: the previous state was not captured, because nothing captured it.

In a staged rollout, the un-updated hosts are the known-good state. They are serving traffic, they hold the previous configuration, and you can read the old file off one of them. In a simultaneous rollout there is no such host, and the recovery is reconstructing the previous configuration from Git — assuming the previous version was committed, and assuming the change is reversible at all, which package upgrades and schema migrations frequently are not.

The corrected form

Three additions, none of them clever.

Service impact possiblestaged, with an explicit canary
- name: Configure the application
hosts: app
become: true
serial:
  - 1
  - 10%
  - 100%
max_fail_percentage: 0
roles:
  - app
post_tasks:
  - name: The service must answer after the change
    ansible.builtin.uri:
      url: 'http://{{ ansible_host }}:8080/healthz'
      status_code: 200
    retries: 5
    delay: 6
    register: health
    until: health.status == 200
    delegate_to: localhost

serial as a list is a ramp: batch one is a single host, then ten percent, then everything remaining. max_fail_percentage: 0 stops the play if any host in a batch fails, so a bad first batch does not proceed to the second. The health check makes “the batch succeeded” mean the service answered rather than that the tasks exited zero — Part XXVI’s distinction between verifying outcomes and verifying tasks.

Read-only / Safethe invocation habit
# 1. Who does this hit?
ansible-playbook site.yml --limit app_prod --list-hosts

# 2. What would change?
ansible-playbook site.yml --limit app_prod --check --diff

# 3. The canary only.
ansible-playbook site.yml --limit canary

And the guardrail that does not depend on remembering, from Part XXX:

Read-only / Safea pre-flight play that refuses an oversized target
- name: Pre-flight
hosts: all
gather_facts: false
vars:
  max_hosts: 25
tasks:
  - name: Refuse a target set larger than the declared ceiling
    run_once: true
    ansible.builtin.assert:
      that: ansible_play_hosts_all | length <= max_hosts | int
      fail_msg: >-
        Refusing: {{ ansible_play_hosts_all | length }} hosts targeted,
        ceiling is {{ max_hosts }}. Narrow with --limit or raise it
        deliberately in the playbook vars.
      success_msg: '{{ ansible_play_hosts_all | length }} hosts, within the ceiling'

“No rollback plan” appears in this lesson rather than its own because it is the same gap seen from the other end: staging exists so that rollback is possible, and a rollback plan is what makes staging worth doing.

A rollback plan is not the sentence “we would revert the commit”. To be one, it must answer:

  1. What is the previous state, concretely? A package version, a config file in Git at a known commit, a database schema revision. If nobody can name it, there is no plan.
  2. Is the change reversible at all? A package downgrade may not restore the previous behaviour; a schema migration usually cannot be reversed; a deleted file is gone. Say so explicitly rather than discovering it during the rollback.
  3. How is the previous state applied? A playbook run with a pinned version, a restore from a backup, a switch back to the previous artefact. It is a procedure, and it should have been tested.
  4. How long does it take, and does that fit the outage budget? A rollback slower than the fix is not a rollback.
  5. What is the decision point? "If the canary is not healthy within ten minutes, we roll back" is a plan. "We will see how it goes" is how a fifteen-minute incident becomes three hours.

Part XLVIII covers the absence of universal rollback in full. The operational summary: your rollback is whatever you explicitly built, and a staged rollout is the cheapest form of it because the un-updated hosts are the previous state, still running.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A defective template reaches 300 hosts simultaneously. Beyond the outage, what is the most damaging consequence?

  2. Q2. Which additions meaningfully reduce the blast radius of a fleet-wide play? Select all that apply.

  3. Q3. In a staged rollout, the hosts that have not yet been updated are themselves the rollback mechanism.

  4. Q4. A playbook has used `hosts: all` for two years without incident. Why is that not evidence that the practice is safe?

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