Four reference files covering: - ansible.md — core commands, ansible.cfg, key settings - ansible-inventory.md — static/dynamic inventory, directory layout - ansible-variables.md — 22-level precedence, scoping, merge behavior - ansible-roles.md — structure, defaults vs vars, dependencies
5.9 KiB
5.9 KiB
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/<group> |
Medium | Environment/group-specific |
host_vars/<host> |
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:
allgroup (lowest)- Parent groups (alphabetical within same level)
- Child groups (override parents)
- Host vars (override all groups)
Controlling Merge Order
# 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):
# 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
# 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
# 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:
# ansible.cfg
[defaults]
hash_behaviour = merge # CAUTION: global, can cause surprises
Prefer explicit merging with combine filter:
- name: Merge defaults with overrides
set_fact:
final_config: "{{ default_config | combine(override_config, recursive=True) }}"
Registered Variables
- 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
# 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
# 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_varsalways win — they override even rolevars/. Don't use-efor defaultsset_factpersists for the host across plays in the same run (not across runs unless cached)hash_behaviour = mergeis global and rarely what you want — prefercombinefilter- Variable names are flat —
foo.baris dict access, not a separate variable - Jinja2 variables in
when:don't need{{ }}— justwhen: 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.cfgansible-inventory— inventory structure, dynamic inventoriesansible-roles— role structure and defaults vs vars