Precedence: When Two Places Disagree
Scenario: Every host is serving port 7777. The group file says 8081, the host file says 9999, and nobody can explain where 7777 came from - until someone finds a vars: block added during an incident six weeks ago.
New words, in plain English
- Precedence - the fixed order that decides which definition wins when a variable is set in several places.
- Role defaults - the LOWEST priority. Designed to be overridden.
- Role vars -
roles/x/vars/main.yml- high priority, and NOT intended for user configuration. - Extra vars -
-eon the command line - the HIGHEST priority. Nothing overrides it.
Ansible resolves conflicts with a fixed, documented order. You do not need to memorise all twenty-two levels, but you must know the shape, from weakest to strongest:
- Role defaults (
roles/x/defaults/main.yml) - lowest, always overridable. - Inventory group vars, with
allweakest and more specific groups stronger. - Inventory host vars - a host beats its groups.
- Play vars (
vars:,vars_files:) - beats everything from the inventory. - Role vars (
roles/x/vars/main.yml) andset_fact- higher still. - Extra vars (
-e) - absolute winner.
The scenario above is the classic trap: a play-level vars: block outranks both group_vars and host_vars, so the carefully-structured inventory becomes decoration. That is why "temporary" incident overrides in a play are so dangerous - they are invisible from the inventory and they silently win.
The design guidance falls straight out of the order: role defaults/ for tunables, group_vars/ for environment and tier settings, host_vars/ for documented exceptions, -e for emergencies only. Keep user-configurable values out of role vars/, because their high priority makes them effectively un-overridable.
Analogy: A dress code: the company handbook is the default, a department can be stricter, an individual can hold a documented exemption, and a manager shouting an instruction on the day beats all three. If the manager shouts every day, the handbook has stopped meaning anything.
A worked example
# roles/webapp/defaults/main.yml (weakest - meant to be overridden)
webapp_port: 8080
# group_vars/all.yml (estate-wide)
app_env: production
# group_vars/webservers.yml (beats group_vars/all)
webapp_port: 8081
# host_vars/lb01.yml (beats its groups - a real exception)
webapp_port: 8088
# In a play - beats BOTH of the above. Use sparingly.
vars:
webapp_port: 7777
# On the command line - beats absolutely everything.
ansible-playbook site.yml -e webapp_port=9999
Do not read files to find out what won - ask Ansible. ansible-inventory --host web01 prints every inventory-sourced variable for that host. A debug task prints the fully-resolved value at exactly the point the play would use it, which also captures play vars, role vars and extra vars that ansible-inventory cannot see.
One more rule that catches people out: when a host belongs to several groups at the same level, the last group loaded wins, and load order is alphabetical unless you set ansible_group_priority. Relying on that is fragile; if two sibling groups both define a variable, restructure rather than depend on the ordering.
Warning: Avars:block in a play beats bothgroup_varsandhost_vars. An override added during an incident and never removed will silently defeat your entire inventory structure.
Goal: Put this to work in the ansible-variable-precedence lab. Open/labs/ansible, pickansible-variable-precedence, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.