Your playbook runs smoothly until it hits a template task, then explodes with a Jinja2 error. Template errors can be cryptic, showing errors in compiled templates that don't match your source files. Let's systematically debug and fix template issues.

Understanding the Error

Template errors typically look like:

bash
fatal: [webserver01]: FAILED! => {"msg": "An unhandled exception occurred while templating '{{ config.port }}'. Error was a <class 'jinja2.exceptions.UndefinedError'>, original message: 'config' is undefined"}

Or syntax errors:

bash
fatal: [webserver01]: FAILED! => {"msg": "template error while templating string: expected token ':', got '}'. String: server {\n    listen {{ port }\n}"}

Or filter errors:

bash
fatal: [webserver01]: FAILED! => {"msg": "template error while templating string: no filter named 'to_yaml'. String: {{ config | to_yaml }}"}

Each error type needs different fixes.

Step 1: Locate the Error Source

Find where the error originates:

bash
ansible-playbook site.yml -vvv 2>&1 | grep -A 5 "template error"

For template files:

bash
# The error shows the template path
# Look for: "could not locate file in lookup"
ansible-playbook site.yml --check

Check if template file exists:

yaml
- name: Debug template path
  debug:
    msg: "Template path: {{ lookup('first_found', 'templates/nginx.conf.j2') }}"

Step 2: Fix Jinja2 Syntax Errors

Syntax errors are the most common. Check these patterns:

Unclosed braces:

```jinja2 # WRONG server { listen {{ port } }

# CORRECT server { listen {{ port }} } ```

Invalid variable names:

```jinja2 # WRONG - hyphens not allowed {{ my-variable }}

# CORRECT {{ my_variable }} ```

Nested quotes:

```jinja2 # WRONG value: "{{ "nested" }}"

# CORRECT - escape or use different quotes value: '{{ "nested" }}' # Or value: "{{ 'nested' }}" ```

For loop syntax:

```jinja2 # WRONG {% for item items %}

# CORRECT {% for item in items %} ```

If statement syntax:

```jinja2 # WRONG {% if item = "value" %}

# CORRECT {% if item == "value" %} ```

Validate Jinja2 syntax:

bash
python3 -c "from jinja2 import Template; Template(open('templates/nginx.conf.j2').read())"

Step 3: Handle Undefined Variables

Variables used in templates must be defined:

jinja2
# This fails if port is undefined
server {
    listen {{ port }};
}

Provide defaults:

jinja2
# With default value
server {
    listen {{ port | default(80) }};
}

Or make the entire block conditional:

jinja2
{% if port is defined %}
server {
    listen {{ port }};
}
{% endif %}

Debug what variables are available:

```yaml - name: Debug all variables debug: var: vars

  • name: Debug specific variable
  • debug:
  • var: port
  • `

Step 4: Fix Variable Scope Issues

Variables from different sources have different scopes:

```yaml # vars section - play variables - hosts: webservers vars: http_port: 80

tasks: - name: Use variable template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf ```

In the template:

jinja2
# http_port is available
server {
    listen {{ http_port }};
}

But variables from previous tasks need set_fact or register:

```yaml - name: Get port shell: cat /etc/app/port register: app_port

  • name: Use registered variable
  • template:
  • src: app.conf.j2
  • dest: /etc/app/app.conf
  • `

In template:

jinja2
# Access registered output
port={{ app_port.stdout }}

Step 5: Debug Template Rendering

Use the template lookup to debug:

yaml
- name: Debug template content
  debug:
    msg: "{{ lookup('template', 'nginx.conf.j2') }}"

Or test template locally:

```bash # Create a test playbook cat > test_template.yml << 'EOF' - hosts: localhost gather_facts: no vars: port: 8080 tasks: - name: Render template debug: msg: "{{ lookup('template', 'templates/nginx.conf.j2') }}" EOF

ansible-playbook test_template.yml ```

Step 6: Fix Filter Errors

Using wrong or non-existent filters:

```jinja2 # WRONG - filter doesn't exist {{ data | to_json }} # Wrong filter name

# CORRECT {{ data | tojson }} ```

Common filter mistakes:

```jinja2 # WRONG - wrong filter name {{ list | to_list }}

# CORRECT {{ list | list }}

# WRONG {{ string | to_upper }}

# CORRECT {{ string | upper }} ```

