Advanced 10 min · 2026-06-21

Ansible Variables Deep Dive: From Inventory to Extra-Vars, Facts, and Debugging

Master Ansible variables and facts: inventory, playbook, role, extra-vars, register, set_fact, hostvars, ansible_facts, gather_facts:false trade-offs, and debug module with production examples..

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
June 21, 2026
last updated
1,596
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Variable precedence (lowest to highest): inventory → playbook → role defaults → role vars → set_fact/register → extra-vars (always win). Use ansible-inventory --list --yaml to dump all inventory variables. Facts are slow: gather_facts: false cuts playbook runtime by 30-50% if you don't need system info. Debug with -i inventory -m debug -a 'var=hostvars[inventory_hostname]' to inspect all variables for a host. register captures task output; access via result.stdout, result.rc, etc. set_fact creates variables that persist for the host across plays, but only within the same playbook run. hostvars gives cross-host access; use hostvars['webserver1']['ansible_default_ipv4']['address']. Extra-vars override everything: ansible-playbook -e 'myvar=override' — be careful in production. Always use --check and --diff before applying variable-heavy changes. Facts are cached with gather_facts: no and setup cache plugin; use fact_caching_timeout to control staleness.

✦ Definition~90s read
What is Ansible Variables and Facts?

Ansible variables are named values that parameterize your automation. They allow you to write plays that adapt to different environments, hosts, or conditions. Variables can be defined in many places: inventory files (host_vars, group_vars), playbooks (vars, vars_files), roles (defaults/main.yml, vars/main.yml), command-line extra-vars, and dynamically via register and set_fact.

Think of Ansible variables like sticky notes you put on a server.

Facts are a special kind of variable automatically gathered by Ansible when gather_facts: true (default). They contain system information like OS distribution, network interfaces, memory, and CPU. Facts are stored in the ansible_facts dictionary and can be accessed like any other variable (e.g., ansible_facts['os_family']).

The gather_facts mechanism runs the setup module on each target host, which collects a large amount of data.

The variable system solves the problem of hardcoding values. Without variables, you'd need separate playbooks for each environment. With variables, you use the same playbook and inject environment-specific values. The challenge is managing precedence: when the same variable is defined in multiple places, which value wins? Understanding the precedence order is critical to avoid surprises.

Plain-English First

Think of Ansible variables like sticky notes you put on a server. Some notes are written on the server itself (inventory variables), some are in the instruction book (playbook variables), and some are shouted at the last minute (extra-vars). Facts are like a full health report the server generates every time you run a playbook—it tells you everything: CPU, memory, IP addresses. But generating that report takes time, so sometimes you skip it (gather_facts: false) if you already know what you need. The register keyword is like taking a snapshot of what a command outputted and writing that on a new sticky note. set_fact is like writing a new sticky note based on calculations. hostvars is like being able to read the sticky notes on any other server in the room. Extra-vars are like the boss yelling a change from the doorway—it overrides everything else.

I once spent a weekend debugging why a playbook deployed the wrong version of an application to production. The symptom was subtle: the app version was correct on 9 out of 10 servers, but one server got an older build. After hours of grepping logs and scratching my head, I discovered the root cause: a variable defined in the inventory file for that specific host was being overridden by a role default variable with a lower priority than I assumed. The variable precedence ladder is not always intuitive, and a single misplaced variable can cause silent failures.

Historically, Ansible started with simple inventory variables and expanded to support complex automation needs. The variable system grew organically: playbook vars, role vars, include_vars, vars_prompt, and extra-vars. Facts were introduced to provide system introspection, but they come at a performance cost. The community has learned hard lessons about variable scoping, precedence, and debugging.

This article covers all variable types: inventory, playbook, role (defaults and vars), and extra-vars. You'll learn how to use register to capture task output, set_fact to create derived variables, the hostvars magic variable for cross-host data, and the ansible_facts dictionary for system facts. We'll explore the trade-offs of gather_facts: false and how to use the debug module effectively. Real production incidents and debugging patterns are included throughout.

1. Variable Types: Where to Define Them

Ansible variables can be defined in multiple places, each with a specific precedence. From lowest to highest: 1. Inventory variables: host_vars, group_vars (e.g., group_vars/all.yml, host_vars/web1.yml). 2. Playbook variables: vars: block in a play or vars_files:. 3. Role defaults: roles/role_name/defaults/main.yml — lowest precedence within a role. 4. Role vars: roles/role_name/vars/main.yml — higher precedence than defaults. 5. Block and task variables: vars: within a block or task. 6. set_fact / register: Dynamic variables created during execution. 7. Extra-vars: -e 'key=value' or --extra-vars @file.json — highest precedence.

