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..
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Solid grasp of DevOps fundamentals
- ✓Comfortable with command-line tools
- ✓Basic Linux administration knowledge
Ansible variables store dynamic values (e.g., hostnames, paths) and can be defined in inventory, playbooks, roles, or facts. Facts are system-level variables gathered automatically by the setup module. The key takeaway: always use | default() to avoid undefined variable errors, and cache facts with gather_facts: no when not needed to speed up playbooks.
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.
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.
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.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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.
--- - name: Demonstrate variable types hosts: all vars: playbook_var: "defined in play" tasks: - name: Show variable precedence ansible.builtin.debug: msg: "{{ playbook_var }}"
-e for debugging, ensure you don't accidentally override production values. Use --extra-vars @file.json to keep overrides trackable.-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.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.
[web] web1 ansible_host=192.168.1.10 web2 ansible_host=192.168.1.11 [web:vars] http_port=80 # host_vars/web1.yml # --- # ansible_host: 192.168.1.10 # http_port: 8080
ansible-inventory --graph to visualize host groupings.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.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 ``
--- - name: Playbook variables demo hosts: localhost connection: local vars: greeting: "Hello from vars" vars_files: - vars/external.yml vars_prompt: - name: user_name prompt: "Enter your name" private: false tasks: - name: Show variables ansible.builtin.debug: msg: "{{ greeting }} {{ user_name }} {{ external_var }}"
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.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.
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``
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.
--- # defaults/main.yml role_default_var: "default value" role_override_var: "default value"
meta/argument_specs.yml (Ansible 2.11+) to define role variable types and defaults. This enables validation and auto-documentation.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.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'}" ``
- 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.
ansible-playbook site.yml --extra-vars "my_var=override_value"--extra-vars @secrets.yml with vault encryption or CI/CD secret injection.-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.--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 }}"
- ```
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.
--- - name: Register task output hosts: localhost connection: local tasks: - name: Run shell command ansible.builtin.shell: echo "Hello from shell" register: shell_result - name: Show registered variable ansible.builtin.debug: var: shell_result.stdout
changed_when and failed_when to control task status.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.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 }}" ``
- 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.
--- - name: Set fact dynamically hosts: localhost connection: local tasks: - name: Calculate sum ansible.builtin.set_fact: sum_result: "{{ 3 + 4 }}" - name: Show dynamic variable ansible.builtin.debug: var: sum_result
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.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'] }}" ``
- 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.
--- - name: Cross-host communication hosts: all tasks: - name: Show IP of other host ansible.builtin.debug: msg: "{{ hostvars['web1']['ansible_default_ipv4']['address'] }}"
gather_facts: true on all hosts early in the playbook to ensure facts are available.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: [].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).
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.
--- - name: Gather and use facts hosts: all tasks: - name: Show OS family ansible.builtin.debug: msg: "{{ ansible_facts['os_family'] }}"
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.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.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.
- 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.
--- - name: Skip fact gathering hosts: all gather_facts: false tasks: - name: This task does not use facts ansible.builtin.debug: msg: "No facts gathered"
ansible_os_family), the playbook will fail with undefined variable errors. Always test with --check after disabling facts.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.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.
debug: var=variable_nameprints 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!".
--- - name: Debug variable inspection hosts: localhost connection: local vars: my_var: "test value" tasks: - name: Print variable ansible.builtin.debug: var: my_var - name: Print with message ansible.builtin.debug: msg: "The value is {{ my_var }}"
tags: [debug] to debug tasks. Run with ansible-playbook --tags debug to see only debug output.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.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:
- Reproduce with verbosity: Run the playbook with
-vvvto see variable resolution. - ```bash
- ansible-playbook playbook.yml -i inventory -vvv | grep 'variable'
- ```
- Dump all variables: Use a debug task to dump hostvars.
- ```yaml
- - name: Dump host variables
- debug:
- var: hostvars[inventory_hostname]
- tags: [never, debug]
- ```
- Check precedence: Use
ansible-inventoryto see inventory variables. - ```bash
- ansible-inventory -i inventory --list --yaml
- ```
- Test with extra-vars: Override the suspect variable to see if it changes behavior.
- ```bash
- ansible-playbook playbook.yml -e 'suspect_var=test_value'
- ```
- Use
--syntax-check: Validates variable references. - ```bash
- ansible-playbook playbook.yml --syntax-check
- ```
- Check fact cache: If using caching, clear it.
- ```bash
- ansible-playbook playbook.yml --flush-cache
- ```
- Isolate the issue: Create a minimal playbook that only prints the variable.
- ```yaml
- - hosts: target_host
- gather_facts: false
- tasks:
- - debug:
- var: suspect_var
- ```
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.
--- - name: Production debugging workflow hosts: all gather_facts: false vars: expected_version: "2.0" tasks: - name: Check installed version ansible.builtin.shell: cat /etc/myapp/version register: version_output ignore_errors: true - name: Debug version ansible.builtin.debug: msg: "Expected {{ expected_version }}, got {{ version_output.stdout | default('unknown') }}" - name: Assert version ansible.builtin.assert: that: - version_output.stdout == expected_version fail_msg: "Version mismatch!"
ansible-lint with rules like var-naming and no-same-owner to catch variable issues early. Integrate into CI pipeline.The Silent Override: Role Defaults vs. Inventory
app_port was expected to be 8080 but was 9090 on affected hosts.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.app_port from the playbook vars block. The inventory value then took effect. Verified with ansible-inventory --list --yaml and ansible-playbook --check.- Always audit all variable definition locations.
- Use
debugmodule to print variable values during playbook runs. - Maintain a variable precedence cheat sheet near your desk.
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.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.register variable appears empty or undefinedregister: 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.hostvars returns undefined for another hosthostvars['hostname'] — hostname must match inventory name exactly. Use debug: var=hostvars to see all available hosts.ansible-inventory --list --yamlansible-playbook --syntax-check -e 'var=override'debug: var=myvar in playbooktime ansible-playbook playbook.ymltime ansible-playbook playbook.yml -e 'ansible_facts={}'gather_facts: false at play levelansible-playbook playbook.yml -vansible -m debug -a 'var=result'result.stdout not resultansible-inventory --list --yaml | grep hostnameansible hostname -m debug -a 'var=hostvars[inventory_hostname]'rm -rf /tmp/ansible_fact_cache/*ansible-playbook playbook.yml --flush-cachefact_caching_timeout: 0 to disable cache| Source | Precedence Level | Scope | Example Definition |
|---|---|---|---|
| Role defaults | 1 (lowest) | Role | roles/role/defaults/main.yml |
| Inventory group_vars | 2 | Group | group_vars/all.yml |
| Inventory host_vars | 3 | Host | host_vars/web1.yml |
| Playbook vars | 4 | Play | vars: block in playbook |
| Playbook vars_files | 5 | Play | vars_files: [file.yml] |
| Role vars | 6 | Role | roles/role/vars/main.yml |
| Block vars | 7 | Block | vars: within block |
| Task vars | 8 | Task | vars: within task |
| set_fact | 9 | Host (dynamic) | set_fact: myvar=value |
| register | 10 | Host (dynamic) | register: myvar |
| Extra-vars (CLI) | 11 (highest) | Global | -e 'myvar=value' |
| File | Command / Code | Purpose |
|---|---|---|
| site.yml | - name: Demonstrate variable types | 1. Variable Types |
| inventory | [web] | 2. Inventory Variables |
| playbook.yml | - name: Playbook variables demo | 3. Playbook Variables |
| roles | role_default_var: "default value" | 4. Role Variables |
| playbook.yml | ansible-playbook site.yml --extra-vars "my_var=override_value" | 5. Extra-Vars |
| register.yml | - name: Register task output | 6. The register Keyword |
| set_fact.yml | - name: Set fact dynamically | 7. The set_fact Module |
| hostvars.yml | - name: Cross-host communication | 8. The hostvars Magic Variable |
| facts.yml | - name: Gather and use facts | 9. Gathered Facts (ansible_facts) |
| no_facts.yml | - name: Skip fact gathering | 10. gather_facts |
| debug.yml | - name: Debug variable inspection | 11. The debug Module |
| debug_workflow.yml | - name: Production debugging workflow | 12. Putting It All Together |
Key takeaways
ansible-inventory --list --yaml to dump all inventory variables.Interview Questions on This Topic
What is the variable precedence order in Ansible?
How do you access the stdout of a registered variable?
result.stdout where result is the variable name from register: result. You can also use result.stdout_lines for a list of lines.What is the difference between role defaults and role vars?
How can you access a variable from another host?
hostvars magic variable: hostvars['target_host']['variable_name']. The target host must be in the inventory and have been processed in the playbook.What is the performance impact of gather_facts: true?
How do you debug a variable that has an unexpected value?
debug: var=variable_name. Also run with -vvv to see variable resolution. Use ansible-inventory to dump inventory variables.What is the difference between set_fact and register?
How do you make a set_fact variable available across hosts?
hostvars or delegate facts to a central host. Alternatively, use a shared variable via a file or database.Frequently Asked Questions
ansible_facts contains facts for the current host. hostvars is a dictionary of all variables (including facts) for all hosts in the inventory.
Yes, extra-vars have the highest precedence and can override any variable including role vars.
Use ansible hostname -m debug -a 'var=hostvars[inventory_hostname]' or add a debug task: debug: var=hostvars[inventory_hostname].
Use ansible-vault to encrypt variable files. Store encrypted files in vars/ and reference them with vars_files. Never use vars_prompt in automation.
Use `when: variable_name.changed or when: variable_name.rc != 0. For example: when: result.changed`.
Fact caching stores gathered facts to disk or redis across runs. Enable in ansible.cfg: fact_caching=jsonfile, fact_caching_connection=/tmp/ansible_fact_cache, fact_caching_timeout=86400.
Place a script or .fact file in /etc/ansible/facts.d/ on the target host. The script must output INI or JSON format. Access via ansible_local['fact_name'].
set_fact variables are only available for the current host and current playbook run. They are not persisted between runs. Use fact caching or extra-vars for persistence.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Ansible. Mark it forged?
8 min read · try the examples if you haven't