Group and Host 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.
# 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.
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.
# 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.
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
- Scattering the same variable into both inventory inline vars and
group_vars, then being unable to tell which value wins — pickgroup_varsand keep the inventory for connection details only. - Naming a file
group_vars/webwithout the.ymlextension 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
db1lands on everydbhost — 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 separatevault.yml. - Copying an entire config per environment to change two values, instead of setting the shared shape in
alland overriding narrowly at the group or host level.
- Keep variables in
group_varsandhost_varsdirectories 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.ymland 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.ymlplusvault.yml) to keep encrypted secrets beside plaintext settings without entangling the two formats. - Name and place the files exactly as Ansible expects — the
.ymlextension, 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.ioafter a change, so you see where a value actually landed rather than where you assumed it would.
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 machinegroup_vars/db.yml— the closest match, since the db group holds a single database hostgroup_vars/all.yml— sinceallultimately resolves down to the individual hostsinventory.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_varsfiles 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
alloverrides 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.ymlbeside a plaintextvars.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_varsfile 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