Introduction
AnsibleUndefinedVariable: 'variable_name' is undefined is one of the most common Ansible errors. It means a variable you referenced doesn't exist at the point where Ansible tries to use it. This guide covers 12 real-world causes with exact fixes.
Error Format
fatal: [server1]: FAILED! => {"msg": "The task includes an option with an
undefined variable. The error was: 'my_variable' is undefined.
...
The error appears to be in '/path/to/playbook.yml': line 10, column 7"}
1. Simple Typo
# ❌ Typo in variable name
- ansible.builtin.debug:
msg: "Port is {{ http_potr }}"
# Should be 'http_port'
# ✅ Fix the typo
- ansible.builtin.debug:
msg: "Port is {{ http_port }}"
2. Variable Not Defined Anywhere
# ❌ Variable never set
- ansible.builtin.debug:
msg: "Database: {{ db_name }}"
# ✅ Define it in vars, group_vars, or defaults
- hosts: all
vars:
db_name: myapp
tasks:
- ansible.builtin.debug:
msg: "Database: {{ db_name }}"
3. Missing default() Filter
# ❌ Crashes if optional_var is not defined
- ansible.builtin.debug:
msg: "Value: {{ optional_var }}"
# ✅ Use default() for optional variables
- ansible.builtin.debug:
msg: "Value: {{ optional_var | default('fallback') }}"
# ✅ Use default(omit) to skip the parameter entirely
- ansible.builtin.user:
name: deploy
shell: "{{ user_shell | default(omit) }}"
4. Variable from Another Host (hostvars)
# ❌ Trying to access another host's variable directly
- ansible.builtin.debug:
msg: "DB IP: {{ db_ip }}"
# db_ip is set on db-server, not on web-server
# ✅ Use hostvars to access other hosts' variables
- ansible.builtin.debug:
msg: "DB IP: {{ hostvars['db-server']['ansible_host'] }}"
# ✅ Or use hostvars with groups
- ansible.builtin.debug:
msg: "DB IP: {{ hostvars[groups['dbservers'][0]]['ansible_default_ipv4']['address'] }}"
5. Facts Not Gathered
# ❌ Facts unavailable when gather_facts is disabled
- hosts: all
gather_facts: false
tasks:
- ansible.builtin.debug:
msg: "OS: {{ ansible_distribution }}"
# ERROR: ansible_distribution is undefined
# ✅ Option A: Enable fact gathering
- hosts: all
gather_facts: true
# ✅ Option B: Gather facts manually
- hosts: all
gather_facts: false
tasks:
- ansible.builtin.setup:
gather_subset: [distribution]
- ansible.builtin.debug:
msg: "OS: {{ ansible_facts['distribution'] }}"
6. Variable Scope in Blocks
# ❌ Variable set inside block/rescue not available outside
- block:
- ansible.builtin.command: cat /etc/config
register: config_output
rescue:
- ansible.builtin.debug:
msg: "Failed"
- ansible.builtin.debug:
msg: "{{ config_output.stdout }}"
# May be undefined if rescue ran (task didn't succeed)
# ✅ Set a default before the block
- ansible.builtin.set_fact:
config_output: { stdout: "" }
- block:
- ansible.builtin.command: cat /etc/config
register: config_output
rescue:
- ansible.builtin.debug:
msg: "Using empty default"
7. Conditional Variable with when
# ❌ Variable only set when condition is true
- ansible.builtin.set_fact:
special_config: "enabled"
when: special_feature
- ansible.builtin.debug:
msg: "Config: {{ special_config }}"
# Undefined if special_feature was false!
# ✅ Always define, or use default()
- ansible.builtin.debug:
msg: "Config: {{ special_config | default('disabled') }}"
8. Registered Variable When Task Skipped
# ❌ Registered var is undefined when task is skipped
- ansible.builtin.command: whoami
register: user_result
when: check_user | default(false)
- ansible.builtin.debug:
msg: "User: {{ user_result.stdout }}"
# Undefined if task was skipped!
# ✅ Check if variable exists and is not skipped
- ansible.builtin.debug:
msg: "User: {{ user_result.stdout }}"
when:
- user_result is defined
- user_result is not skipped
9. Dict Key Missing
# ❌ Key doesn't exist in the dictionary
- ansible.builtin.debug:
msg: "IP: {{ ansible_facts['eth0']['ipv4']['address'] }}"
# Fails if eth0 doesn't exist
# ✅ Use default filter on nested access
- ansible.builtin.debug:
msg: "IP: {{ ansible_facts.get('eth0', {}).get('ipv4', {}).get('address', 'N/A') }}"
# ✅ Or use Jinja2 default
- ansible.builtin.debug:
msg: "IP: {{ (ansible_facts['eth0']['ipv4']['address']) | default('N/A') }}"
10. Variable in Role Defaults Not Overridden
# roles/myapp/defaults/main.yml
app_port: 8080
# app_secret: ← intentionally not set, must be provided
# ❌ Playbook doesn't provide required variable
- hosts: all
roles:
- myapp
# ✅ Provide required variables
- hosts: all
roles:
- role: myapp
vars:
app_secret: "{{ vault_app_secret }}"
# ✅ Or validate with assert
# roles/myapp/tasks/main.yml
- ansible.builtin.assert:
that: app_secret is defined
fail_msg: "app_secret must be defined!"
11. Loop Variable Name Collision
# ❌ Using 'item' in nested loops
- ansible.builtin.include_tasks: inner.yml
loop: "{{ outer_list }}"
# inner.yml also has a loop using 'item' — collision!
# ✅ Use loop_control to rename loop variables
- ansible.builtin.include_tasks: inner.yml
loop: "{{ outer_list }}"
loop_control:
loop_var: outer_item
12. Jinja2 Variable Scope in Loops
# ❌ Variable set inside Jinja2 loop not available outside
- ansible.builtin.set_fact:
result: >-
{% set found = false %}
{% for item in my_list %}
{% if item == 'target' %}
{% set found = true %}
{% endif %}
{% endfor %}
{{ found }}
{# 'found' inside loop is a different scope! Always false #}
# ✅ Use namespace for Jinja2 loop variables
- ansible.builtin.set_fact:
result: >-
{% set ns = namespace(found=false) %}
{% for item in my_list %}
{% if item == 'target' %}
{% set ns.found = true %}
{% endif %}
{% endfor %}
{{ ns.found }}
Prevention Best Practices
# 1. Always use default() for optional variables
port: "{{ custom_port | default(8080) }}"
# 2. Validate required variables early
- ansible.builtin.assert:
that:
- db_host is defined
- db_password is defined
fail_msg: "Required database variables are missing"
# 3. Use ansible-lint to catch undefined variables
# pip install ansible-lint
# ansible-lint playbook.yml
# 4. Use --check mode to test without executing
# ansible-playbook site.yml --check
Ansible 13 Note
In ansible-core 2.20, INJECT_FACTS_AS_VARS is deprecated. If you access facts as ansible_distribution instead of ansible_facts['distribution'], you'll get deprecation warnings now and undefined errors in ansible-core 2.24.
Related Articles
Conclusion
Most undefined variable errors come from a few causes: typos, missing default() filters, skipped tasks with register, or accessing variables from other hosts without hostvars. The fix pattern is consistent: use default() for optional variables, assert for required ones, and always check if registered variables are defined before using them. In Ansible 13+, start migrating from injected facts (ansible_distribution) to the ansible_facts dictionary to avoid future breakage.