The Problem
Your playbook uses set_fact to define variables dynamically, but you encounter errors:
fatal: [server]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'my_var' is undefined"}Or the fact you set doesn't exist in later tasks:
fatal: [server]: FAILED! => {"msg": "'hostvars[inventory_hostname']['my_fact']' is undefined"}Or facts set on one host aren't available on others.
Why This Happens
set_fact errors occur because:
Variable in value undefined - Referencing a variable that doesn't exist.
Scope misunderstanding - Facts are host-specific, not global.
Jinja2 syntax errors - Incorrect template syntax in the value.
Facts not available across hosts - Need hostvars to access other hosts' facts.
Timing issues - Using facts before they're set.
Understanding set_fact
set_fact creates host-specific variables that persist for the playbook run:
```yaml - hosts: webservers tasks: - name: Set a fact set_fact: my_config: "value"
- name: Use the fact
- debug:
- msg: "{{ my_config }}" # Works on same host
`
Diagnosing the Issue
Debug the fact value:
- name: Debug fact
debug:
msg: "my_fact is {{ my_fact | default('UNDEFINED') }}"Check hostvars for cross-host access:
- name: Check other host facts
debug:
msg: "{{ hostvars['other_host']['my_fact'] | default('UNDEFINED') }}"Run with verbosity:
ansible-playbook playbook.yml -vvThe Fix
Fix 1: Handle Undefined Variables in set_fact
Use defaults for potentially undefined variables:
```yaml # WRONG - fails if source_var undefined - name: Set fact set_fact: my_fact: "{{ source_var }}"
# CORRECT - use default - name: Set fact set_fact: my_fact: "{{ source_var | default('default_value') }}" ```
Complex defaults:
- name: Set fact with complex default
set_fact:
app_config:
port: "{{ app_port | default(8080) }}"
name: "{{ app_name | default('myapp') }}"
enabled: "{{ app_enabled | default(true) }}"Fix 2: Understand Host-Specific Scope
Facts are per-host, not global:
```yaml - hosts: web1,web2 tasks: - name: Set fact on each host set_fact: host_id: "{{ inventory_hostname | regex_replace('web', '') }}"
- name: Show host's own fact
- debug:
- msg: "My ID: {{ host_id }}" # Works
- name: Show other host's fact (WRONG)
- debug:
- msg: "Other ID: {{ hostvars.web2.host_id }}"
- when: inventory_hostname == 'web1'
- # Works because we access hostvars
`
Fix 3: Access Facts Across Hosts
Use hostvars for cross-host fact access:
```yaml - hosts: web1,web2 tasks: - name: Set shared config on first host set_fact: shared_token: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters') }}" run_once: yes delegate_to: web1
- name: Access from other hosts
- debug:
- msg: "Token: {{ hostvars['web1']['shared_token'] }}"
- # All hosts can access web1's facts via hostvars
`
Fix 4: Set Facts for All Hosts
Use run_once with delegate_facts:
```yaml - hosts: webservers tasks: - name: Generate common fact once set_fact: deployment_id: "{{ ansible_date_time.epoch }}" run_once: yes delegate_to: "{{ groups['webservers'][0] }}" # Only sets on first host
- name: Access from all hosts
- debug:
- msg: "{{ hostvars[groups['webservers'][0]]['deployment_id'] }}"
`
Or set on all hosts:
- name: Set on all hosts
set_fact:
deployment_id: "{{ hostvars[groups['webservers'][0]]['deployment_id'] }}"
when: hostvars[groups['webservers'][0]]['deployment_id'] is definedFix 5: Fix Jinja2 Syntax in set_fact
Complex values need proper syntax:
```yaml # WRONG - unquoted colon - name: Set fact set_fact: my_url: http://example.com:8080 # YAML parsing error
# CORRECT - quote the value - name: Set fact set_fact: my_url: "http://example.com:8080" ```
Dictionary values:
- name: Set dictionary fact
set_fact:
config:
host: "{{ inventory_hostname }}"
port: 8080
enabled: true
# Proper YAML dictionaryJSON strings:
- name: Parse JSON into fact
set_fact:
parsed_data: "{{ raw_json | from_json }}"Fix 6: Handle Fact Dependencies
Order matters for dependent facts:
```yaml - hosts: webservers tasks: # WRONG - second_fact depends on first_fact before it's set - name: Set second fact set_fact: second_fact: "{{ first_fact }}/subdir"
- name: Set first fact
- set_fact:
- first_fact: "/opt/app"
# CORRECT - set in correct order - hosts: webservers tasks: - name: Set first fact set_fact: first_fact: "/opt/app"
- name: Set second fact
- set_fact:
- second_fact: "{{ first_fact }}/subdir"
`
Fix 7: Use cacheable Facts
Make facts survive playbook runs:
- name: Set cacheable fact
set_fact:
my_persistent_fact: "value"
cacheable: yes
# Fact available in later playbook runs via hostvarsFix 8: Convert Between Types
Type conversion in set_fact:
```yaml - name: Convert to integer set_fact: port_int: "{{ port_string | int }}"
- name: Convert to list
- set_fact:
- items_list: "{{ items_string | split(',') }}"
- name: Convert to boolean
- set_fact:
- enabled_bool: "{{ enabled_string | bool }}"
`
Verifying the Fix
Test fact persistence:
```yaml # test_facts.yml - hosts: localhost gather_facts: no tasks: - name: Set fact set_fact: test_fact: "hello world"
- name: Verify fact
- assert:
- that:
- - test_fact == "hello world"
- success_msg: "Fact set correctly"
- name: Check fact in hostvars
- debug:
- msg: "{{ hostvars['localhost']['test_fact'] }}"
`
Run:
ansible-playbook test_facts.yml -vPrevention
Validate facts before using:
- name: Verify required facts exist
assert:
that:
- my_fact is defined
- my_fact | length > 0
fail_msg: "Required fact my_fact must be set"Document fact dependencies:
# Requires: first_fact must be set in previous task
- name: Set dependent fact
set_fact:
second_fact: "{{ first_fact }}/path"