Introduction

Ansible's assert module validates conditions during playbook execution — but it cannot run inside Jinja2 templates. Templates render text; modules execute tasks. Understanding this boundary helps you choose the right validation approach for each situation.

Why assert Can't Run in Templates

Jinja2 templates are a rendering engine — they produce text output. Ansible modules are task executors — they perform actions on hosts. These are fundamentally different:

ContextPurposeCan Run Modules?
Jinja2 template (.j2 file)Generate text outputNo
Ansible task (playbook)Execute actionsYes
Jinja2 expression in taskEvaluate to a valueNo
# WRONG — assert is a module, not a Jinja2 filter or function
{{ assert(my_var > 10) }}

# CORRECT — assert is used as a task
- ansible.builtin.assert:
    that: my_var > 10

The assert Module — Correct Usage

Basic Assertion

- name: Validate deployment variables
  ansible.builtin.assert:
    that:
      - env is defined
      - env in ['dev', 'staging', 'production']
    fail_msg: "env must be 'dev', 'staging', or 'production'. Got: {{ env | default('undefined') }}"
    success_msg: "Environment validated: {{ env }}"

Multiple Conditions

- name: Pre-flight checks
  ansible.builtin.assert:
    that:
      - ansible_distribution == 'CentOS' or ansible_distribution == 'RedHat'
      - ansible_distribution_major_version | int >= 8
      - ansible_memtotal_mb >= 2048
      - ansible_processor_vcpus >= 2
      - disk_free_gb | default(0) | float >= 10
    fail_msg: |
      Server does not meet minimum requirements:
      - OS: RHEL/CentOS 8+ (got: {{ ansible_distribution }} {{ ansible_distribution_version }})
      - RAM: 2GB+ (got: {{ ansible_memtotal_mb }}MB)
      - CPU: 2+ cores (got: {{ ansible_processor_vcpus }})
    quiet: true  # Only show fail_msg on failure

Assert with Loop

- name: Validate all required variables
  ansible.builtin.assert:
    that:
      - "{{ item }} is defined"
      - "{{ item }} | length > 0"
    fail_msg: "Required variable '{{ item }}' is missing or empty"
  loop:
    - db_host
    - db_name
    - db_user
    - app_secret_key

Assert in Handlers

tasks:
  - name: Deploy application
    ansible.builtin.copy:
      src: app.tar.gz
      dest: /opt/app/
    notify: verify deployment

handlers:
  - name: verify deployment
    ansible.builtin.assert:
      that:
        - "'running' in service_status.stdout"
      fail_msg: "Application failed to start after deployment"

Validation Inside Jinja2 Templates

When you need validation logic within a template file, use these Jinja2-native approaches:

Method 1: Raise an Error

{# Fail template rendering if variable is missing #}
{% if db_host is not defined %}
  {{ 'db_host is required but not defined' | mandatory }}
{% endif %}

database_host={{ db_host }}

The mandatory filter raises an AnsibleUndefinedVariable error with your message.

Method 2: Conditional Content with Warnings

{% if workers is defined and workers | int > 0 %}
worker_processes {{ workers }};
{% else %}
# WARNING: workers not defined, using auto
worker_processes auto;
{% endif %}

Method 3: Default Values

max_connections={{ max_conn | default(100) }}
log_level={{ log_level | default('info') }}
bind_address={{ bind_addr | default('0.0.0.0') }}

Method 4: Division by Zero Trick (Last Resort)

{% if critical_var is not defined %}
  {% set _ = 1/0 %}  {# Forces template error #}
{% endif %}

This works but produces an ugly error. Prefer mandatory filter instead.

Combining assert Tasks with Templates

The best pattern: validate inputs with assert before rendering templates:

- name: Validate configuration
  ansible.builtin.assert:
    that:
      - domain is defined
      - domain | regex_search('^[a-z0-9.-]+$')
      - ssl_cert_path is defined
      - workers | default(0) | int > 0
      - workers | int <= ansible_processor_vcpus * 2
    fail_msg: "Invalid configuration. Check domain, ssl_cert_path, and workers."

- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: 'nginx -t -c %s'
  notify: reload nginx

assert Module Parameters

ParameterTypeDescription
thatlistConditions to evaluate (all must be true)
fail_msgstringMessage on failure
success_msgstringMessage on success
quietboolSuppress success output

Practical Examples

Validate Before Database Migration

- name: Pre-migration checks
  ansible.builtin.assert:
    that:
      - db_backup_exists.stat.exists
      - db_size_gb | float < max_migration_size_gb
      - maintenance_window | bool
    fail_msg: "Cannot migrate: backup={{ db_backup_exists.stat.exists }}, size={{ db_size_gb }}GB, maintenance={{ maintenance_window }}"

Validate API Response

- name: Check API health
  ansible.builtin.uri:
    url: "http://localhost:8080/health"
  register: api_health

- name: Assert API is healthy
  ansible.builtin.assert:
    that:
      - api_health.status == 200
      - api_health.json.status == 'healthy'
      - api_health.json.db_connected | bool
    fail_msg: "API health check failed: {{ api_health.json | default('no response') }}"

Conclusion

The assert module runs in tasks, not in templates. Use it for pre-flight validation before templates run. Inside Jinja2 templates, use the mandatory filter for required variables, default() for optional values, and conditional blocks for warnings. The best pattern: assert validates inputs → template renders with confidence → validate parameter verifies the output.