← All TILs · ansible

An inventory as a directory: a hosts file without variables, and group_vars per role

ansible - 2026-10-03

Second entry in the Ansible inventory from scratch series. Item 1 wrote an inventory, the list of hosts Ansible manages sorted into groups, as a single YAML file, and kept its variables out of it. This entry gives those variables a place: an inventory directory, with a group_vars/ directory beside the hosts file. It's the layout the Red Hat Community of Practice's automation good practices recommend, and it has a few rules that fail silently. Everything below ran with ansible-core 2.21.4.

The layout

A variable is a named value that tasks and templates read; a role is a packaged set of tasks that configures one thing, such as PostgreSQL, and reads its settings from variables named after it, postgresql_version or postgresql_server_conf. The inventory from item 1, grown into a directory:

inventory/
├── hosts.yml
└── group_vars/
    ├── all/ansible.yml
    ├── app/podman.yml
    ├── backup/backup.yml
    └── postgresql/postgresql.yml

group_vars/postgresql/postgresql.yml:

postgresql_version: "16"
postgresql_server_conf:
  port: 5432
  max_connections: 200

The example's playbook printed the role variables each host received:

app1:
  podman_firewall: [{"port": "8080/tcp", "state": "enabled"}]
  podman_registries_conf: {"unqualified-search-registries": ["registry.example.org"]}
db1:
  backup_keep: 7
  backup_schedule: "daily"
  postgresql_server_conf: {"port": 5432, "max_connections": 200}
  postgresql_version: "16"
jump1:
  (no role variables)

db1 got the postgresql group's variables although the hosts file lists it under db: db is a child of postgresql, and item 1 showed that a host of a child group is a host of its parent. jump1, in no group of its own, got only all's connection settings, from all/ansible.yml.

Why bother, rather than writing the variables in hosts.yml:

Which files Ansible reads

The inventory guide, Organizing host and group variables, says Ansible reads "all the files in these directories in lexicographical order", with the extensions .yml, .yaml, .json or none. ansible-core's loader, parsing/dataloader.py, adds the details, and the example tested each one with a small inventory of its own, printed with ansible-inventory --host db1.

Layout Result
group_vars/postgresql.yml beside group_vars/postgresql/ the file is never read
group_vars/postgres/, for a group called postgresql never read, and no warning
9-base.yml and 10-upgrade.yml set the same variable 9-base.yml's value wins
a ~ backup, a hidden .postgresql.yml, a notes.md all three skipped
a subdirectory, tuning/memory.yml read
a README with no extension the inventory fails to load

In short

The example repository

The series' companion repository, abdelhousni/ansible-inventory-series, holds this inventory, with every host connecting locally. vars.yml writes each host's role variables to its out/ directory, and run.sh runs it, then prints what Ansible makes of each of the five layouts in pitfalls/. Its CI runs it on every push and compares the output with the expected one.

Sources

Created 2026-10-03T04:20:34+02:00 · Edit