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

bash
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:

bash
fatal: [webserver]: FAILED! => {
    "msg": "AnsibleError: template error while templating string: unexpected '}'. Line: 15, Column: 12"
}

Or type errors:

bash
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:

bash
ansible-playbook site.yml -vvv

Test the template directly:

yaml
- name: Debug template variables
  debug:
    msg: "{{ lookup('template', 'templates/config.j2') }}"

Render template locally to inspect:

bash
# 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}) }}"

jinja
# 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:

jinja
{% 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:

yaml
- name: Show rendered template
  debug:
    msg: "{{ lookup('template', 'templates/config.j2') }}"

Debug specific variables:

yaml
- name: Show variable value
  debug:
    msg: "config = {{ config | to_nice_yaml }}"

Use type checks:

yaml
- name: Check variable type
  debug:
    msg: "ports is {{ ports | type_debug }}"

Check defined status:

yaml
- name: Check if defined
  debug:
    msg: "server_name is {{ 'defined' if server_name is defined else 'undefined' }}"

Validation Before Template

Validate data before rendering:

yaml
- 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. 1.Always use defaults: {{ var | default('value') }}
  2. 2.Validate inputs: Use assert before templates
  3. 3.Use type_debug: {{ var | type_debug }} when unsure of type
  4. 4.Test locally first: Use ansible localhost -m debug
  5. 5.Quote safely: Use | quote for shell commands
  6. 6.Use to_json: {{ var | to_json }} for complex data