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:
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:
fatal: [webserver01]: FAILED! => {"msg": "template error while templating string: expected token ':', got '}'. String: server {\n listen {{ port }\n}"}Or filter errors:
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:
ansible-playbook site.yml -vvv 2>&1 | grep -A 5 "template error"For template files:
# The error shows the template path
# Look for: "could not locate file in lookup"
ansible-playbook site.yml --checkCheck if template file exists:
- 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:
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:
# This fails if port is undefined
server {
listen {{ port }};
}Provide defaults:
# With default value
server {
listen {{ port | default(80) }};
}Or make the entire block conditional:
{% 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:
# 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:
# Access registered output
port={{ app_port.stdout }}Step 5: Debug Template Rendering
Use the template lookup to debug:
- 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:
ansible-doc -t filter -lOr debug filter output:
- name: Test filter
debug:
msg: "{{ my_list | join(',') }}"Step 7: Handle Complex Data Structures
Accessing nested data incorrectly:
# 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:
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:
{% for item in items %}
{{ item }}
{% endfor %}Produces extra blank lines. Use whitespace control:
{% 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:
ansible-playbook validate_template.ymlStep 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:
{% 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.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.Document required variables in templates:
{#
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.Use defaults for all optional variables:
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.