Production pattern: Use inventory variables for environment-specific values, role defaults for sane defaults, and extra-vars only for emergency overrides. Avoid defining the same variable in multiple places; it leads to confusion.

Code example: ``yaml # inventory/group_vars/all.yml app_port: 8080 app_user: deploy ` `yaml # playbook.yml - hosts: webservers vars: app_port: 9090 # overrides inventory roles: - role: app ` `yaml # roles/app/defaults/main.yml app_port: 3000 # lowest priority app_user: root ``

To see the resolved value, use: ``bash ansible web1 -m debug -a 'var=app_port' -i inventory ``

Gotcha: Role defaults are often overridden unintentionally. Always check with --check and --diff.

Variable Precedence Pitfall
Extra-vars override everything, including role vars. If you use -e for debugging, ensure you don't accidentally override production values. Use --extra-vars @file.json to keep overrides trackable.
Production Insight
In a recent incident, a team member used -e 'app_port=8080' to test, but the playbook had a role var set to 9090. The extra-vars won, but they forgot to remove it, causing a production deployment to use the wrong port. The fix was to audit ansible-playbook --syntax-check output and enforce no extra-vars in CI/CD.
Key Takeaway
Variable precedence is a ladder; know where each type stands. Use inventory for environment config, role defaults for fallbacks, and extra-vars sparingly.

2. Inventory Variables: host_vars and group_vars

Inventory variables are defined in the inventory directory structure. Common patterns: - group_vars/all.yml: variables for all hosts. - group_vars/webservers.yml: variables for the webservers group. - host_vars/web1.yml: variables specific to host web1.

Directory structure: `` inventory/ production/ hosts.yml group_vars/ all.yml webservers.yml host_vars/ web1.yml ``

Precedence within inventory: host_vars > group_vars (child groups can inherit from parent groups, but more specific group wins).

Production tip: Use ansible-inventory --list --yaml to dump all inventory variables for debugging. Example: ``bash ansible-inventory -i inventory/production --list --yaml ``

Code example: ``yaml # group_vars/webservers.yml http_port: 80 server_name: example.com ` `yaml # host_vars/web1.yml http_port: 8080 # overrides group_vars ``

Gotcha: If you use dynamic inventory scripts, ensure they output variables correctly. Test with ansible-inventory before running playbooks.

Dynamic Inventory Variables
Cloud inventory plugins (e.g., aws_ec2) can tag hosts with variables. Use ansible-inventory --graph to visualize host groupings.
Production Insight
We once had a production outage because a host_vars file had a typo: http_port: 8000 instead of 80. The playbook used http_port to configure nginx, and the server listened on port 8000, causing a mismatch with the load balancer. We now run ansible-inventory --list --yaml | grep http_port as a pre-deployment check.
Key Takeaway
Inventory variables are the foundation; use host_vars for exceptions, group_vars for common config. Always validate with ansible-inventory.

3. Playbook Variables: vars, vars_files, and vars_prompt

Playbook variables are defined directly in the playbook file. They have higher precedence than inventory but lower than role vars.

vars block: ``yaml - hosts: all vars: package: nginx state: present tasks: - name: Install package apt: name: "{{ package }}" state: "{{ state }}" ``

vars_files: Load variables from external files. ``yaml - hosts: all vars_files: - vars/common.yml - "vars/{{ ansible_os_family }}.yml" ``

vars_prompt: Interactive input (avoid in automation). ``yaml - hosts: all vars_prompt: - name: "db_password" prompt: "Enter database password" private: yes ``

Production pattern: Use vars_files to separate sensitive data (encrypted with ansible-vault) from playbook logic. Example: ``bash ansible-vault create vars/secrets.yml ` Then in playbook: `yaml vars_files: - vars/secrets.yml ``

Gotcha: vars_prompt is not idempotent and breaks automation. Never use it in CI/CD pipelines.

Debug: Print all playbook variables with: ``yaml - debug: var: vars ``

