Chapter 2: Inventory
Topic 09

Group and Host Variables

Variables

Variables in Ansible attach to hosts and groups, and the cleanest place to put them is not the inventory file at all. It is a directory layout Ansible loads automatically: group_vars/ and host_vars/, sitting beside your inventory or playbook. A file named group_vars/web.yml applies to every host in web; host_vars/db1.larkspur.io.yml applies to that one host.

The directory layout, not inline inventory variables, is how real projects stay readable. The host list stays a list of hosts, and the structured config — the nginx settings, the PostgreSQL version, one database's tuning — lives in named files a reviewer can find and a diff can show. This topic builds the Larkspur variable layout the rest of the book leans on.

The Auto-loaded Directories

Next to your inventory or playbook, Ansible reads group_vars/<groupname>.yml and host_vars/<hostname>.yml automatically, with no line in the inventory telling it to. The filename is the wiring: name a file after a group and its variables attach to that group, name one after a host and they attach to that host.

group_vars/all.yml is a special case worth knowing on its own — it sets defaults for every host in the inventory, because all is the group every host belongs to. It is the natural home for fleet-wide settings like the timezone and the base package list, the things true of every Larkspur box regardless of tier.

group_vars/all.yml — defaults that reach every host
# applies to every host, because all contains every host
timezone: Etc/UTC
base_packages:
  - curl
  - vim
  - htop

Why Not Inline

Putting variables directly in the inventory file works for two or three simple values. It stops working as the values grow into structured maps — an nginx config block, a set of application settings — that bury the host list under data and make a small inventory hard to scan for what it is supposed to be: a list of hosts.

The group_vars and host_vars directories keep those large, structured sets out of the host list and in version-controlled, reviewable files. A change to the web tier's settings shows up as a diff in group_vars/web.yml, not as a churned line in the middle of an inventory, which is exactly the separation a pull-request review wants.

Layering by Specificity

Variables layer by how specific the scope is. all is the broadest, a named group like web is narrower, and a single host is the narrowest. When the same variable is set at more than one level, Ansible merges them so the more specific value wins — a host overrides its group, a group overrides all.

Narrowing scope — the more specific value wins
group_vars/all
timezone, base_packages — every host
group_vars/web
nginx + gunicorn — the web tier
host_vars/db1.larkspur.io
shared_buffers — just this database

This is the everyday mechanism behind "staging is like prod but with smaller instances." Set the shared shape in group_vars/all.yml, then override the one or two values that differ at the group or host level, instead of copying the whole config per environment. The override is narrow and the common case is written once.

Directories Instead of Files

group_vars/web can be a directory rather than a single web.yml file, and Ansible merges every file inside it. That turns one group's variables into a small collection — a vars.yml for plaintext settings and a vault.yml for encrypted secrets — that load together as if they were one file.

This is exactly how you keep a group's plaintext settings beside its encrypted Vault file without mixing the two. The plaintext stays readable and diffable; the secret stays encrypted on its own; and neither forces the other to share its format. The layout below splits the web group's settings from its secret.

A group's vars split into a directory of merged files
# group_vars/web/ is a directory; both files load and merge
group_vars/
  web/
    vars.yml      # plaintext: nginx_worker_processes, app_port
    vault.yml     # ansible-vault encrypted: the app's secret key

The Larkspur Layout

Pulling it together, the Larkspur variable tree mirrors the inventory's groups. group_vars/all.yml holds the timezone and base packages; group_vars/web.yml holds the nginx and gunicorn settings; group_vars/db.yml holds the PostgreSQL version; and host_vars/db1.larkspur.io.yml holds the tuning specific to that one database server.

Each value sits at the scope it actually has, so a setting meant for one host never leaks onto a whole tier and a fleet-wide default is written exactly once. The rest of the book builds on this layout, and confirming a host's resolved variables with ansible-inventory --host db1.larkspur.io is how you check it landed where you meant.

The Larkspur variable layout — scope matched to file
group_vars/
  all.yml       # timezone, base_packages
  web.yml       # nginx + gunicorn settings
  db.yml        # postgresql_version
host_vars/
  db1.larkspur.io.yml   # this DB's shared_buffers tuning
Common Mistakes
  • Scattering the same variable into both inventory inline vars and group_vars, then being unable to tell which value wins — pick group_vars and keep the inventory for connection details only.
  • Naming a file group_vars/web without the .yml extension or the structure Ansible expects, so it is silently never loaded and the variables simply do not exist at run time.
  • Putting host-specific values in a group file, so a setting meant for db1 lands on every db host — match each file's scope to the variable's scope or watch it spread.
  • Mixing plaintext and secret variables in one group_vars/web.yml, making it impossible to encrypt the secrets without encrypting the whole file — split into a directory with a separate vault.yml.
  • Copying an entire config per environment to change two values, instead of setting the shared shape in all and overriding narrowly at the group or host level.
Best Practices
  • Keep variables in group_vars and host_vars directories rather than inline inventory, so structured config is reviewable and the host list stays a host list.
  • Put fleet-wide defaults in group_vars/all.yml and override narrowly at the group or host level, leaning on specificity instead of repeating the same value across environments.
  • Use a directory per group (group_vars/web/vars.yml plus vault.yml) to keep encrypted secrets beside plaintext settings without entangling the two formats.
  • Name and place the files exactly as Ansible expects — the .yml extension, the matching group or host name — so auto-loading actually happens instead of silently not.
  • Confirm a host's resolved variables with ansible-inventory --host db1.larkspur.io after a change, so you see where a value actually landed rather than where you assumed it would.
Comparable tools Puppet Hiera the agent-world data-layering analog for hosts and groups Terraform .tfvars the provisioning-side equivalent of variable files Salt pillars play the group- and host-data role in Salt

Knowledge Check

Which file applies its variables to a single host rather than a group?

  • host_vars/db1.larkspur.io.yml — named after the host, so its variables attach to just that machine
  • group_vars/db.yml — the closest match, since the db group holds a single database host
  • group_vars/all.yml — since all ultimately resolves down to the individual hosts
  • inventory.yml — since host variables are only ever allowed to live inside the main inventory file itself

Why do group_vars directories beat inline inventory variables for real projects?

  • They keep large, structured config in version-controlled, reviewable files while the host list stays readable
  • Inline inventory variables are silently ignored by Ansible at parse time, so none of them ever actually take effect
  • Inline variables load more slowly because Ansible re-parses the whole inventory before every single task
  • Only group_vars files may hold string values, while inline inventory variables are restricted to numbers

How does all → group → host layering support per-environment overrides?

  • The shared shape is set once at the broad level and the few differing values are overridden narrowly, the specific one winning
  • Each environment has to redefine every single variable in full from scratch, since the layers never merge with one another at all
  • The broadest level always wins, so a value set in all overrides any value at the group or host level
  • Ansible picks one of the layers at random for each run unless you mark one as authoritative

Why split a group's variables into a directory instead of one file?

  • It keeps an encrypted vault.yml beside a plaintext vars.yml, so secrets stay encrypted without encrypting the readable settings
  • A directory of small files loads faster than one large file because Ansible can read all of its members on parallel threads at once
  • Ansible refuses to load any single group_vars file once it grows past a few dozen lines
  • Only a directory can hold list and map values, since a single file is limited to plain scalars

You got correct