Introduction
The AnsibleUndefinedVariable error is one of the most common Ansible errors. It occurs when a playbook references a variable that hasn't been defined anywhere — inventory, group_vars, host_vars, role defaults, or task vars. This guide shows every way to fix, prevent, and debug undefined variable errors.
The Error
fatal: [webserver1]: FAILED! => {
"msg": "The task includes an option with an undefined variable.
The error was: 'my_variable' is undefined.
The error appears to be in '/home/user/playbook.yml': line 8, column 7"
}
Why It Happens
# 1. Variable never defined anywhere
- name: This fails
ansible.builtin.debug:
msg: "{{ app_port }}" # Where is app_port defined? Nowhere.
# 2. Typo in variable name
vars:
application_port: 8080
- name: Typo
ansible.builtin.debug:
msg: "{{ applicaton_port }}" # Missing 'i' — silent until runtime
# 3. Variable defined for wrong host/group
# group_vars/webservers.yml defines 'nginx_port'
# But task runs on host in 'databases' group — no nginx_port
# 4. Conditional variable
- name: Only set in some cases
ansible.builtin.set_fact:
result_path: "/tmp/output"
when: generate_output | default(false)
- name: Fails if generate_output was false
ansible.builtin.copy:
src: "{{ result_path }}" # Undefined if skipped above
dest: /opt/output
Fix 1: The default Filter
The most common fix — provide a fallback value:
# Simple default
- name: Use default value
ansible.builtin.debug:
msg: "Port is {{ app_port | default(8080) }}"
# Default empty string
- name: Optional variable
ansible.builtin.debug:
msg: "Extra args: {{ extra_args | default('') }}"
# Default empty list
- name: Optional list
ansible.builtin.debug:
msg: "Packages: {{ extra_packages | default([]) }}"
# Default empty dict
- name: Optional dict
ansible.builtin.user:
name: "{{ user.name }}"
groups: "{{ user.groups | default(omit) }}"
# Chained defaults
- name: First defined wins
ansible.builtin.debug:
msg: "{{ custom_port | default(env_port) | default(8080) }}"
# default(omit) — skip the parameter entirely
- name: Optional parameter
ansible.builtin.apt:
name: nginx
state: "{{ nginx_state | default(omit) }}"
# If nginx_state is undefined, 'state' parameter is omitted
Fix 2: Conditional Checks
# Check if defined before using
- name: Only if variable exists
ansible.builtin.debug:
msg: "{{ special_config }}"
when: special_config is defined
# Ternary with defined check
- name: Conditional value
ansible.builtin.set_fact:
config_path: "{{ custom_path if custom_path is defined else '/etc/default/app.conf' }}"
# Block with when
- name: Optional configuration block
when: advanced_config is defined
block:
- name: Deploy advanced config
ansible.builtin.template:
src: advanced.conf.j2
dest: /etc/app/advanced.conf
- name: Restart service
ansible.builtin.systemd:
name: app
state: restarted
Fix 3: Mandatory Variables
Sometimes you want the error — to fail fast when required variables are missing:
# Fail with clear message
- name: Validate required variables
ansible.builtin.assert:
that:
- db_host is defined
- db_password is defined
- app_domain is defined
fail_msg: "Required variables missing. Set db_host, db_password, and app_domain."
# mandatory filter — fails with clear error
- name: Must be defined
ansible.builtin.debug:
msg: "Database: {{ db_host | mandatory }}"
# Error: "Mandatory variable 'db_host' not defined"
# Role argument_specs (Ansible 2.11+)
# roles/webapp/meta/argument_specs.yml
argument_specs:
main:
short_description: Deploy web application
options:
app_domain:
type: str
required: true
description: Domain name for the application
app_port:
type: int
required: false
default: 8080
description: Port to listen on
Fix 4: Variable Precedence
# Ansible checks 22 levels of precedence (highest wins):
# 1. extra vars (-e) ← HIGHEST
# 2. include params
# 3. role params
# 4. set_facts / registered vars
# 5. task vars
# 6. block vars
# 7. role vars (roles/x/vars/main.yml)
# 8. play vars_files
# 9. play vars_prompt
# 10. play vars
# 11. host_vars (host_vars/hostname.yml)
# 12. group_vars (most specific group)
# 13. group_vars (parent groups)
# 14. group_vars/all
# 15. inventory host_vars
# 16. inventory group_vars
# 17. inventory file vars
# 18. role defaults (roles/x/defaults/main.yml) ← LOWEST
# 19. command line values
# Common mistake: defining in role defaults but overriding with empty
# roles/webapp/defaults/main.yml
app_port: 8080 # Good default
# group_vars/staging.yml
app_port: # Oops — this sets it to None, not "use default"
Debugging Undefined Variables
# Debug all variables for a host
- name: Show all variables
ansible.builtin.debug:
var: vars
# Or specific namespaces:
# var: hostvars[inventory_hostname]
# var: group_names
# var: ansible_facts
# Check where a variable comes from
- name: Variable origin
ansible.builtin.debug:
msg: |
app_port value: {{ app_port | default('UNDEFINED') }}
app_port type: {{ app_port | default('') | type_debug }}
# Run with verbose to see variable loading
# ansible-playbook playbook.yml -vvv
# List all variables for a host
# ansible -m debug -a "var=hostvars[inventory_hostname]" webserver1
Nested Variable Errors
# Nested dict access — common trap
- name: Fails if 'database' or 'host' key missing
ansible.builtin.debug:
msg: "{{ config.database.host }}"
# Fix: Use default at each level
- name: Safe nested access
ansible.builtin.debug:
msg: "{{ (config | default({})).get('database', {}).get('host', 'localhost') }}"
# Or use the dict default pattern
- name: Cleaner nested default
ansible.builtin.debug:
msg: "{{ config.database.host | default('localhost') }}"
when: config is defined and config.database is defined
# Best fix: Flatten in role defaults
# roles/webapp/defaults/main.yml
db_host: localhost
db_port: 5432
db_name: app
# Instead of nested: config.database.host
Common Mistakes
# ❌ MISTAKE 1: default(false) vs default(False)
# Both work, but be consistent
- name: Boolean default
ansible.builtin.debug:
msg: "{{ enable_debug | default(false) }}"
# ❌ MISTAKE 2: Forgetting that registered vars are host-scoped
- name: Run on first host
ansible.builtin.command: hostname
register: result
run_once: true
- name: Fails on other hosts
ansible.builtin.debug:
msg: "{{ result.stdout }}"
# FIX: Use hostvars[groups['all'][0]].result.stdout
# ❌ MISTAKE 3: Variable in when clause
- name: Wrong — Jinja2 in when
ansible.builtin.debug:
msg: "hello"
when: "{{ my_var }}" # Don't use {{ }} in when
# FIX:
when: my_var | default(false)
# ❌ MISTAKE 4: Undefined in template
# template.j2: Server={{ server_name }}
# If server_name is undefined, template task fails
# FIX: Use default in template:
# Server={{ server_name | default('localhost') }}
Prevent Undefined Variables Globally
# ansible.cfg
[defaults]
# Make undefined variables an error (default behavior)
error_on_undefined_vars = true
# Or for development, make them warnings
# error_on_undefined_vars = false
# (Variables render as empty string instead of failing)
Related Articles
- Ansible Variables
- Ansible Jinja2 Templates
- Ansible Troubleshooting
- Ansible group_vars vs host_vars
- Ansible Error Handling
Conclusion
Undefined variable errors have three fixes: default() filter for optional variables, is defined checks for conditional logic, and mandatory filter or argument_specs for required variables. Most errors come from typos, wrong host/group scope, or missing role defaults. Use ansible-playbook -vvv to trace where variables are loaded from, and use assert tasks to validate required variables early in your playbook.