Your playbook runs smoothly until it hits a template task, then explodes with a rendering error. Jinja2 template errors can be cryptic, pointing to line numbers that don't match your template file. The error might be in variable access, syntax, or data structure handling.
The Error
fatal: [webserver]: FAILED! => {
"msg": "AnsibleError: An unhandled exception occurred while templating '{{ config.database.host }}'. Error was a <class 'ansible.errors.AnsibleUndefinedVariable'>, original message: 'dict object' has no attribute 'database'"
}Or a syntax error:
fatal: [webserver]: FAILED! => {
"msg": "AnsibleError: template error while templating string: unexpected '}'. Line: 15, Column: 12"
}Or type errors:
fatal: [webserver]: FAILED! => {
"msg": "AnsibleError: An unhandled exception occurred while templating '{{ ports | join(',' }}'. Error was unexpected '/'Quick Diagnosis
Enable verbose output to see more details:
ansible-playbook site.yml -vvvTest the template directly:
- name: Debug template variables
debug:
msg: "{{ lookup('template', 'templates/config.j2') }}"Render template locally to inspect:
# Quick template test
ansible localhost -m debug -a "msg={{ lookup('template', 'templates/test.j2') }}"Common Causes and Fixes
Undefined Variable
The most common error—referencing a variable that doesn't exist.
Problem template:
``jinja
server {
listen {{ port }};
server_name {{ server_name }};
root {{ document_root }};
}
If port is undefined, rendering fails.
Fix with default:
``jinja
server {
listen {{ port | default(80) }};
server_name {{ server_name | default('localhost') }};
root {{ document_root | default('/var/www/html') }};
}
Assert required variables:
``yaml
- name: Ensure variables are defined
assert:
that:
- server_name is defined
- document_root is defined
fail_msg: "Required variables missing"
Nested Dictionary Access
Accessing nested keys that might not exist.
Problem:
``jinja
database_host: {{ config.database.host }}
database_port: {{ config.database.port }}
Fix with default for each level:
``jinja
database_host: {{ (config.database | default({})).host | default('localhost') }}
database_port: {{ (config.database | default({})).port | default(5432) }}
Or use a safer approach:
``yaml
# In tasks
- set_fact:
db_config: "{{ config.database | default({'host': 'localhost', 'port': 5432}) }}"
# In template
database_host: {{ db_config.host }}
database_port: {{ db_config.port }}Jinja2 Syntax Errors
Jinja2 has specific syntax requirements.
Unclosed braces: ```jinja # Wrong {{ variable }
# Correct {{ variable }} ```
Mixing braces: ```jinja # Wrong - using single braces { variable }
# Correct {{ variable }} ```
For loop syntax: ```jinja # Wrong {% for item in items } {{ item }} {% endfor %}
# Correct {% for item in items %} {{ item }} {% endfor %} ```
If statement syntax: ```jinja # Wrong {% if condition } content {% endif %}
# Correct {% if condition %} content {% endif %} ```
Quote Escaping
Quotes inside strings need proper escaping.
Problem:
``jinja
command: "echo '{{ message }}'"
# Fails if message contains quotes
Fix with filters:
``jinja
command: echo '{{ message | quote }}'
Or use to_json:
``jinja
config: {{ config | to_json }}
List and Dict Operations
Errors when operating on wrong data types.
Problem:
``jinja
{{ ports.split(',') }} # Fails if ports is already a list
Fix with type checking:
``jinja
{{ ports if ports is string else ports | join(',') }}
Or use the appropriate filter:
``jinja
# Always get a list
{{ (ports | default([])) if ports is not string else ports.split(',') }}
Conditional Rendering
Issues with conditionals in templates.
Problem:
``jinja
{% if enable_ssl %}
ssl_certificate {{ ssl_cert }};
{% endif %}
If enable_ssl is undefined, this fails.
Fix:
``jinja
{% if enable_ssl | default(false) | bool %}
ssl_certificate {{ ssl_cert | default('/etc/ssl/cert.pem') }};
{% endif %}
Variable Scope in Loops
Variables defined inside loops might not be accessible outside.
Problem:
``jinja
{% for host in groups['webservers'] %}
{% set server_ip = hostvars[host]['ansible_host'] %}
server {{ host }}: {{ server_ip }};
{% endfor %}
# server_ip not accessible here
The variable is scoped to the loop—this is expected behavior.
If you need to track something across iterations:
{% set all_ips = [] %}
{% for host in groups['webservers'] %}
{% set _ = all_ips.append(hostvars[host]['ansible_host']) %}
{% endfor %}
All IPs: {{ all_ips | join(', ') }}Raw Block Issues
Using Jinja2 syntax in content that should be literal.
Problem—generating Jinja2 templates:
``jinja
# This tries to render {{ name }}
config_template: {{ name }}
Use raw block:
``jinja
{% raw %}
config_template: {{ name }}
{% endraw %}
Line Continuation and Whitespace
Jinja2 control blocks add whitespace.
Problem:
``jinja
{% for item in items %}
{{ item }}
{% endfor %}
Produces extra blank lines.
Use whitespace control:
``jinja
{%- for item in items %}
{{ item }}
{%- endfor %}
The - after % removes whitespace before/after the block.
Debugging Templates
Debug the template content:
- name: Show rendered template
debug:
msg: "{{ lookup('template', 'templates/config.j2') }}"Debug specific variables:
- name: Show variable value
debug:
msg: "config = {{ config | to_nice_yaml }}"Use type checks:
- name: Check variable type
debug:
msg: "ports is {{ ports | type_debug }}"Check defined status:
- name: Check if defined
debug:
msg: "server_name is {{ 'defined' if server_name is defined else 'undefined' }}"Validation Before Template
Validate data before rendering:
- name: Validate configuration
assert:
that:
- config is defined
- config is mapping
- "'host' in config.database"
fail_msg: "Invalid configuration structure"Verification
After fixing, verify the template renders:
```bash # Syntax check the playbook ansible-playbook site.yml --syntax-check
# Check mode to see rendered output ansible-playbook site.yml --check --diff
# Run on a test host ansible-playbook site.yml --limit testhost -vvv ```
Best Practices
- 1.Always use defaults:
{{ var | default('value') }} - 2.Validate inputs: Use
assertbefore templates - 3.Use type_debug:
{{ var | type_debug }}when unsure of type - 4.Test locally first: Use
ansible localhost -m debug - 5.Quote safely: Use
| quotefor shell commands - 6.Use to_json:
{{ var | to_json }}for complex data