A health check and a health gate are different things, and the
difference is not in the check.
A gate is a check whose failure changes what happens next. A monitor is
a check whose failure is recorded somewhere. The same uri task is
either one depending on three things around it: whether it can fail,
whether the play stops when it does, and whether anything after it
depends on it having passed.
Most health checks in deployment roles are monitors. They look
identical to gates in review.
The three ways a gate stops being one
Read-only / Safea check that cannot fail— Read-only. ignore_errors means the play continues regardless of the result, so this task can only ever produce a log line.
# A monitor. Reads like a gate.
- name: Check the application is healthy
ansible.builtin.uri:
url: 'http://{{ inventory_hostname }}:8080/healthz'
ignore_errors: true
ignore_errors: true is the obvious one. It is usually added the
week after a flaky check failed a run, and it is never removed.
No failure policy on the play. A check that fails one host in a
serial: 10 batch, on a play with no max_fail_percentage, stops that
host and lets the other nine continue — and the next batch starts on
schedule. The check failed and the deployment proceeded.
Placement after the last thing that matters. A health check as the
final task of the play, after the last batch, is a report. There is
nothing left for it to gate.
The retry arithmetic, which is not what it looks like
until reruns a task until its condition holds. retries and delay
govern how many times and how far apart. The upstream defaults are
retries: 3 and delay: 5 — small enough that an unset retries
on a service that takes twenty seconds to start is a guaranteed failure.
The arithmetic people do is retries × delay. That is the sleeping
time, not the elapsed time.
Read-only / Safea health gate with an honest budget— Read-only. Polls the health endpoint until it reports the deployed version. The worst case is thirty attempts, each able to consume the full request timeout, plus twenty-nine delays.
- name: Wait for the application to serve the version we deployed
ansible.builtin.uri:
url: 'http://{{ ansible_host | default(inventory_hostname) }}:8080/healthz'
return_content: true
status_code: [200]
timeout: 10
register: health
retries: 30
delay: 5
until:
- health.status | default(0) == 200
- health.json.version | default('') == myapp_version
- health.json.database | default('') == 'connected'
changed_when: false
The naive figure is 150 seconds. The real one is nearly three times
that, and it is reached exactly when the service is not responding at
all — the case where every attempt burns the full timeout rather than
failing fast.
Two mechanical details worth knowing:
The registered variable gains an attempts key recording how many
tries the task took. Logging it turns “the deployment felt slow
tonight” into a number, and a service whose attempts has been creeping
up over months is telling you something before it fails.
Since ansible-core 2.16, retries works without until — the
task retries until it succeeds, at most retries times. That is useful
for a genuinely flaky operation and is not what a health gate wants: a
health gate should express its success condition explicitly, so the
run log says what was being waited for.
wait_for on a port is not a readiness check
wait_for polls a TCP port and succeeds when something accepts a
connection. That proves a process called listen(). It proves nothing
about whether that process can serve a request.
The gap is not theoretical. A JVM application binds its port during
start-up and then spends forty seconds initialising; a service with a
cold connection pool accepts connections and returns errors; a process
that crashed and was restarted by systemd binds the port immediately and
fails every request.
Read-only / Safethe port check as a cheap first stage, not the gate— Read-only. Waits for the port to accept connections, then for the application to report itself ready. The first is fast and catches a service that did not start at all; the second is the actual gate.
- name: Wait for the listener to appear
ansible.builtin.wait_for:
host: '{{ ansible_host | default(inventory_hostname) }}'
port: 8080
state: started
timeout: 60
- name: Wait for the application to report itself ready
ansible.builtin.uri:
url: 'http://{{ ansible_host | default(inventory_hostname) }}:8080/readyz'
return_content: true
register: ready
retries: 20
delay: 3
until: ready.json.status | default('') == 'ready'
changed_when: false
Note the explicit host:. wait_for defaults host to 127.0.0.1,
which means an unqualified wait_for polls a port on the host the
task is running on — and under delegate_to: localhost it polls the
controller. That default is correct for its commonest use and wrong for
the one people write first.
Assert on content, not on 200
An HTTP 200 from a health endpoint means a handler returned. In a large
share of real applications, that handler is a static return 200 that
knows nothing about the state of anything.
Read-only / Safewhat to assert on instead— Read-only. Each condition is a claim the endpoint has to know something to satisfy, and each default is chosen so an absent field fails the condition.
Every default() there is chosen to fail the condition. That is the
discipline: default('') compared against a real value is false;
default(1) compared against 0 is false; default(999999) fails a
threshold. A field the endpoint stopped reporting must not read as good
news.
The version condition is the one to add first if a role has only one.
It is what distinguishes “the service is healthy” from “the service is
healthy and is running what this play deployed” — and the second is the
question a deployment is actually asking.
Knowledge check
Knowledge check · 4 questions
Q1. A health check task has retries: 30, delay: 5 and the uri module with timeout: 10. What is its worst-case duration on one host?
Q2. What distinguishes a health gate from a health monitor?
Q3. Which criticisms of a health gate written as "wait_for port 8080, then assert status 200" are valid? Select all that apply.
Q4. Setting retries: 200 with delay: 10 makes a health gate more robust, because it tolerates any legitimate slow start.
Passing score: 75%. Answers are checked in this browser.