Skip to content

Facts & Variable Precedence

Variables covered where you define values. This page covers the variables Ansible provides automatically (facts and magic variables) and the rules for which value wins when the same name is defined in more than one place.

Facts — what Ansible discovers about a host

Before running tasks, Ansible connects to each host and gathers facts: OS, IP addresses, CPU, memory, disks, and more. They live in the ansible_facts dictionary, available in tasks and templates.

Why ansible_facts['...'] and not ansible_distribution_release

Older examples use top-level variables like ansible_distribution_release. They still work, but since ansible-core 2.20 that form prints a deprecation warning (INJECT_FACTS_AS_VARS will default to False in 2.24), so every example in this section uses the dictionary form: ansible_facts['distribution_release']. The setup module's filter= still matches the ansible_-prefixed names, as below.

# See every fact for a host
ansible server1.example.com -m setup

# Filter to a subset
ansible server1.example.com -m setup -a "filter=ansible_distribution*"
ansible server1.example.com -m setup -a "filter=ansible_memtotal_mb"

Facts you'll reach for often:

Fact Example value
ansible_facts['distribution'] Debian
ansible_facts['distribution_major_version'] 13
ansible_facts['distribution_release'] trixie
ansible_facts['os_family'] Debian
ansible_facts['default_ipv4']['address'] 192.0.2.10
ansible_facts['hostname'] server1
ansible_facts['processor_vcpus'] 4
ansible_facts['memtotal_mb'] 7900

They're used throughout this repo — e.g. the Docker role builds the apt repo line with {{ ansible_facts['distribution_release'] }} so the same task works on Debian 12 or 13.

Turn off fact-gathering to speed up simple plays

Gathering facts adds a round-trip per host. If a play doesn't need any facts, skip it:

- hosts: homelab
  gather_facts: false

Magic variables — always available

These describe the run itself, not the machine:

Variable Meaning
inventory_hostname The host's name as written in the inventory (server1.example.com)
inventory_hostname_short Just the first label (server1) — used for borg repo paths here
group_names List of groups the current host belongs to
groups Dict of every group → its hosts (groups['apps'])
hostvars Access another host's variables: hostvars['nas.example.com'].borg_repo_path
ansible_play_hosts Hosts still active in the current play
- name: Show which groups this host is in
  ansible.builtin.debug:
    msg: "{{ inventory_hostname }} is in {{ group_names }}"

- name: Reference every app host
  ansible.builtin.debug:
    msg: "app servers: {{ groups['apps'] }}"

Setting variables at runtime — set_fact

Create or compute a variable mid-play:

- name: Compute half the RAM for a container limit
  ansible.builtin.set_fact:
    container_mem_mb: "{{ (ansible_facts['memtotal_mb'] | int * 0.5) | int }}"

- name: Use it later
  ansible.builtin.debug:
    msg: "Limiting to {{ container_mem_mb }} MB"

register (from Writing Tasks) is the other runtime source — it captures a task's result.

Variable precedence (which value wins)

When the same variable name is defined in multiple places, Ansible picks by a fixed priority. From lowest to highest (higher wins), the parts you'll actually touch:

  1. Role defaults (roles/x/defaults/main.yml) — the weakest, meant to be overridden
  2. group_vars/all
  3. group_vars/<specific group>
  4. host_vars/<host>
  5. Facts / set_fact / registered vars
  6. -e / --extra-vars on the command line — always wins
graph LR
  A[role defaults] --> B[group_vars/all]
  B --> C[group_vars/group]
  C --> D[host_vars/host]
  D --> E[set_fact / register]
  E --> F["--extra-vars (-e)"]

Practical takeaways:

  • Put safe fallbacks in roles/<role>/defaults/main.yml — anything can override them.
  • Put shared settings in group_vars/all.yml.
  • Host-specific overrides go in host_vars/<host>.yml — they beat group vars (that's why host_vars/nas.example.com.yml can override docker_compose_dir).
  • Force a one-off value with -e:
ansible-playbook playbooks/base.yml -e "timezone=UTC" --limit server1.example.com

The full precedence list is longer

Ansible actually defines ~22 precedence levels. The six above cover everyday homelab use; see the reference if you hit a surprising override.

Ref: Ansible — Variable precedence