Lesson 4 of 9 · 24m
Variables, Facts, and the 22-Level Precedence Maze
Where variables can live, why 'host_vars beats group_vars' is only the start, how gathered facts work, and the precedence rules that explain every 'where did that value come from' bug.
A teammate once spent a day chasing why a deploy used the wrong API URL. The value was overridden in five places: role defaults, group_vars, host_vars, a vars: block on the play, and finally an -e on the Jenkins job that nobody remembered. The playbook wasn't broken — the team didn't share a model of which var wins.
Facts: what the host tells you
gather_facts: true (the default) runs the setup module and populates ansible_facts: OS, network, memory, mounts, virtualisation, all of it.
- name: Show the facts I'd template with
ansible.builtin.debug:
msg: "{{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }} on {{ ansible_facts['default_ipv4']['address'] }}"Facts are per-host and gathered fresh each play — ansible_facts['memtotal_mb'] isn't from last week's cache unless you enable fact caching. On 5,000 hosts, gather_facts: false on plays that don't need them is a real speed win, or switch to gather_subset: '!all' / smart gathering.
Where variables live — from weakest to strongest
Roughly, lowest to highest precedence:
- Role
defaults/main.yml— the contract layer; meant to be overridden. - Inventory
group_vars/all, then group_vars (deeper groups win), thenhost_vars. - Gathered host facts.
- Play
vars:,vars_prompt:,vars_files:. - Role
vars/main.yml, then block and taskvars:— high precedence, not for overridable knobs. set_factandregistered results during the run.-eextra vars on the command line — wins everything. Always.
The mnemonic that matters operationally: defaults are for libraries, host/group vars are for environment, -e is for emergencies and CI parameters. Anything else is a surprise.
register and set_fact: data born during the run
- name: Get package version
ansible.builtin.command: dpkg-query -W -f='${Version}' nginx
register: nginx_version
changed_when: false
- name: Record it for later tasks
ansible.builtin.set_fact:
deployed_nginx: "{{ nginx_version.stdout }}"register captures a task's full result (stdout, rc, changed, module-specific fields). set_fact mints a real host variable visible to later tasks and plays — a common pattern for computed values like "the new instance's IP from the cloud module result."
Magic variables you should know
inventory_hostname, groups['webservers'] (all hosts in a group), hostvars['db01']['ansible_facts'] (another host's facts — enables "template every web node with the DB's IP"), play_hosts, ansible_play_batch (hosts in the current serial slice).
Field notes
- A YAML value that starts with
{{must be quoted —port: "{{ app_port }}"— or the parser reads it as a dict and the play fails to load. vars/inside a role outranks inventory vars — if you want operators to tune it, it belongs indefaults/.- Debug with
ansible.builtin.debug: var=suspicious_variable— cleaner thanmsg:because it prints the resolved structure.