Files
howtos/topics/ansible-variables.md
user 80f82dcde7 docs: add ansible howto collection
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
2026-02-21 20:33:34 +01:00

202 lines
5.9 KiB
Markdown

# 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:
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