vars_prompt in Automation
vars_prompt requires interactive TTY. In Jenkins or Ansible Tower, it will hang indefinitely. Use ansible-vault or environment variables instead.
Production Insight
We used vars_prompt for DB passwords in early automation. When we migrated to Jenkins, the playbook hung for hours. We replaced it with vault-encrypted files and --ask-vault-pass in the job configuration.
Key Takeaway
Use vars_files for external config, avoid vars_prompt in automation. Encrypt sensitive files with ansible-vault.

4. Role Variables: defaults vs. vars

Roles can define variables in two locations: defaults/main.yml (lowest precedence) and vars/main.yml (higher precedence). The difference is crucial for role reusability.

Role defaults (defaults/main.yml)
  • Lowest precedence; easily overridden by playbook or inventory.
  • Should contain sane defaults for the role.
  • Example: ``yaml # roles/nginx/defaults/main.yml nginx_port: 80 nginx_root: /var/www/html ``
Role vars (vars/main.yml)
  • Higher precedence; cannot be overridden by inventory or playbook vars (only by extra-vars or set_fact).
  • Should contain internal constants that should not change.
  • Example: ``yaml # roles/nginx/vars/main.yml nginx_service: nginx nginx_user: www-data ``

Precedence order: role defaults < inventory < playbook vars < role vars < set_fact < extra-vars.

Production pattern: Use defaults/main.yml for configurable settings, vars/main.yml for internal constants. Document which variables are overridable.

Code example: ``yaml # playbook.yml - hosts: all vars: nginx_port: 8080 # overrides role defaults, but not role vars roles: - nginx ``

Gotcha: If you define a variable in both defaults and vars with the same name, vars wins. Avoid duplication.

Role Variable Documentation
Use meta/argument_specs.yml (Ansible 2.11+) to define role variable types and defaults. This enables validation and auto-documentation.
Production Insight
A junior engineer put a critical API key in defaults/main.yml thinking it would be overridden by group_vars. But group_vars had lower precedence than role vars, not defaults. The API key was exposed. We now enforce that secrets go into vars/main.yml and are vault-encrypted.
Key Takeaway
Use defaults for overridable settings, vars for fixed internals. Never put secrets in defaults.

5. Extra-Vars: The Emergency Override

Extra-vars (-e or --extra-vars) have the highest precedence of all variable sources. They can be passed as key=value pairs, JSON, or YAML files.

Syntax: ``bash ansible-playbook playbook.yml -e 'myvar=value' ansible-playbook playbook.yml -e '@vars.json' ansible-playbook playbook.yml -e "{'myvar':'value'}" ``

Use cases
  • Override a variable temporarily for testing.
  • Inject environment-specific values in CI/CD (e.g., build number, commit SHA).
  • Emergency config changes without modifying files.

Production caution: Extra-vars can override anything, including role vars. They bypass normal precedence. Use them sparingly and audit with --syntax-check.

Code example: ``bash # Override app version in deployment ansible-playbook deploy.yml -e 'app_version=v2.1.3' -i production ``

Debug: To see all extra-vars: ``bash ansible-playbook playbook.yml -e 'debug_extra=true' --syntax-check ` Add a task: `yaml - debug: var: extra_vars | default({}) ``

Gotcha: Extra-vars from files are merged; if the same key exists in multiple files, the last one wins. Order matters: -e @a.yml -e @b.yml — b overrides a.

Extra-Vars in CI/CD
Never use plaintext extra-vars for secrets in CI/CD logs. Use --extra-vars @secrets.yml with vault encryption or CI/CD secret injection.
Production Insight
We had a CI/CD pipeline that passed -e 'deploy_env=staging'. One day a developer accidentally passed deploy_env=prod and the playbook deployed to production. We now use a whitelist of allowed extra-vars in the pipeline script.
Key Takeaway
Extra-vars are powerful but dangerous. Use them only for temporary overrides and validate with --syntax-check.

6. The register Keyword: Capturing Task Output

The register keyword captures the output of a task into a variable. It stores a dictionary with keys: changed, failed, rc, stdout, stderr, etc.

Basic usage: ```yaml - name: Check if file exists stat: path: /etc/app.conf register: file_stat

  • name: Print file existence
  • debug:
  • msg: "File exists: {{ file_stat.stat.exists }}"
  • ```
Accessing output
  • result.stdout: standard output (string).
  • result.stdout_lines: list of lines.
  • result.rc: return code (0 for success).
  • result.stderr: error output.

Common patterns: ```yaml - name: Run a command command: /usr/bin/uptime register: uptime_output

  • name: Show uptime
  • debug:
  • var: uptime_output.stdout
  • ```

