Introduction

When Ansible renders Jinja2 templates, any reference to an undefined variable causes immediate failure. The AnsibleUndefinedVariable error halts playbook execution, blocking deployments until the missing variable is identified. This commonly occurs when templates assume data structures that don't exist for all hosts, when inventory changes remove variables, or when role defaults are incomplete.

The error message often points to the template file rather than the inventory location where the variable should be defined, making diagnosis challenging.

Symptoms

Template rendering fails with undefined variable:

bash
$ ansible-playbook deploy.yml
TASK [Render nginx configuration] **********************************************
fatal: [web-server-01]: FAILED! => {
    "changed": false,
    "msg": "AnsibleUndefinedVariable: 'nginx_worker_processes' is undefined"
}

Nested dictionary key undefined:

bash
$ ansible-playbook configure.yml
TASK [Create app config] *******************************************************
fatal: [app-server-02]: FAILED! => {
    "msg": "AnsibleUndefinedVariable: 'dict object' has no attribute 'ssl'"
}

Template file error with context:

bash
$ ansible-playbook site.yml
TASK [Template application config] *********************************************
fatal: [db-server-01]: FAILED! => ({
    "msg": "AnsibleUndefinedVariable: Unable to look up a name or access an attribute in template (templates/app.conf.j2).\nMake sure your variable name does not contain invalid characters like '-'.\n\nThe error was: 'db_connection_string' is undefined"
}

Variable works for some hosts but not others:

bash
$ ansible-playbook deploy.yml
TASK [Render config] ***********************************************************
ok: [web-server-01]
ok: [web-server-02]
fatal: [web-server-03]: FAILED! => {"msg": "'redis_host' is undefined"}

Common Causes

1. Variable Defined in Specific Group Vars

Variable exists for production but not staging:

```bash $ cat group_vars/production.yml redis_host: prod-redis.internal

$ cat group_vars/staging.yml # redis_host not defined ```

2. Nested Dictionary Missing Parent Key

Accessing child key when parent doesn't exist:

jinja2
# templates/app.conf.j2
ssl_certificate {{ app.ssl.cert_path }};
# Fails if app.ssl doesn't exist

3. Variable Precedence Confusion

Variable overridden at wrong precedence level:

```yaml # In role defaults app_port: 8080

# But extra-vars passes empty # ansible-playbook site.yml -e "app_port=" ```

4. Conditional Variable Not Always Set

Variable defined only in conditional block:

yaml
- set_fact:
    ssl_enabled: true
  when: use_ssl | default(false)
# ssl_enabled undefined when use_ssl is false

5. hostvars Access Without Host Running

Referencing hostvars for host that hasn't run:

jinja2
{{ hostvars['primary-db']['ansible_default_ipv4']['address'] }}
# Fails if primary-db hasn't gathered facts yet

6. Typo in Variable Name

Simple spelling error:

jinja2
# Template has:
server_name {{ sever_name }};
# But variable is defined as: server_name

Step-by-Step Fix

Step 1: Identify the Missing Variable

Run with verbose output to see error details:

```bash # High verbosity shows template context ansible-playbook site.yml -vvv

# Look for: # "msg": "AnsibleUndefinedVariable: 'variable_name' is undefined" # The error appears to be in 'templates/config.j2' ```

Debug the specific host:

```bash # Run for failing host only ansible-playbook site.yml --limit web-server-03 -vvv

# Check variable state ansible web-server-03 -m debug -a "var=hostvars[inventory_hostname]" ```

Step 2: Inspect Variable Scope

Check where variable should be defined:

```yaml # Add debug tasks before template task - name: Check variable state block: - name: Show all variables debug: var: hostvars[inventory_hostname] when: debug_mode | default(false)

  • name: Check specific variable
  • debug:
  • msg: |
  • Variable: app_port
  • Defined: {{ app_port is defined }}
  • Value: {{ app_port | default('UNDEFINED') }}
  • name: Show group membership
  • debug:
  • msg: "Groups: {{ group_names | join(', ') }}"
  • `

Check inventory files:

```bash # Check all group vars grep -r "app_port" group_vars/

# Check host vars grep -r "app_port" host_vars/

# Check role defaults grep -r "app_port" roles/*/defaults/ ```

Step 3: Add Safe Defaults for Optional Values

Use Jinja2 default filter:

```jinja2 # templates/nginx.conf.j2 worker_processes {{ nginx_worker_processes | default(ansible_processor_vcpus) }}; listen {{ nginx_port | default(80) }}; server_name {{ nginx_server_name | default('_') }};

# Use omit to skip parameter if undefined # In playbook: - template: src: config.j2 dest: /etc/app/config.yml mode: "{{ config_mode | default(omit) }}" ```

Complex default handling:

```jinja2 # Nested dictionary with defaults ssl_certificate {{ (app.ssl | default({})).cert_path | default('/etc/ssl/cert.pem') }};

# Default based on condition worker_processes {{ nginx_worker_processes | default(ansible_processor_vcpus if ansible_processor_vcpus is defined else 1) }};

# Default with error message for debugging {% set port = app_port | default('UNDEFINED') %} {% if port == 'UNDEFINED' %} # WARNING: app_port not defined, using default {% set port = 8080 %} {% endif %} listen {{ port }}; ```

Step 4: Handle Nested Dictionary Access Safely

Check parent existence before child access:

```jinja2 # WRONG - fails if app.ssl doesn't exist ssl_certificate {{ app.ssl.cert_path }};

# CORRECT - safe nested access {% set ssl_config = (app | default({})).ssl | default({}) %} {% if ssl_config.cert_path is defined %} ssl_certificate {{ ssl_config.cert_path }}; {% endif %}

# Using json_query for safe access ssl_certificate {{ app | json_query('ssl.cert_path') | default('') }}; ```

Create helper variables:

```yaml # In playbook, before template task - name: Set safe defaults set_fact: app_ssl: "{{ (app | default({})).ssl | default({}) }}" app_db: "{{ (app | default({})).database | default({}) }}"

  • name: Render template
  • template:
  • src: app.conf.j2
  • dest: /etc/app/app.conf
  • `

Step 5: Validate Required Variables Before Template

Add explicit validation:

```yaml - name: Validate required variables assert: that: - db_host is defined - db_port is defined - db_name is defined - app_name is defined fail_msg: | Required variables are missing for {{ inventory_hostname }}. Please check group_vars/{{ group_names | first }}.yml success_msg: "All required variables defined" run_once: "{{ validate_once | default(false) }}"

  • name: Render configuration
  • template:
  • src: app.conf.j2
  • dest: /etc/app/app.conf
  • `

Step 6: Handle Conditional Variables Properly

Initialize variables even when condition is false:

```yaml # WRONG - ssl_enabled undefined when use_ssl is false - set_fact: ssl_enabled: true when: use_ssl | default(false)

# CORRECT - always set - set_fact: ssl_enabled: "{{ use_ssl | default(false) | bool }}"

# Then in template: {% if ssl_enabled %} ssl_certificate /etc/ssl/cert.pem; {% endif %} ```

Step 7: Fix hostvars Access Issues

Handle cross-host variable access safely:

```jinja2 # WRONG - fails if primary-db hasn't gathered facts db_primary_ip: {{ hostvars['primary-db']['ansible_default_ipv4']['address'] }}

# CORRECT - safe access {% set db_host = hostvars.get('primary-db', {}).get('ansible_default_ipv4', {}).get('address', '127.0.0.1') %} db_primary_ip: {{ db_host }}

# Or check first {% if 'primary-db' in hostvars and 'ansible_default_ipv4' in hostvars['primary-db'] %} db_primary_ip: {{ hostvars['primary-db']['ansible_default_ipv4']['address'] }} {% else %} db_primary_ip: 127.0.0.1 {% endif %} ```

Force facts gathering before cross-host access:

yaml
- name: Gather facts from all hosts
  setup:
  delegate_to: "{{ item }}"
  delegate_facts: true
  loop: "{{ groups['databases'] }}"
  when: hostvars[item]['ansible_default_ipv4'] is not defined

Step 8: Create Variable Validation Playbook

Create a standalone validation playbook:

```yaml # validate_variables.yml - name: Validate all required variables hosts: all gather_facts: true vars: required_vars: - db_host - db_port - app_name

tasks: - name: Check each required variable debug: msg: "{{ item }}: {{ lookup('vars', item, default='UNDEFINED') }}" loop: "{{ required_vars }}"

  • name: Validate required variables exist
  • assert:
  • that: "{{ item }} is defined"
  • fail_msg: "{{ item }} is not defined for {{ inventory_hostname }}"
  • loop: "{{ required_vars }}"
  • name: Report variable sources
  • debug:
  • msg: "{{ item }} defined in: {{ hostvars[inventory_hostname][item] | default('nowhere') }}"
  • loop: "{{ required_vars }}"
  • `

Verification

Test template rendering:

```bash # Run playbook with verbose ansible-playbook site.yml --tags template -v

# Check template was rendered ansible all -m shell -a "cat /etc/app/app.conf"

# Verify all hosts succeeded ansible-playbook site.yml --check ```

Verify variable availability:

```bash # Check variable for all hosts ansible all -m debug -a "msg={{ app_port | default('UNDEFINED') }}"

# Verify nested access ansible all -m debug -a "msg={{ (app | default({})).ssl | default({}) }}"

# Run validation playbook ansible-playbook validate_variables.yml ```

  • [ansible-playbook-undefined-variable-jinja2-strict-mode](/articles/ansible-playbook-undefined-variable-jinja2-strict-mode)
  • [ansible-variable-precedence-debugging](/articles/ansible-variable-precedence-debugging)
  • [ansible-template-best-practices](/articles/ansible-template-best-practices)
  • [WordPress troubleshooting: Ansible Artifact Download Uses an Old Mi](ansible-artifact-download-uses-an-old-mirror-after-proxy-change)
  • [WordPress troubleshooting: Ansible Audit Trail Misses Events Under ](ansible-audit-trail-misses-events-under-burst-load)
  • [WordPress troubleshooting: Ansible Background Worker Gets Stuck in ](ansible-background-worker-stuck-in-a-retry-loop)
  • [WordPress troubleshooting: Ansible Backup Completes but Restore Fai](ansible-backup-completes-but-restore-fails-checksum-validation)
  • [WordPress troubleshooting: Ansible Batch Importer Duplicates Rows A](ansible-batch-importer-duplicates-rows-after-a-retry)

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "WordPress troubleshooting: Ansible Jinja2 Template Variable Undefin", "description": "Learn how to fix Ansible Jinja2 Template Variable Undefined Error. Professional WordPress troubleshooting solutions with step-by-step guidance. WP error fix, WordPress optimization, WP security, WordPress performance.", "url": "https://www.fixwikihub.com/ansible-template-jinja2-variable-undefined", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-01-22T10:44:03.967Z", "dateModified": "2026-01-22T10:44:03.967Z" } </script>