// learn Β· linux Β· rhce Β· 10 min

Ansible variables & facts.

The same variable can be defined in a dozen places β€” what wins is decided by precedence. Watch one value climb a precedence ladder, then meet gathered facts and ansible_facts, register and set_fact, and custom facts.d.

Ansible variables and facts animated tutorial. Defining variables in play vars, vars_files, group_vars and host_vars, inventory, role defaults and vars, and CLI extra-vars; referencing them with Jinja2 templating; the variable precedence ladder where extra-vars win and role defaults lose; gathering facts with gather_facts and the setup module; reading ansible_facts; capturing runtime values with register and set_fact; magic variables; and custom facts in facts.d under ansible_local. RHCE EX294 ready.
// ansible Β· variables & facts

The same variable can be defined in a dozen places. Watch one value, http_port, climb a precedence ladder as more sources define it β€” then meet facts, register, set_fact, and custom facts.d.

// before you start
you should know
  • Β·Know an Ansible inventory, groups, and group_vars
  • Β·Comfortable reading basic YAML and a simple playbook
  • Β·Seen Jinja2 {{ }} templating before (helps, not required)
by the end you'll

Define variables in every common location, reference them with Jinja2, predict which source wins by precedence, gather and read facts, and capture runtime values with register and set_fact.

pace: 10 minutes

ansible β€” step 1 / 6 Β· defining
variable: http_porthighest precedence on top ↑
extra-vars -e
β€” not set β€”
play vars:
β€” not set β€”
host_vars/servera
β€” not set β€”
group_vars/webservers
β€” not set β€”
inventory group_vars/all
http_port = 80← wins
role defaults (roles/web/defaults/main.yml)
http_port = 8080
role defaults always lose ↓

Defining variables β€” many places

A variable can be set in a startling number of places: a play's vars: block, files pulled in with vars_files:, the group_vars/ and host_vars/ directories, the inventory itself, a role's defaults/ and vars/, captured at runtime with register, asserted with set_fact, and passed on the CLI with -e. Names use letters, digits, and underscores and cannot start with a digit. At this stage only the low rungs are set β€” role defaults give http_port its baseline.

$vars:\n http_port: 9000
$# group_vars/webservers.yml\nhttp_port: 8000
$ansible-playbook site.yml -e "http_port=9999"
// key insight

The same variable can be set in a dozen places β€” what actually matters is precedence. Extra-vars (-e) always win; role defaults always lose. Everything else sits on a ladder in between, and for scalars the highest rung wins outright β€” there is no merging. Know the ladder and "why is my variable not the value I set?" stops being a mystery.