Conditional execution based on register: ``yaml - name: Restart service if config changed service: name: nginx state: restarted when: config_update.changed ``

Gotcha: register only captures the task's result; if the task is skipped, the variable is undefined. Use | default({}) to avoid errors.

Production pattern: Always check result.failed or result.rc when using command or shell modules.

Register and Idempotency
register does not affect idempotency by itself. It just stores output. Use changed_when and failed_when to control task status.
Production Insight
We used register to capture the output of a database migration script. When the script failed with non-zero exit code, result.rc was 1, but the task didn't fail because we forgot failed_when: result.rc != 0. The play continued and deployed a broken schema. Now we always set failed_when on critical commands.
Key Takeaway
Use register to capture task output, but always check rc or failed for error handling.

7. The set_fact Module: Creating Dynamic Variables

set_fact creates or updates variables during playbook execution. Unlike register, it can assign arbitrary values, not just task results.

Basic usage: ``yaml - name: Set a fact set_fact: my_custom_var: "{{ some_other_var | upper }}" ``

Use cases
  • Combine multiple register variables into one.
  • Transform data (e.g., parse JSON, concatenate strings).
  • Cache expensive lookups.

Example: ```yaml - name: Get instance ID uri: url: http://169.254.169.254/latest/meta-data/instance-id return_content: yes register: instance_id_result

  • name: Set instance ID fact
  • set_fact:
  • instance_id: "{{ instance_id_result.content }}"
  • ```

Cross-host facts: set_fact creates a host-level variable. To share across hosts, use hostvars.

Gotcha: set_fact variables are only available for the current host and current playbook run. They are not persistent.

Production pattern: Use set_fact to compute derived values early in the playbook to avoid repeated lookups.

set_fact and Caching
set_fact variables are not cached across runs. Use fact caching (e.g., jsonfile) if you need persistence.
Production Insight
We used set_fact to compute a deployment timestamp: set_fact: deploy_ts="{{ ansible_date_time.epoch }}". This was used in log file names. One day the timestamp was reused across multiple runs because we didn't realize set_fact runs once per host per play. We added run_once: true to avoid duplication.
Key Takeaway
Use set_fact for dynamic variables within a run. Remember they are per-host and ephemeral.

8. The hostvars Magic Variable: Cross-Host Communication

hostvars is a dictionary containing all variables for all hosts in the inventory. It allows a play to access facts or variables from other hosts.

Syntax: ``yaml {{ hostvars['target_host']['variable_name'] }} ``

Common use case: Get IP address of a database server from an application server play. ``yaml - name: Print database IP debug: msg: "DB IP is {{ hostvars['db1']['ansible_default_ipv4']['address'] }}" ``

Limitations
  • The target host must be in the current play's inventory.
  • Variables from that host are only available if the host has been reached in the same playbook run (or facts cached).

Production pattern: Use hostvars to build dynamic inventories or configuration files.

Code example: ``yaml - name: Generate haproxy config template: src: haproxy.cfg.j2 dest: /etc/haproxy/haproxy.cfg vars: backend_servers: "{{ groups['webservers'] | map('extract', hostvars, ['ansible_default_ipv4', 'address']) | list }}" ``

Gotcha: If the target host hasn't been processed yet (e.g., different play), hostvars will be empty. Use serial or order to control execution order.

hostvars Availability
hostvars for a host are only populated after that host has been visited in a play. Use gather_facts: true on all hosts early in the playbook to ensure facts are available.
Production Insight
We tried to use hostvars['db1']['ansible_fqdn'] in a play that targeted only application servers. The variable was undefined because db1 hadn't been reached yet. We added a separate play at the beginning to gather facts from all hosts: - hosts: all, gather_facts: true, tasks: [].
Key Takeaway
hostvars enables cross-host data access, but ensure the target host is processed first. Use a fact-gathering play for all hosts.

9. Gathered Facts (ansible_facts): System Introspection

Facts are system information automatically gathered by Ansible when gather_facts: true (default). They are stored in the ansible_facts dictionary and can be accessed directly as variables (e.g., ansible_os_family, ansible_default_ipv4.address).

Common facts
  • ansible_os_family: Debian, RedHat, etc.
  • ansible_distribution: Ubuntu, CentOS, etc.
  • ansible_distribution_version: 20.04, 7.9, etc.
  • ansible_default_ipv4.address: Primary IPv4 address.
  • ansible_memtotal_mb: Total memory in MB.
  • ansible_processor_cores: Number of CPU cores.

