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

ModuleChecksWaits/RetriesBest For
wait_forPort open, file exists, string in file✅ Built-in timeoutTCP port readiness
uriHTTP endpoint, status code, body⚠️ Use until for retriesHTTP/API health checks
assertAny condition expression❌ No waitingValidating gathered data
wait_for_connectionSSH/connection ready✅ Built-in timeoutAfter 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

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.