// exam-ready Β· variables & facts
$vars:
$vars_files:
$group_vars/NAME.yml
$host_vars/HOST.yml
$-e "k=v"
${{ var }}
${{ var | default(X) }}
$var['key'] / var.key
$gather_facts: true | false
$ansible HOST -m setup
$-a "filter=ansible_*"
$ansible_facts['default_ipv4']['address']
$register: r β†’ r.stdout / r.rc / r.changed
$set_fact: k: v
$inventory_hostname, hostvars, groups
$/etc/ansible/facts.d/*.fact β†’ ansible_local
// check yourself
4 quick questions
Q1

http_port is set in group_vars/webservers (8000) and also passed as -e http_port=9999. Which value does the play use?

Q2

How do you populate ansible_facts for a host?

Q3

What is the difference between register and set_fact?

Q4

A variable's value starts with {{. What must you do in the YAML?

These aren't graded β€” they're just for active recall, which is what actually makes the lesson stick.

πŸ”
// next: repetition & logic

Ansible loops & conditionals

Now that a variable can hold a list or a dict, drive tasks with it: loop over items, gate work with when, and handle each host's data cleanly.

open β†’

Read this lesson as text6 chapters Β· quiz answers

  1. 01Defining variables β€” many places

    A variable can be set in a startling number of places: a play's vars: block, files pulled in with vars_files:, the group_vars/ and host_vars/ directories, the inventory itself, a role's defaults/ and vars/, captured at runtime with register, asserted with set_fact, and passed on the CLI with -e. Names use letters, digits, and underscores and cannot start with a digit. At this stage only the low rungs are set β€” role defaults give http_port its baseline.

    • $ vars:\n http_port: 9000 β€” set in the play itself
    • $ # group_vars/webservers.yml\nhttp_port: 8000 β€” auto-loaded for the group
    • $ ansible-playbook site.yml -e "http_port=9999" β€” CLI extra-var
  2. 02Using variables β€” Jinja2 templating

    Reference a variable with double braces: {{ http_port }}. One YAML trap: if a value STARTS with {{, you must quote the whole string, or YAML reads the brace as the start of a dict and errors. Reach into a dict with myvar.key or myvar['key'], and into a list with mylist[0] β€” the bracket form is safest when keys collide with method names. Here group_vars and host_vars have also been set, so a higher rung now wins.

    • $ - name: template config\n ansible.builtin.template:\n dest: /etc/web.conf\n vars: { port: "{{ http_port }}" } β€” use it in a task
    • $ {{ users['deploy'].uid }} β€” dict access, bracket + dot
    • $ {{ http_port | default(80) }} β€” fallback if undefined
  3. 03Precedence β€” who wins

    When the same variable is defined in multiple places, the highest-precedence source wins outright β€” for scalars the sources do NOT merge. Roughly, from strongest to weakest: command-line -e (always wins) > task vars > block vars > role/include vars > play vars > host_vars > group_vars > inventory > role defaults (always lose). Now every rung is set, so the very top of the ladder β€” extra-vars β€” takes the value and everything below it is ignored.

    • $ ansible-playbook site.yml -e "http_port=9999" β€” -e beats everything
    • $ # host_vars/servera.yml beats group_vars/webservers β€” more specific host wins
    • $ # roles/web/defaults/main.yml β€” defaults are the floor β€” always lose
  4. 04Facts β€” what Ansible learns about a host

    At the start of a play Ansible runs an implicit setup task (gather_facts: true) which populates ansible_facts with memory, IP addresses, distribution, mounts, and much more. Turn it off with gather_facts: false when you don't need it, or trim it with gather_subset to speed plays up. Access values as ansible_facts['default_ipv4']['address'] β€” the older flat names like ansible_default_ipv4 still work but the namespaced form is preferred.

    • $ ansible servera -m setup β€” dump every gathered fact
    • $ ansible servera -m setup -a "filter=ansible_distribution*" β€” just the distro facts
    • $ {{ ansible_facts['default_ipv4']['address'] }} β€” use a fact in a task
  5. 05register & set_fact

    register captures a task's return value β€” stdout, rc, changed, and more β€” into a variable you can test or print in later tasks. set_fact defines an arbitrary variable at runtime that then persists for the rest of the play, handy for computed values. Both create host-scoped vars. Magic vars round it out: inventory_hostname, hostvars (every host's facts/vars), groups, and group_names are always available.

    • $ - shell: id -u deploy\n register: result\n- debug: var=result.stdout β€” capture + reuse
    • $ - set_fact:\n web_port: "{{ http_port }}" β€” define at runtime, persists
    • $ {{ hostvars['serverb']['ansible_facts'] }} β€” read another host's facts
  6. 06Custom facts (facts.d)

    You can teach a managed host to report your own metadata. Drop an executable script or a static INI .fact file into /etc/ansible/facts.d on the target; the setup module reads it and exposes the data under ansible_local. This is the clean way to ship site-specific facts β€” environment, tier, owner β€” without baking them into playbooks. The key on the ladder is unchanged here; custom facts are read-only data, not a precedence source.

    • $ # /etc/ansible/facts.d/env.fact\n[web]\ntier = frontend β€” INI custom fact
    • $ {{ ansible_local.env.web.tier }} β€” read it back
    • $ ansible servera -m setup -a "filter=ansible_local" β€” see all local facts

Quiz β€” answers

http_port is set in group_vars/webservers (8000) and also passed as -e http_port=9999. Which value does the play use?
β†’9999 β€” extra-vars passed with -e have the highest precedence and win over everything.Command-line extra-vars (-e) sit at the very top of the precedence ladder β€” nothing overrides them. For scalars there is no merging; the highest rung simply wins, so 9999 is used.
How do you populate ansible_facts for a host?
β†’gather_facts runs the setup module at play start (on by default), or you run the setup module ad-hoc β€” that fills ansible_facts.Facts come from the setup module. A play gathers them implicitly via gather_facts: true (the default), and you can also collect them on demand with ansible HOST -m setup.
What is the difference between register and set_fact?
β†’register captures a task's return value (stdout, rc, changed); set_fact assigns an arbitrary value at runtime β€” both persist for the rest of the play.register stores whatever a task returned so later tasks can inspect it. set_fact lets you compute and name a value yourself. Both create host-scoped variables that live for the remainder of the play.
A variable's value starts with {{. What must you do in the YAML?
β†’Quote the whole value as a string, otherwise YAML treats the leading { as the start of a dict and errors.A bare leading {{ looks like inline-dict syntax to the YAML parser. Wrapping the whole value in quotes β€” port: "{{ http_port }}" β€” tells YAML it is a string, and Jinja2 then templates it normally.
// more in systems

keep going β€” these pair well with what you just learned.

see all systems β†’

See also

back to RHCSA / RHCE trackall lessons