Introduction
After deploying a service, you need to verify it's actually running. Ansible offers several modules for this: wait_for checks ports and files, uri makes HTTP requests, and assert validates conditions. Each targets a different layer of the health check stack. This guide shows which to use and how to combine them.
Quick Comparison
| Module | Checks | Waits/Retries | Best For |
|---|---|---|---|
wait_for | Port open, file exists, string in file | ✅ Built-in timeout | TCP port readiness |
uri | HTTP endpoint, status code, body | ⚠️ Use until for retries | HTTP/API health checks |
assert | Any condition expression | ❌ No waiting | Validating gathered data |
wait_for_connection | SSH/connection ready | ✅ Built-in timeout | After reboot |
wait_for — Port and File Checks
# Wait for port to be open
- name: Wait for PostgreSQL to start
ansible.builtin.wait_for:
port: 5432
host: "{{ db_host }}"
timeout: 60
# Wait for port on localhost
- name: Wait for app to start
ansible.builtin.wait_for:
port: 8080
delay: 5 # Wait 5s before first check
timeout: 120 # Give up after 120s
# Wait for port to CLOSE (drain connections)
- name: Wait for old process to stop
ansible.builtin.wait_for:
port: 8080
state: stopped
timeout: 30
# Wait for file to appear
- name: Wait for deployment marker
ansible.builtin.wait_for:
path: /opt/app/READY
state: present
# Wait for string in file
- name: Wait for app to log "ready"
ansible.builtin.wait_for:
path: /var/log/app/startup.log
search_regex: "Application started"
timeout: 120
# Wait for file to be removed
- name: Wait for lock file to be released
ansible.builtin.wait_for:
path: /tmp/deploy.lock
state: absent
timeout: 300
uri — HTTP Health Checks
# Basic HTTP check
- name: Verify application health
ansible.builtin.uri:
url: "http://localhost:8080/health"
register: health_check
# Check with retries (using until)
- name: Wait for app to be healthy
ansible.builtin.uri:
url: "http://localhost:8080/health"
return_content: true
register: health
until: health.status == 200
retries: 30
delay: 5
# Check response body
- name: Verify API returns expected data
ansible.builtin.uri:
url: "http://localhost:8080/api/status"
return_content: true
register: api_status
until:
- api_status.status == 200
- "'healthy' in api_status.content"
retries: 10
delay: 3
# HTTPS with authentication
- name: Check authenticated endpoint
ansible.builtin.uri:
url: "https://{{ inventory_hostname }}/api/health"
headers:
Authorization: "Bearer {{ api_token }}"
validate_certs: true
status_code: [200, 204]
# POST health check
- name: Run deep health check
ansible.builtin.uri:
url: "http://localhost:8080/health/deep"
method: POST
body_format: json
body:
check_database: true
check_redis: true
register: deep_health
failed_when: deep_health.json.status != "healthy"
assert — Validate Conditions
# Validate after gathering data
- name: Check disk space
ansible.builtin.command: df -h / --output=pcent
register: disk_usage
changed_when: false
- name: Verify sufficient disk space
ansible.builtin.assert:
that:
- disk_usage.stdout_lines[1] | trim | replace('%','') | int < 90
fail_msg: "Disk usage at {{ disk_usage.stdout_lines[1] | trim }} — too high!"
success_msg: "Disk OK: {{ disk_usage.stdout_lines[1] | trim }}"
# Validate service health from multiple checks
- ansible.builtin.assert:
that:
- health.status == 200
- health.json.database == "connected"
- health.json.cache == "connected"
- health.json.version == expected_version
fail_msg: "Post-deploy validation failed: {{ health.json | default('no response') }}"
wait_for_connection — After Reboot
# Wait for host to come back after reboot
- name: Reboot the server
ansible.builtin.reboot:
reboot_timeout: 300
# Or manually:
- name: Reboot
ansible.builtin.command: shutdown -r now
async: 1
poll: 0
- name: Wait for server to come back
ansible.builtin.wait_for_connection:
delay: 30 # Wait 30s for reboot to begin
timeout: 300 # Give up after 5 minutes
Combined Health Check Pattern
# Full deployment verification
- name: Deploy application
ansible.builtin.include_role:
name: deploy
# Layer 1: Port check (fastest — is the process listening?)
- name: Wait for app port
ansible.builtin.wait_for:
port: "{{ app_port }}"
timeout: 60
# Layer 2: HTTP check (is the app responding?)
- name: Wait for HTTP health endpoint
ansible.builtin.uri:
url: "http://localhost:{{ app_port }}/health"
return_content: true
register: health
until: health.status == 200
retries: 20
delay: 3
# Layer 3: Deep validation (is everything connected?)
- name: Validate full health
ansible.builtin.assert:
that:
- health.json.status == "healthy"
- health.json.database == "connected"
- health.json.version == app_version
fail_msg: "Deployment validation failed: {{ health.json }}"
success_msg: "✅ Deployed {{ app_version }} successfully"
Common Mistakes
# ❌ Using uri without retries (fails if app isn't ready yet)
- ansible.builtin.uri:
url: http://localhost:8080/health
# Fails immediately if app takes 10s to start!
# ✅ Use until for retries
- ansible.builtin.uri:
url: http://localhost:8080/health
until: result.status == 200
retries: 10
delay: 5
register: result
# ❌ Using wait_for for HTTP checks (only checks TCP)
- ansible.builtin.wait_for:
port: 8080
# Port may be open but app returning 500!
# ✅ Use wait_for for port, then uri for HTTP
- ansible.builtin.wait_for:
port: 8080
- ansible.builtin.uri:
url: http://localhost:8080/health
# ❌ Hardcoding timeouts too short
timeout: 10 # App needs 30s to start — will fail!
# ✅ Set generous timeouts, fail fast with delay
timeout: 120
delay: 5
Related Articles
Conclusion
wait_for for TCP port and file readiness (fastest, lowest-level check). uri + until for HTTP endpoint health checks (most common post-deploy validation). assert for validating complex conditions after data gathering. wait_for_connection after reboots. The production pattern: wait_for port → uri HTTP health → assert deep validation. Layer your checks from fast/simple to slow/thorough.