# Ansible Variables > Variable precedence, inheritance, and scoping — the part everyone gets wrong. ## Precedence (lowest to highest) Ansible merges variables from many sources. **Higher number wins.** ``` 1. command line values (for constants, not variables) 2. role defaults (roles/x/defaults/main.yml) 3. inventory file or script group vars 4. inventory group_vars/all 5. playbook group_vars/all 6. inventory group_vars/* 7. playbook group_vars/* 8. inventory file or script host vars 9. inventory host_vars/* 10. playbook host_vars/* 11. host facts / cached set_facts 12. play vars (vars: in play) 13. play vars_prompt 14. play vars_files 15. role vars (roles/x/vars/main.yml) 16. block vars (vars: on block) 17. task vars (vars: on task) 18. include_vars 19. set_facts / registered vars 20. role params (role: {role: x, param: val}) 21. include params 22. extra vars (-e / --extra-vars) *** always wins *** ``` ### Practical Summary | Source | Precedence | Use for | |-------------------------|:----------:|--------------------------------| | Role defaults | Lowest | Sane defaults, meant to be overridden | | `group_vars/all` | Low | Organization-wide defaults | | `group_vars/` | Medium | Environment/group-specific | | `host_vars/` | Medium+ | Host-specific overrides | | Play/block/task `vars:` | High | Playbook-scoped values | | Role vars | High | Role internals, not for override | | `set_fact` | High | Runtime-computed values | | Extra vars (`-e`) | Highest | CLI overrides, CI/CD pipelines | ## Group Variable Inheritance When a host belongs to multiple groups, merge order is: 1. **`all` group** (lowest) 2. **Parent groups** (alphabetical within same level) 3. **Child groups** (override parents) 4. **Host vars** (override all groups) ### Controlling Merge Order ```yaml # inventory/hosts.yml all: children: env_staging: vars: app_env: staging log_level: debug env_production: vars: app_env: production log_level: warn webservers: hosts: web1.example.com: # member of webservers AND env_production vars: http_port: 8080 ``` Use `ansible_group_priority` to force ordering (default: 1): ```yaml # group_vars/env_production.yml ansible_group_priority: 10 # higher = wins over other groups app_env: production ``` ## Variable Scoping | Scope | Defined in | Visible to | |----------|-----------------------------------|---------------------------| | Global | `extra_vars`, config, env | Everything | | Play | `vars:`, `vars_files:`, `include_vars` | Current play | | Host | Inventory, facts, `set_fact` | That host across plays | | Role | `defaults/`, `vars/` | Role + dependents | | Task | `register`, task-level `vars:` | Subsequent tasks (same host) | ## Common Patterns ### Layered Defaults ```yaml # group_vars/all.yml — base defaults ntp_server: ntp.pool.org log_level: info app_port: 8080 # group_vars/production.yml — override for prod log_level: warn # host_vars/web1.example.com.yml — host-specific app_port: 9090 ``` Result for `web1` in `production`: `ntp_server=ntp.pool.org`, `log_level=warn`, `app_port=9090`. ### Role Defaults vs Role Vars ```yaml # roles/nginx/defaults/main.yml — LOW precedence, user overrides these nginx_worker_processes: auto nginx_listen_port: 80 # roles/nginx/vars/main.yml — HIGH precedence, internal to role nginx_conf_path: /etc/nginx/nginx.conf # don't let users change this nginx_user: www-data ``` **Rule of thumb:** put everything in `defaults/` unless the role breaks if the value changes. ### Merging Dictionaries and Lists By default, Ansible **replaces** (does not merge) dicts and lists. To merge: ```ini # ansible.cfg [defaults] hash_behaviour = merge # CAUTION: global, can cause surprises ``` Prefer explicit merging with `combine` filter: ```yaml - name: Merge defaults with overrides set_fact: final_config: "{{ default_config | combine(override_config, recursive=True) }}" ``` ### Registered Variables ```yaml - name: Check service status command: systemctl is-active nginx register: nginx_status ignore_errors: true - name: Restart if stopped service: name: nginx state: started when: nginx_status.rc != 0 ``` ### Vault-Encrypted Variables ```yaml # group_vars/all/vault.yml (encrypted) vault_db_password: "s3cret" # group_vars/all/main.yml (references vault) db_password: "{{ vault_db_password }}" ``` Prefix vault variables with `vault_` so it's clear where values originate. ## Debugging Variables ```bash # Show all vars for a host ansible -m debug -a "var=hostvars[inventory_hostname]" web1.example.com # Show specific variable ansible -m debug -a "var=http_port" web1.example.com # In a playbook - debug: var=ansible_facts - debug: msg="{{ http_port }} on {{ inventory_hostname }}" ``` ## Gotchas - `extra_vars` always win — they override even role `vars/`. Don't use `-e` for defaults - `set_fact` persists for the host across plays in the same run (not across runs unless cached) - `hash_behaviour = merge` is global and rarely what you want — prefer `combine` filter - Variable names are flat — `foo.bar` is dict access, not a separate variable - Jinja2 variables in `when:` don't need `{{ }}` — just `when: my_var == "x"` - Undefined variables fail by default. Use `{{ my_var | default('fallback') }}` - `group_vars/` at playbook level AND inventory level both load — watch for conflicts ## See Also - `ansible` — core commands and ansible.cfg - `ansible-inventory` — inventory structure, dynamic inventories - `ansible-roles` — role structure and defaults vs vars