Introduction

debug and assert look similar — both evaluate expressions and print messages. But they serve different purposes: debug is for inspecting values during development, assert is for enforcing conditions that must be true. Using debug when you need assert means silent failures; using assert for inspection clutters playbooks with unnecessary failures.

Quick Comparison

Featuredebugassert
PurposePrint/inspect valuesValidate conditions
On failureNever failsFails the play
OutputShows msg or varShows success_msg or fail_msg
Use caseTroubleshooting, developmentPre-flight checks, validation
Production useRemove or disableKeep — they're guardrails

debug — Inspect Values

# Print a message
- ansible.builtin.debug:
    msg: "Deploying version {{ app_version }} to {{ inventory_hostname }}"

# Print a variable (structured output)
- ansible.builtin.debug:
    var: ansible_facts['default_ipv4']

# Print with verbosity control
- ansible.builtin.debug:
    msg: "Connection details: {{ hostvars[inventory_hostname] }}"
    verbosity: 2  # Only shows with -vv or higher

# Print multiple variables
- ansible.builtin.debug:
    msg:
      - "Host: {{ inventory_hostname }}"
      - "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
      - "Memory: {{ ansible_memtotal_mb }}MB"
      - "CPUs: {{ ansible_processor_vcpus }}"

Debug Patterns

# Debug registered output
- ansible.builtin.command: systemctl is-active nginx
  register: nginx_status
  changed_when: false
  failed_when: false

- ansible.builtin.debug:
    var: nginx_status
    # Shows full object: stdout, stderr, rc, changed, etc.

# Debug only when troubleshooting
- ansible.builtin.debug:
    msg: "{{ lookup('env', 'PATH') }}"
  when: debug_mode | default(false)
  tags: debug

assert — Enforce Conditions

# Validate required variables
- name: Check required variables
  ansible.builtin.assert:
    that:
      - db_host is defined
      - db_password is defined
      - app_version is defined
    fail_msg: "Missing required variables! Check group_vars."
    success_msg: "All required variables present."

# Validate system requirements
- name: Check minimum resources
  ansible.builtin.assert:
    that:
      - ansible_memtotal_mb >= 4096
      - ansible_processor_vcpus >= 2
      - ansible_mounts | selectattr('mount', 'eq', '/') | map(attribute='size_available') | first | int > 10737418240
    fail_msg: "Server does not meet minimum requirements (4GB RAM, 2 CPU, 10GB disk)"

# Validate service is running before deployment
- name: Ensure database is accessible
  ansible.builtin.uri:
    url: "http://{{ db_host }}:5432"
  register: db_check
  failed_when: false

- ansible.builtin.assert:
    that: db_check.status == 200
    fail_msg: "Database at {{ db_host }} is not accessible!"

Assert Patterns

# Pre-flight checks at the start of playbook
- name: Pre-flight validation
  hosts: all
  gather_facts: true
  tasks:
    - name: Validate environment
      ansible.builtin.assert:
        that:
          - env in ['dev', 'staging', 'prod']
          - ansible_distribution == 'Ubuntu'
          - ansible_distribution_version is version('22.04', '>=')
        fail_msg: "Unsupported environment: {{ env }} on {{ ansible_distribution }} {{ ansible_distribution_version }}"
        quiet: true  # Suppress per-assertion output

# Validate after deployment
- name: Post-deploy validation
  ansible.builtin.uri:
    url: "http://localhost:{{ app_port }}/health"
  register: health

- ansible.builtin.assert:
    that:
      - health.status == 200
      - "'healthy' in health.json.status"
    fail_msg: "Deployment verification failed! Health check: {{ health.json | default('no response') }}"

Using Both Together

- name: Deploy with validation
  hosts: webservers
  tasks:
    # assert: Pre-flight check
    - ansible.builtin.assert:
        that: app_version is defined
        fail_msg: "app_version must be set via -e"

    # debug: Show what we're about to do
    - ansible.builtin.debug:
        msg: "Deploying {{ app_version }} to {{ inventory_hostname }}"

    - name: Deploy application
      ansible.builtin.include_role:
        name: deploy

    # debug: Show result
    - ansible.builtin.debug:
        msg: "Deploy complete. Verifying..."
      verbosity: 1

    # assert: Post-deploy validation
    - ansible.builtin.uri:
        url: "http://localhost:{{ app_port }}/health"
      register: health

    - ansible.builtin.assert:
        that: health.status == 200
        fail_msg: "Health check failed after deploy!"
        success_msg: "✅ {{ inventory_hostname }} healthy"

Common Mistakes

# ❌ Using debug to "validate" (doesn't fail!)
- ansible.builtin.debug:
    msg: "WARNING: disk space low!"
  when: disk_free_mb < 1000
  # This just prints — playbook continues even if disk is dangerously low

# ✅ Use assert to actually stop execution
- ansible.builtin.assert:
    that: disk_free_mb >= 1000
    fail_msg: "Only {{ disk_free_mb }}MB free — need at least 1000MB"

# ❌ Using assert for inspection (noisy, wrong tool)
- ansible.builtin.assert:
    that: app_port > 0
    success_msg: "Port is {{ app_port }}"
  # Overkill for just seeing a value

# ✅ Use debug for inspection
- ansible.builtin.debug:
    msg: "Port is {{ app_port }}"

# ❌ Leaving debug tasks in production playbooks
# ✅ Use verbosity or tags to control debug output
- ansible.builtin.debug:
    msg: "{{ complex_variable }}"
    verbosity: 2  # Only with -vv

Conclusion

debug = "show me this value" (development/troubleshooting). assert = "this must be true or stop" (validation/guardrails). Use assert at the start of playbooks for pre-flight checks and after critical operations for verification. Use debug during development, then either remove it or gate it behind verbosity for production. The combination of both creates defensive playbooks that validate assumptions explicitly instead of failing mysteriously halfway through.