Accessing facts: ``yaml - debug: msg: "OS is {{ ansible_os_family }} {{ ansible_distribution_version }}" ``

Custom facts: Place scripts or static files in /etc/ansible/facts.d/ on target hosts. They are gathered as ansible_local.

Example custom fact: ``bash # /etc/ansible/facts.d/role.fact [general] role = webserver datacenter = us-east ` Accessed as {{ ansible_local['role']['general']['role'] }}`.

Gotcha: Fact gathering adds ~10 seconds per host. For large environments, this is significant.

Fact Caching
Use fact_caching=jsonfile and fact_caching_timeout to cache facts across runs. Set gather_facts: false and use setup: module with filter: for selective gathering.
Production Insight
We managed 500 servers. Fact gathering added 5 minutes per playbook run. We implemented fact caching with fact_caching=jsonfile and fact_caching_timeout=86400, reducing runtime to 30 seconds. We also use gather_facts: false in most plays and call setup: only when needed.
Key Takeaway
Facts are powerful but expensive. Cache them or disable gathering when not needed.

10. gather_facts: false: Performance vs. Convenience

Setting `gather_facts: false at the play level skips the setup` module invocation, significantly reducing playbook runtime. However, you lose access to system facts.

When to disable
  • You only need custom variables, not system info.
  • You have cached facts from an earlier run.
  • You are running simple tasks (e.g., file copy, package install) that don't depend on OS.

Syntax: ``yaml - hosts: all gather_facts: false tasks: - name: Install package apt: name: nginx state: present ``

Performance gain: On a 100-host environment, disabling facts can cut runtime from 10 minutes to 2 minutes.

Trade-off: You cannot use ansible_os_family or other facts. If you need a specific fact, use the setup module selectively: ``yaml - name: Gather minimal facts setup: filter: ansible_os_family when: ansible_os_family is not defined ``

Production pattern: Use gather_facts: false in most plays, and explicitly call setup: with filter: for required facts. Cache facts with fact_caching.

Gotcha: If you disable facts and then try to use a fact variable, you'll get an undefined variable error. Use | default('') or conditional checks.

Facts Disabled but Required
If your role depends on facts (e.g., ansible_os_family), the playbook will fail with undefined variable errors. Always test with --check after disabling facts.
Production Insight
We disabled gather_facts to speed up a deployment playbook, but a role used ansible_distribution to choose package manager. The playbook failed silently because the variable was undefined. We added a conditional setup: task with filter: ansible_distribution before the role.
Key Takeaway
Disable facts for speed, but ensure no tasks depend on them. Use selective setup: calls to fetch only needed facts.

11. The debug Module: Inspecting Variables in Flight

The debug module is your best friend for variable inspection. It can print messages, variable values, or verbosity-dependent output.

Common uses
  • debug: var=variable_name prints the variable's value.
  • debug: msg="The value is {{ variable }}" prints a formatted message.
  • debug: var=hostvars[inventory_hostname] dumps all variables for the current host.

Verbosity control: ``yaml - debug: msg: "This only shows with -v" verbosity: 1 ``

Production pattern: Add debug tasks with verbosity: 2 to avoid cluttering normal output. Use --verbose or -v to see them.

Code example: ```yaml - name: Print all facts debug: var: ansible_facts verbosity: 2

  • name: Print specific variable
  • debug:
  • var: my_custom_var
  • ```

Gotcha: debug: var=myvar prints the variable name and value. If the variable is undefined, it prints the string "VARIABLE IS NOT DEFINED!".

Debugging with Tags
Add tags: [debug] to debug tasks. Run with ansible-playbook --tags debug to see only debug output.
Production Insight
During a complex multi-role playbook, we added debug: var=hostvars with verbosity: 2 at each stage. This helped us trace how variables changed across roles. We later removed them, but they were invaluable for debugging a variable override issue.
Key Takeaway
Use debug module liberally during development, with verbosity to control output. Remove or comment out in production playbooks.

12. Putting It All Together: A Production Debugging Workflow

