AnsibleXXXII · Rolling DeploymentsRolling Deployments
Return to service and the soak interval
What you'll learn
- Verify a host is receiving traffic rather than that the enable call succeeded
- Place the return-to-service step so no failure path can skip it
- Justify a soak interval and choose its length
- Explain why the last batch needs the same verification as the first
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
The drain created an obligation. This is where it is discharged.
Two things have to be true before the batch is finished: the host is back in the load balancer, and it is actually serving traffic. Those are different claims, and only the first one is easy.
The enable call is not the verification
- name: Return this host to the load balancer pool
ansible.builtin.uri:
url: >-
https://lb.example.com/api/v1/pools/{{ app_pool }}/members/{{ inventory_hostname }}
method: PATCH
body_format: json
body:
state: active
headers:
Authorization: 'Bearer {{ lb_api_token }}'
status_code:
- 200
- 204
delegate_to: localhost
changed_when: trueA 204 means the load balancer accepted the request. It does not mean
the backend is in the pool, that its health check has passed, or that a
single request has reached it.
Most load balancers will not send traffic to a backend until their own
health check has passed against it, and that check runs on the
balancer’s schedule — commonly every five to thirty seconds, sometimes
requiring several consecutive successes. So there is a window, after
your enable call returns 204, during which the host is enabled and
receiving nothing.
If the play proceeds to drain the next batch during that window, capacity is lower than you think. Do it across enough batches and you can drain your way into an outage while every task reports success.
- name: Wait for the balancer to mark this backend healthy and in service
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
retries: 20
delay: 3
until:
- member.json.state | default('') == 'active'
- member.json.health | default('') == 'healthy'
changed_when: falseAgain the default() values are chosen to fail the condition. An absent
field must not read as success.
Proving traffic is arriving
The strongest form asks the host itself whether requests are reaching it. Any service that exposes a request counter can answer:
- name: Record the request counter before waiting
ansible.builtin.uri:
url: 'http://{{ ansible_host | default(inventory_hostname) }}:8080/metrics'
return_content: true
register: metrics_before
changed_when: false
- name: Give the balancer time to route requests here
ansible.builtin.pause:
seconds: '{{ traffic_settle_seconds | default(20) }}'
- name: Confirm the request counter has moved
ansible.builtin.uri:
url: 'http://{{ ansible_host | default(inventory_hostname) }}:8080/metrics'
return_content: true
register: metrics_after
retries: 10
delay: 5
until: >-
(metrics_after.json.requests_total | default(0) | int)
> (metrics_before.json.requests_total | default(0) | int)
changed_when: falseThe step that must never be skipped
Part XXXI established that when a failure policy triggers, the play stops at the end of the failing task and the surviving hosts run no further tasks. A return-to-service step written as an ordinary final task is exactly such an unreached task.
- name: Deploy this host, returning it to service whatever happens
block:
- name: Drain, deploy, restart and health-check
ansible.builtin.include_tasks: deploy-one-host.yml
always:
- name: Return a healthy host to the load balancer
ansible.builtin.uri:
url: >-
https://lb.example.com/api/v1/pools/{{ app_pool }}/members/{{ inventory_hostname }}
method: PATCH
body_format: json
body:
state: active
headers:
Authorization: 'Bearer {{ lb_api_token }}'
status_code:
- 200
- 204
delegate_to: localhost
changed_when: true
when: health.json.version | default('') == release_version
- name: Announce a host left out of service
ansible.builtin.debug:
msg: >-
CAPACITY WARNING: {{ inventory_hostname }} failed its health
check and remains OUT OF SERVICE in pool {{ app_pool }}.
The fleet is running with reduced capacity until this host is
repaired or manually returned.
when: health.json.version | default('') != release_versionThe soak interval
The last step of the loop is doing nothing, on purpose.
- name: Soak before starting the next batch
ansible.builtin.pause:
seconds: '{{ soak_seconds | default(120) }}'
run_once: true
when: not (skip_soak | default(false) | bool)The health check answers “is this host working now”. A whole class of failures does not present immediately:
- A memory leak in the new version that takes minutes to exhaust the heap, then crashes the process.
- A connection pool that is fine until it fills, which depends on traffic volume over time.
- A cache that starts cold and is fine until the first cache-expiring request pattern arrives.
- A scheduled job the new version runs on a timer, which fails the first time it fires.
- A slow resource leak — file descriptors, threads, temporary files.
Without a soak, a rolling deploy can put the new version on the entire fleet before any of these has had time to appear on the first host. The canary was healthy when you checked it, and by the time it fell over, all 200 hosts were running the same code.
The soak is what converts the canary from “it started successfully” into “it survived a realistic period of production traffic”.
Choosing the length. Long enough for the failure mode you are most afraid of, and no longer. Two minutes is a reasonable default for a web application, and it is a default rather than an answer — if the leak you have seen before takes fifteen minutes to manifest, a two-minute soak does not protect you against it.
Knowledge check
Knowledge check · 4 questions
Q1. A rolling play re-enables each backend and gets a 204 from the load balancer API, then immediately drains the next batch. What risk does this create?
Q2. What does a soak interval between batches protect against that a health check cannot?
Q3. Which statements about placing the return-to-service step in an always block are correct? Select all that apply.
Q4. The final batch of a rolling deployment deserves the same health check and return-to-service verification as the first, even though every other host is already healthy.
Passing score: 75%. Answers are checked in this browser.