Introduction
The VARIABLE IS NOT DEFINED! error is one of the most common Ansible troubleshooting scenarios. Most of the time the root cause is a misspelled variable name or a variable that was never set. However, there is a special case involving ansible_hostname and other Ansible facts that catches many users off guard — the variable exists, is spelled correctly, and yet Ansible reports it as undefined.
This article explains why this happens, the difference between ansible_hostname and inventory_hostname, how gather_facts controls fact availability, and multiple strategies to fix and prevent this error.
Understanding the Error
When Ansible encounters an undefined variable during template rendering or task execution, it produces output like this:
"ansible_hostname": "VARIABLE IS NOT DEFINED!"
or in newer Ansible versions:
fatal: [demo.example.com]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible_hostname' is undefined."}
This happens because ansible_hostname is an Ansible fact — a variable that is automatically populated when Ansible gathers facts from the target host. If fact gathering is disabled, the variable simply does not exist.
Why ansible_hostname Requires gather_facts
Ansible facts are collected by the setup module, which runs automatically at the beginning of each play when gather_facts: true (the default). Facts include:
| Fact Variable | Description | Example Value |
|---|---|---|
ansible_hostname | Short hostname of the target | webserver01 |
ansible_fqdn | Fully qualified domain name | webserver01.example.com |
ansible_os_family | OS family | RedHat, Debian |
ansible_distribution | Distribution name | Ubuntu, CentOS |
ansible_default_ipv4.address | Primary IPv4 address | 192.168.1.100 |
ansible_memtotal_mb | Total memory in MB | 8192 |
When you set gather_facts: false, none of these variables are available unless you explicitly call the setup module later in the play.
ansible_hostname vs inventory_hostname
A critical distinction that often confuses users:
ansible_hostname — Requires fact gathering. Returns the actual hostname as reported by the target machine (output of hostname -s). This is a runtime fact collected from the remote host.
inventory_hostname — Always available. Returns the hostname as defined in your Ansible inventory file. This is a magic variable that does not require fact gathering.
# inventory
[webservers]
web01.example.com ansible_host=192.168.1.10
In this example:
inventory_hostname=web01.example.com(always available)ansible_hostname= whateverhostname -sreturns on the target (requires facts)
Error Playbook
The following playbook triggers the error because gather_facts: false prevents fact collection:
---
- name: hostname Playbook
hosts: all
gather_facts: false
tasks:
- name: print hostname
ansible.builtin.debug:
var: ansible_hostname
Error Output
$ ansible-playbook -i inventory troubleshooting/variablenotdefined_error.yml
PLAY [hostname Playbook] **********************
TASK [print hostname] *************************
ok: [demo.example.com] => {
"ansible_hostname": "VARIABLE IS NOT DEFINED!"
}
PLAY RECAP ************************************
demo.example.com : ok=1 changed=0 unreachable=0 failed=0
Solution 1: Enable gather_facts
The simplest fix — set gather_facts: true (or remove the line, since true is the default):
---
- name: hostname Playbook
hosts: all
gather_facts: true
tasks:
- name: print hostname
ansible.builtin.debug:
var: ansible_hostname
Fixed Output
$ ansible-playbook -i inventory troubleshooting/variablenotdefined_fix.yml
PLAY [hostname Playbook] **********************
TASK [Gathering Facts] ************************
ok: [demo.example.com]
TASK [print hostname] *************************
ok: [demo.example.com] => {
"ansible_hostname": "webserver01"
}
Solution 2: Use inventory_hostname Instead
If you disabled fact gathering for performance reasons and only need the host identifier, use inventory_hostname:
---
- name: hostname Playbook
hosts: all
gather_facts: false
tasks:
- name: print hostname from inventory
ansible.builtin.debug:
var: inventory_hostname
- name: print short hostname
ansible.builtin.debug:
msg: "{{ inventory_hostname_short }}"
Solution 3: Gather Facts Selectively
If you need ansible_hostname but want to minimize fact-gathering overhead, gather only the subset you need:
---
- name: hostname Playbook
hosts: all
gather_facts: false
tasks:
- name: gather only network facts
ansible.builtin.setup:
gather_subset:
- network
- min
- name: print hostname
ansible.builtin.debug:
var: ansible_hostname
Available subsets include: all, min, hardware, network, virtual, ohai, facter.
Solution 4: Use default Filter for Graceful Fallback
When you are unsure whether facts will be available, use the default filter to provide a fallback value:
---
- name: hostname Playbook
hosts: all
gather_facts: false
tasks:
- name: print hostname with fallback
ansible.builtin.debug:
msg: "Host: {{ ansible_hostname | default(inventory_hostname) }}"
Solution 5: Cache Facts Across Plays
If your playbook has multiple plays and only the first gathers facts, enable fact caching so subsequent plays can access them:
# ansible.cfg
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 3600
With gathering = smart, Ansible only gathers facts if they are not already cached.
Common Scenarios That Trigger This Error
Scenario 1: Disabled gather_facts for Performance
- hosts: all
gather_facts: false # <-- facts not collected
tasks:
- name: configure hostname
ansible.builtin.hostname:
name: "{{ ansible_hostname }}-new" # FAILS
Fix: Either enable gather_facts or use inventory_hostname.
Scenario 2: Using ansible_hostname in a Role with gather_facts Disabled at Play Level
- hosts: all
gather_facts: false
roles:
- configure_network # role templates use ansible_hostname
Fix: Add gather_facts: true at the play level, or add a setup task in the role's tasks/main.yml.
Scenario 3: Cross-Play Variable Access
- name: Play 1
hosts: webservers
gather_facts: true
tasks:
- name: do something
ansible.builtin.debug:
msg: "OK"
- name: Play 2
hosts: dbservers
gather_facts: false
tasks:
- name: reference webserver hostname
ansible.builtin.debug:
msg: "{{ hostvars['web01']['ansible_hostname'] }}" # May fail
Fix: Enable fact caching or gather facts in both plays.
Other Commonly Undefined Ansible Facts
The same VARIABLE IS NOT DEFINED! error applies to all facts when gather_facts: false:
ansible_distribution— OS distribution nameansible_os_family— OS family (RedHat, Debian, Suse, etc.)ansible_default_ipv4— Default IPv4 network infoansible_memtotal_mb— Total RAMansible_processor_vcpus— CPU countansible_kernel— Kernel versionansible_env— Environment variables
Debugging Undefined Variables
Use the ansible.builtin.debug module to inspect available variables:
- name: list all facts
ansible.builtin.debug:
var: ansible_facts
- name: check if variable is defined
ansible.builtin.debug:
msg: "ansible_hostname is {{ 'defined' if ansible_hostname is defined else 'NOT defined' }}"
Run with increased verbosity to see fact gathering:
ansible-playbook -i inventory playbook.yml -vvv
Best Practices
- Leave
gather_facts: trueunless you have a specific reason to disable it — the performance impact is usually minimal - Use
inventory_hostnamewhen you need the inventory name, not the actual machine hostname - Use
gather_subsetto minimize overhead if fact gathering is slow (e.g., large cloud environments) - Enable fact caching for multi-play playbooks and large inventories
- Use the
defaultfilter when a variable might not be defined:{{ var | default('fallback') }} - Document which plays require facts — add comments when setting
gather_facts: false
Related Articles
- Ansible become Privilege Escalation
- Ansible Variables Guide
- Ansible debug Module
- Ansible Playbook Guide
Conclusion
The VARIABLE IS NOT DEFINED! ansible_hostname error is almost always caused by gather_facts: false in the play definition. The fix is straightforward — either enable fact gathering, use inventory_hostname as an alternative, gather facts selectively with setup and gather_subset, or use the default filter for graceful fallback. Understanding the distinction between runtime facts and magic variables is key to writing robust Ansible playbooks.