When a variable-related issue arises in production, follow this systematic workflow:

  1. Reproduce with verbosity: Run the playbook with -vvv to see variable resolution.
  2. ```bash
  3. ansible-playbook playbook.yml -i inventory -vvv | grep 'variable'
  4. ```
  5. Dump all variables: Use a debug task to dump hostvars.
  6. ```yaml
  7. - name: Dump host variables
  8. debug:
  9. var: hostvars[inventory_hostname]
  10. tags: [never, debug]
  11. ```
  12. Check precedence: Use ansible-inventory to see inventory variables.
  13. ```bash
  14. ansible-inventory -i inventory --list --yaml
  15. ```
  16. Test with extra-vars: Override the suspect variable to see if it changes behavior.
  17. ```bash
  18. ansible-playbook playbook.yml -e 'suspect_var=test_value'
  19. ```
  20. Use --syntax-check: Validates variable references.
  21. ```bash
  22. ansible-playbook playbook.yml --syntax-check
  23. ```
  24. Check fact cache: If using caching, clear it.
  25. ```bash
  26. ansible-playbook playbook.yml --flush-cache
  27. ```
  28. Isolate the issue: Create a minimal playbook that only prints the variable.
  29. ```yaml
  30. - hosts: target_host
  31. gather_facts: false
  32. tasks:
  33. - debug:
  34. var: suspect_var
  35. ```

Real incident: We had a variable app_version that was undefined in production. Following the workflow, we discovered that the fact cache was stale (contained an old version). Flushing the cache and re-gathering facts resolved it.

Automated Variable Validation
Use ansible-lint with rules like var-naming and no-same-owner to catch variable issues early. Integrate into CI pipeline.
Production Insight
We now include a pre-flight task in all playbooks that validates required variables are defined:
```yaml
- name: Validate required variables
fail:
msg: "Variable {{ item }} is not defined"
when: item not in vars
loop:
- app_version
- deploy_env
```
Key Takeaway
A systematic debugging workflow saves hours. Automate variable validation in your playbooks.
● Production incidentPOST-MORTEMseverity: high

The Silent Override: Role Defaults vs. Inventory

Symptom
Playbook ran without errors, but the application configuration was wrong on some hosts. The variable app_port was expected to be 8080 but was 9090 on affected hosts.
Assumption
The engineer assumed inventory variables take precedence over role defaults (they do, but only if the role variable is defined in vars/main.yml, not defaults/main.yml).
Root cause
The role defined app_port: 9090 in defaults/main.yml. Inventory defined app_port: 8080 in group_vars/all. Role defaults have lower precedence than inventory group_vars, so inventory should win. However, the playbook also included a vars: block that set app_port: 9090 — playbook vars have higher precedence than inventory. The engineer forgot they had added that playbook var.
Fix
Removed the redundant app_port from the playbook vars block. The inventory value then took effect. Verified with ansible-inventory --list --yaml and ansible-playbook --check.
Key lesson
  • Always audit all variable definition locations.
  • Use debug module to print variable values during playbook runs.
  • Maintain a variable precedence cheat sheet near your desk.