Check available filters:

bash
ansible-doc -t filter -l

Or debug filter output:

yaml
- name: Test filter
  debug:
    msg: "{{ my_list | join(',') }}"

Step 7: Handle Complex Data Structures

Accessing nested data incorrectly:

yaml
# Inventory variable
servers:
  - name: web1
    port: 8080
  - name: web2
    port: 8081

```jinja2 # WRONG {{ servers.web1.port }}

# CORRECT {{ servers[0].port }} # Or iterate {% for server in servers %} {{ server.name }}: {{ server.port }} {% endfor %} ```

For dictionaries:

yaml
config:
  database:
    host: localhost
    port: 5432

```jinja2 # Correct access db_host={{ config.database.host }} db_port={{ config.database.port }}

# Safe access with default db_host={{ config.database.host | default('localhost') }} ```

Step 8: Fix Whitespace Issues

Jinja2 whitespace can create ugly output:

jinja2
{% for item in items %}
{{ item }}
{% endfor %}

Produces extra blank lines. Use whitespace control:

jinja2
{% for item in items -%}
{{ item }}
{% endfor %}

The - strips whitespace on that side. Common patterns:

```jinja2 {%- if condition -%} content with no surrounding whitespace {%- endif -%}

{# Comment that produces no output -#} ```

Step 9: Handle Special Characters

Templates with braces that aren't Jinja2:

```jinja2 # This breaks - Ansible tries to parse it regexp: "[0-9]{3}"

# Escape with raw block {% raw %} regexp: "[0-9]{3}" {% endraw %}

# Or escape individual braces regexp: "[0-9]{{ '{{' }}3{{ '}}' }}" ```

For JSON in templates:

```jinja2 # WRONG - conflicts with Jinja2 { "key": "{{ value }}" }

# CORRECT - use tojson filter {{ config | tojson }} ```

Step 10: Validate Template Before Using

Create a validation playbook:

```yaml - hosts: localhost gather_facts: no vars: # All variables your template needs port: 8080 server_name: localhost

tasks: - name: Validate template syntax debug: msg: "Template is valid" when: lookup('template', 'templates/nginx.conf.j2') is defined

  • name: Show rendered template
  • debug:
  • msg: "{{ lookup('template', 'templates/nginx.conf.j2') }}"
  • `

Run validation:

bash
ansible-playbook validate_template.yml

Step 11: Use Type Checking

Type errors when filters expect specific types:

```jinja2 # WRONG - string passed to math filter {{ "100" | int + 50 }} # Works - int filter converts

# But this fails {{ "not_a_number" | int + 50 }} # "not_a_number" becomes 0

# Safer approach {% if port is number %} port={{ port }} {% else %} # Invalid port value {% endif %} ```

Use type tests:

jinja2
{% if value is string %}
{{ value }}
{% elif value is number %}
{{ value | int }}
{% endif %}

Quick Verification

Test template rendering:

```bash # Create test values cat > test_vars.yml << 'EOF' port: 8080 server_name: localhost EOF

# Test template ansible localhost -m debug -a "msg={{ lookup('template', 'templates/nginx.conf.j2') }}" -e "@test_vars.yml" ```

Prevention Best Practices

  1. 1.Use template validation in CI:

```yaml - name: Validate templates hosts: localhost gather_facts: no tasks: - name: Check all templates find: paths: templates patterns: "*.j2" register: templates

  • name: Validate each template
  • debug:
  • msg: "{{ item.path }}"
  • with_items: "{{ templates.files }}"
  • when: lookup('template', item.path) is defined
  • `
  1. 1.Document required variables in templates:
jinja2
{#
Required variables:
  - port: The port to listen on
  - server_name: The server hostname
Optional variables:
  - ssl: Enable SSL (default: false)
#}
server {
    listen {{ port | default(80) }};
    server_name {{ server_name }};
}
  1. 1.Use defaults for all optional variables:
jinja2
server {
    listen {{ port | default(80) }};
    server_name {{ server_name | default('localhost') }};
    {% if ssl is defined and ssl %}
    listen {{ ssl_port | default(443) }} ssl;
    {% endif %}
}

Template errors require understanding Jinja2 syntax, variable scope, and filter usage. Use the debug module extensively, validate templates in isolation, and always provide defaults for optional variables.