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

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:

  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

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