Skip to main content
RunBook Academy

AnsibleIV · Inventory FundamentalsInventory mechanics

group_vars and host_vars on disk

Intermediate⏱ ~22 minbash

What you'll learn

  • Lay out group_vars/ and host_vars/ as files or as directories, and predict the read order
  • State which of the two search paths wins when both define the same variable
  • Diagnose an ignored variable using ansible-inventory --host and a debug task together
  • Explain why ansible-inventory --host can disagree with what a playbook actually sees

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Variables set inside the inventory file work and do not scale. Once a group has more than a handful of settings, or any of them is a list or a dictionary, the inventory becomes unreadable and the merge conflicts become constant.

group_vars/ and host_vars/ are the answer: one file per group or host, loaded automatically, no wiring required. The mechanism is straightforward. The part that causes incidents is where Ansible looks for them, because it looks in two places and does not tell you which one it used.

The layout

A file per group, named for the group:

estate/
├── inventory/
│   └── hosts.yml
├── group_vars/
│   ├── all.yml
│   ├── webservers.yml
│   └── databases.yml
├── host_vars/
│   └── db01.example.com.yml
└── playbooks/
    └── site.yml

group_vars/all.yml applies to every host, because every host is in all. group_vars/webservers.yml applies to the members of webservers, including any host that is a member through nesting. host_vars/db01.example.com.yml applies to that host alone, and the filename must match the inventory name exactly — if the inventory says db01.example.com, a file called db01.yml is read by nothing.

The extension may be .yml, .yaml, .json, or absent entirely. Ansible tries them; consistency within a repository matters more than which one you pick.

A directory instead of a file

Any of those files can be a directory, and every file inside it is read:

group_vars/
└── webservers/
    ├── 10-network.yml
    ├── 20-application.yml
    └── 30-secrets.yml

This is the right shape once a group has more than about thirty lines of settings, and it is the standard place to isolate vault-encrypted content — 30-secrets.yml can be encrypted while its siblings stay readable and reviewable in a pull request.

Files inside the directory are read in lexicographic order, and later files override earlier ones for the same key. Hence the numeric prefixes: 10-, 20-, 30- make the order explicit rather than an accident of what people named things.

The two search paths

Here is the part that produces real incidents.

Ansible looks for group_vars/ and host_vars/ relative to the inventory file, and relative to the playbook file. Both. Every run.

estate/
├── group_vars/
│   └── web.yml          <-- playbook-adjacent
├── inventory/
│   ├── hosts.yml
│   └── group_vars/
│       └── web.yml      <-- inventory-adjacent
└── show.yml

Two files, the same name, both valid, both loaded. When they define the same variable, one wins — and it is the playbook-adjacent one.

Here is that arrangement executed. inventory/group_vars/web.yml contains served_by: inventory-adjacent and inventory_only; group_vars/web.yml next to the playbook contains served_by: playbook-adjacent and playbook_only.

Read-only / Safewhat the playbook actually sees
$ ansible-playbook -i inventory/hosts.yml show.yml
PLAY [Show which group_vars won] ***********************************************

TASK [Print the contested variable] ********************************************
ok: [localhost] => {
  "msg": "served_by=playbook-adjacent inventory_only=yes-only-here playbook_only=yes-only-here"
}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Both files were read. The playbook-adjacent value won the contested key, and every key defined in only one of them came through. This is a merge with a precedence rule, not a replacement — which is why the situation survives so long undetected. Most keys are uncontested, so most of both files appears to work.

Read-only / Safethe same host, according to ansible-inventory
$ ansible-inventory -i inventory/hosts.yml --host localhost
{
  "ansible_connection": "local",
  "inventory_only": "yes-only-here",
  "served_by": "inventory-adjacent"
}

Two commands, one repository, one moment in time, two different answers. Neither is lying. ansible-inventory is telling you the truth about the inventory, and the playbook is telling you the truth about the run.

The operational rule follows directly: ansible-inventory --host is authoritative for inventory variables and nothing else. When a variable is not what you expect, use both — and a difference between them is itself the diagnosis.

Diagnosing “the variable I set is being ignored”

The symptom is always the same and the cause is usually one of four things. Work them in this order, because it is cheapest first.

Read-only / Safestep 1 - what does the inventory say?
ansible-inventory -i inventory/hosts.yml --host web01.example.com
Read-only / Safestep 2 - what does the play see?
ansible -i inventory/hosts.yml web01.example.com \
  -m debug -a 'var=my_variable' --connection=local
Read-only / Safestep 3 - find every file that could define it
grep -rn 'my_variable' group_vars/ host_vars/ inventory/ playbooks/ roles/

The four causes, in the order you will meet them:

  1. A second group_vars tree, playbook-adjacent, overriding the inventory-adjacent one. Steps 1 and 2 disagree.
  2. Two groups both setting it. Steps 1 and 2 agree with each other and disagree with you. The last lesson in this part owns this.
  3. Something later in the precedence chain — a play vars:, a role default being overridden, -e on the command line. Ansible’s full variable precedence order is a subject of its own later in the course; for now, know that group_vars is near the bottom of it and is easy to override by accident.
  4. A filename that does not match. host_vars/db01.yml for an inventory host named db01.example.com is read by nothing at all, silently. Step 3 finds this one instantly, which is why it is worth running even when you are sure.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A repository has group_vars/web.yml beside the inventory and another beside the playbook, both setting `app_port`. Which value does the play use?

  2. Q2. `ansible-inventory --host web01` shows every variable the playbook will see for that host.

  3. Q3. A variable set in group_vars/webservers.yml has no effect on web01. Which of these could explain it? Select all that apply.

  4. Q4. A playbook is moved from site.yml at the repository root into playbooks/site.yml. Nothing else changes, and some variables stop taking effect. Why?

Passing score: 75%. Answers are checked in this browser.