Skip to main content
RunBook Academy

AnsibleXXXII · Rolling DeploymentsRolling Deployments

When the rollout fails halfway

Expert⏱ ~27 minansible-playbook

What you'll learn

  • Establish the true state of a fleet after a failed rollout, including load balancer state
  • Identify drained-and-orphaned hosts as the urgent category
  • Choose between hold, roll forward and roll back on stated criteria
  • Explain why re-running the playbook is usually the worst available option

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.

Batch four of eleven failed its health check. The failure policy did its job and stopped the play.

You now have a fleet in three or four different states at once, an exit code that tells you almost nothing about which hosts are in which, and a decision to make. This is the situation the whole part has been building toward, because it is the one that actually happens.

The four states

flowchart LR
  subgraph FLEET["the fleet, immediately after the stop"]
    direction LR
    A["batches 1-3<br/><b>NEW</b><br/>deployed, healthy,<br/>in service"]
    B["batch 4, failed host<br/><b>BROKEN</b><br/>deployed, unhealthy,<br/>drained"]
    C["batch 4, healthy hosts<br/><b>ORPHANED</b><br/>possibly deployed,<br/><b>drained, never returned</b>"]
    D["batches 5-11<br/><b>OLD</b><br/>untouched,<br/>in service"]
  end
  C -.->|"the urgent one"| U["capacity is reduced<br/>and nothing said so"]

The third column is the one that gets missed.

Batches 1–3 are fine. New version, healthy, serving traffic.

The failed host in batch 4 is broken and you know it — the health check said so, it is in the log, it is why the play stopped.

The healthy hosts in batch 4 are the problem. Part XXXI established by execution that when a failure policy triggers, the surviving hosts in the batch do not run the remaining tasks. They were drained. They may or may not have been deployed to. They were never returned to service. And the recap reports them with failed=0:

Read-only / Safethe recap of an aborted batch, from Part XXXI
$ ansible-playbook -i inv6.ini rollout.yml
PLAY RECAP *********************************************************************
h01                        : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
h02                        : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
h03                        : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

h02 and h03 look like successes. The only tell is ok=1 where a completed host would show ok=2, and nobody reads a recap that closely at 03:00.

Batches 5–11 were never touched and are absent from the recap entirely.

Establishing the true split

Do not reason about it from the run log. The log tells you what Ansible attempted; the fleet tells you what is true.

Three questions, and they need three different sources.

Read-only / Safethe post-failure census
- name: Establish the real state of the fleet
hosts: appservers
gather_facts: false
tasks:
  - name: What version is on disk
    ansible.builtin.slurp:
      src: /etc/app/deployed-version
    register: marker
    failed_when: false

  - name: What version is the process actually serving
    ansible.builtin.uri:
      url: 'http://{{ ansible_host | default(inventory_hostname) }}:8080/healthz'
      return_content: true
    register: live
    failed_when: false
    changed_when: false

  - name: Is the load balancer sending it traffic
    ansible.builtin.uri:
      url: >-
        https://lb.example.com/api/v1/pools/{{ app_pool }}/members/{{ inventory_hostname }}
      method: GET
      headers:
        Authorization: 'Bearer {{ lb_api_token }}'
      return_content: true
    delegate_to: localhost
    register: member
    failed_when: false
    changed_when: false

  - name: Report all three together
    ansible.builtin.debug:
      msg: >-
        {{ inventory_hostname }}
        disk={{ (marker.content | default('') | b64decode | trim) or 'ABSENT' }}
        serving={{ live.json.version | default('NO-ANSWER') }}
        lb={{ member.json.state | default('UNKNOWN') }}

The combinations and what each means:

diskservinglbMeaningUrgency
newnewactivedeployed and in servicedone
oldoldactivenever touchedfine
newnewdisableddeployed, healthy, orphanedurgent
oldolddisableddrained before deploy, orphanedurgent
newoldanywritten, never restartedinvestigate
newno answerdisableddeployed and brokenexpected, contained
anyno answeractivebroken and serving trafficmost urgent

The last row is the worst case and it is possible: a host whose health check the play never got to, left enabled, serving errors to users. If your play returns hosts to service before checking them, or if a previous run left state behind, this is where it shows up.

Hold, forward, or reverse

Now the decision. All three are legitimate; the criteria are what matter.

Hold

Leave the fleet split. Fix nothing tonight.

Correct when all three hold:

  1. The versions are compatible. Old and new can run simultaneously without breaking each other — the version-skew question. If you have not established this, you cannot hold.
  2. Capacity is intact. No orphaned hosts, or they have been returned.
  3. The split is stable. Nothing will change it while you are asleep — no autoscaling that would launch hosts on an unknown version, no scheduled job that assumes uniformity.

When those hold, holding is frequently the best decision available. Nothing is broken, and diagnosis at 10:00 with the team awake is better than diagnosis at 03:00 alone. The failure this avoids is the one where a tired person makes a hasty change and turns a stable split into an outage.

Roll forward

Finish the rollout.

Correct when the new version is known good and the failure was incidental — a repository timeout, a host with a full disk, a transient network problem — or when the split itself is the danger and completing it is the fastest way out.

The mechanism is --limit scoped to the hosts still on the old version, generated from the census rather than assumed:

Service impact possibleresuming against the remainder
ansible-playbook site.yml --limit @/tmp/still-on-old.txt --list-hosts

ansible-playbook site.yml --limit @/tmp/still-on-old.txt --check --diff

ansible-playbook site.yml --limit @/tmp/still-on-old.txt

The precondition people skip: fix the thing that failed first. If the failure was a genuine problem with the new version, rolling forward deploys a known-broken release to the rest of the fleet, faster than the original rollout would have because you have now disabled the gate that stopped it.

Roll back

Return the deployed hosts to the old version.

Correct when the new version is the problem.

It is also the option people most overestimate. A rollback is a deployment: same drain, same restart, same health checks, same risk — executed by a more tired person under more time pressure, on a path that is usually less rehearsed than the forward one.

It is only fast if it was built in advance:

  • The previous artefact is still available. A package repository that keeps one version, or a deploy directory that keeps the last release, is the difference between a five-minute rollback and a rebuild.
  • Any migration applied by the new version is reversible, and the reverse was written and tested before the forward one ran. This is the most common thing that makes rollback impossible.
  • The rollback path is the same playbook with a different release_version, not a separate script that has never been run.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A rolling deploy fails its health check on one host in batch 4 of 11 and the failure policy stops the play. Which category of host is the most urgent to deal with?

  2. Q2. Where should you look first after a rolling deployment fails?

  3. Q3. Which conditions must hold for "hold the split until business hours" to be a safe choice? Select all that apply.

  4. Q4. A rollback should be treated as a deployment in its own right, with the same drain, health-check and return-to-service steps as the forward rollout.

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