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:
| Context | Purpose | Can Run Modules? |
|---|---|---|
Jinja2 template (.j2 file) | Generate text output | No |
| Ansible task (playbook) | Execute actions | Yes |
| Jinja2 expression in task | Evaluate to a value | No |
# 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
| Parameter | Type | Description |
|---|---|---|
that | list | Conditions to evaluate (all must be true) |
fail_msg | string | Message on failure |
success_msg | string | Message on success |
quiet | bool | Suppress 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') }}"
Related Articles
- Ansible Ternary Filter
- Jinja2 Conditionals in Ansible
- Ansible Debug Module Guide
- Ansible Best Practices Guide
- Ansible-Lint Guide
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.