Production debug guideSymptom → Root cause → Fix4 entries
Symptom · 01
Variable 'app_port' has unexpected value 9090 instead of 8080
Fix
Check variable precedence: inventory vars (lowest), playbook vars, role defaults, role vars, set_fact/register, extra-vars (highest). Use ansible-playbook -e 'app_port=debug' --syntax-check to see if extra-vars override. Use debug: var=app_port in a task to print resolved value.
Symptom · 02
Playbook takes 2 minutes per host due to fact gathering
Fix
Set gather_facts: false at play level if you don't need system facts. Use setup: module explicitly only when needed. Consider fact caching: fact_caching=jsonfile with fact_caching_timeout=3600.
Symptom · 03
register variable appears empty or undefined
Fix
Check the task's output: register: result captures stdout, stderr, rc, etc. Access with result.stdout, not result. Ensure the task actually runs (not skipped). Use debug: var=result to inspect.
Symptom · 04
hostvars returns undefined for another host
Fix
Ensure the other host is in the inventory and has been reached in the play. Use hostvars['hostname'] — hostname must match inventory name exactly. Use debug: var=hostvars to see all available hosts.
★ Ansible Variables and Facts Quick Referenceprint this for your desk
Variable value unexpected
Immediate action
Check all definition locations
Commands
ansible-inventory --list --yaml
ansible-playbook --syntax-check -e 'var=override'
Fix now
Use debug: var=myvar in playbook
Playbook too slow due to facts+
Immediate action
Add gather_facts: false
Commands
time ansible-playbook playbook.yml
time ansible-playbook playbook.yml -e 'ansible_facts={}'
Fix now
Set gather_facts: false at play level
register variable empty+
Immediate action
Inspect task output
Commands
ansible-playbook playbook.yml -v
ansible -m debug -a 'var=result'
Fix now
Access with result.stdout not result
hostvars undefined for host+
Immediate action
Verify host in inventory
Commands
ansible-inventory --list --yaml | grep hostname
ansible hostname -m debug -a 'var=hostvars[inventory_hostname]'
Fix now
Ensure host is in inventory and play includes it
Facts stale or missing+
Immediate action
Clear fact cache
Commands
rm -rf /tmp/ansible_fact_cache/*
ansible-playbook playbook.yml --flush-cache
Fix now
Set fact_caching_timeout: 0 to disable cache
Variable Precedence Reference Table
SourcePrecedence LevelScopeExample Definition
Role defaults1 (lowest)Roleroles/role/defaults/main.yml
Inventory group_vars2Groupgroup_vars/all.yml
Inventory host_vars3Hosthost_vars/web1.yml
Playbook vars4Playvars: block in playbook
Playbook vars_files5Playvars_files: [file.yml]
Role vars6Roleroles/role/vars/main.yml
Block vars7Blockvars: within block
Task vars8Taskvars: within task
set_fact9Host (dynamic)set_fact: myvar=value
register10Host (dynamic)register: myvar
Extra-vars (CLI)11 (highest)Global-e 'myvar=value'

Key takeaways

1
Variable precedence is critical
role defaults < inventory < playbook vars < role vars < set_fact/register < extra-vars.
2
Use ansible-inventory --list --yaml to dump all inventory variables.
3
Disable gather_facts for performance, but ensure no tasks depend on facts.
4
Use register to capture task output and set_fact for computed variables.
5
hostvars enables cross-host data access; ensure target hosts are processed first.
6
Use debug module with verbosity to inspect variables without cluttering output.
7
Extra-vars override everything; use sparingly and validate in CI/CD.
8
Automate variable validation with fail tasks to catch undefined variables early.

Common mistakes to avoid

6 patterns
×

Assuming inventory host_vars override role vars

Symptom
Variable value from role vars used instead of inventory
Fix
Understand precedence: role vars > inventory host_vars. Use role defaults for overridable values.
×

Using extra-vars in CI/CD without validation

Symptom
Accidental override of critical variables
Fix
Whitelist allowed extra-vars in pipeline script. Use vault for secrets.
×

Forgetting to access register variable correctly

Symptom
Error: 'result' is undefined or not a dictionary
Fix
Use result.stdout, result.rc, etc. Check if task was skipped.
×

Assuming hostvars are available for all hosts at start

Symptom
Undefined variable when accessing another host's facts
Fix
Add a fact-gathering play for all hosts before the main play.
×

Disabling gather_facts without checking dependencies

Symptom
Undefined ansible_* variables
Fix
Use setup: with filter: for required facts, or cache facts.
×

Not using verbosity with debug module

Symptom
Debug output floods normal playbook output
Fix
Set verbosity: 2 on debug tasks; run with -v only when needed.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the variable precedence order in Ansible?
Q02JUNIOR
How do you access the stdout of a registered variable?
Q03SENIOR
What is the difference between role defaults and role vars?
Q04SENIOR
How can you access a variable from another host?
Q05SENIOR
What is the performance impact of gather_facts: true?
Q06JUNIOR
How do you debug a variable that has an unexpected value?
Q07SENIOR
What is the difference between set_fact and register?
Q08SENIOR
How do you make a set_fact variable available across hosts?
Q01 of 08JUNIOR

What is the variable precedence order in Ansible?

ANSWER
From lowest to highest: role defaults, inventory group_vars, inventory host_vars, playbook vars, playbook vars_files, role vars, block vars, task vars, set_fact/register, extra-vars. Extra-vars always win.
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
What is the difference between ansible_facts and hostvars?
02
Can I use extra-vars to override role vars?
03
How do I see all variables for a host?
04
What is the best practice for storing secrets in Ansible?
05
How do I conditionally run a task based on a register variable?
06
What is fact caching and how do I enable it?
07
How do I create a custom fact?
08
Why is my set_fact variable not available in the next play?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
June 21, 2026
last updated
1,596
articles · all by Naren
🔥

That's Ansible. Mark it forged?

10 min read · try the examples if you haven't

Previous
Ansible Inventory Management
5 / 23 · Ansible
Next
Ansible Conditionals and Loops