Inventory & variables¶
Scope: the course page shows one hand-written
inventory/hosts.ymland agroup_vars/all.yml. This page slows down on why inventory is split into groups and per-host/per-group variable files, and what dynamic inventory buys you over a static file. Its centerpiece is the full variable precedence ladder — the single most common source of "why did my override not take" confusion.
TL;DR — in 30 seconds:
- Inventory is just hosts + groups + vars from any source — a play targeting
hosts: webdoesn't care whether that came from a hand-written YAML file or a live API query. host_varsalways beatsgroup_vars, at any group specificity — there's no "more specific group wins" rule; host-level variables simply sit higher on the precedence ladder.- Command-line
-e(extra-vars) is the highest-precedence override, by design — it's the explicit "I mean it, override everything" escape hatch for one run.
1. Inventory is just a source of hosts + groups + variables¶
Every Ansible run needs three things about its targets: which hosts, how they're grouped, and
what variables apply to each. An inventory source — static file or dynamic plugin — supplies all
three. Ansible doesn't care which source it came from; a play targeting hosts: web behaves identically
whether web was typed by hand into YAML or discovered by querying an API a second ago.
| Source | How hosts are found | Update mechanism | Best fit |
|---|---|---|---|
| Static inventory | Hand-written in hosts.yml (or INI) |
Edit the file | Small, stable fleets — you know every host by name |
| Dynamic inventory | Queried at run time from an external system (a cloud API, a CMDB) | Automatic — reflects the source's current state | Fleets that autoscale, get replaced, or are too large to hand-maintain |
A dynamic inventory is a plugin (for example amazon.aws.aws_ec2) configured by a small YAML file whose
name ends *.aws_ec2.yml; Ansible calls the plugin at the start of every run, and it returns hosts grouped
by whatever the source exposes (tags, region, VPC). The trade-off: a static file is inspectable and
diffable in a PR; a dynamic source is always current but only as trustworthy as the tags/metadata it reads
from.
flowchart LR
Static["Static hosts.yml<br/>(hand-maintained)"]
Dynamic["Dynamic plugin<br/>(queries a live source)"]
Parsed["Parsed inventory:<br/>hosts + groups + vars"]
Play["A play's hosts: pattern"]
Static --> Parsed
Dynamic --> Parsed
Parsed --> Play
2. Groups are how you avoid repeating yourself¶
A group is a named bucket of hosts. Two things make groups worth using beyond "list of names":
- Targeting — a play's
hosts:line can address a whole group (hosts: web) instead of enumerating every host, and supports set operations:web:&prod(intersection),web:!staging(exclusion). - Shared variables — a variable set at the group level applies to every member without repeating it per host.
Nested groups (the children: key) let you compose without duplicating host lists — a prod group
that is simply web + db as children means adding a host to web automatically makes it part of prod
too, with no second listing to keep in sync.
3. group_vars and host_vars — where variables actually live on disk¶
Two directories sit next to the inventory file, each holding one YAML file per group or per host:
inventory/
├── hosts.yml
├── group_vars/
│ ├── all.yml # applies to every host
│ ├── web.yml # applies to hosts in the `web` group
│ └── prod.yml # applies to hosts in the `prod` group
└── host_vars/
├── web-01.yml # applies only to web-01
└── db-01.yml # applies only to db-01
Ansible loads every matching file automatically — there's no include: to write. The naming is the
wiring: a file named group_vars/web.yml applies to the web group purely because of its filename. This
is also why a typo in a group or host name silently produces "my variable never applied": nothing errors,
the file just never matches any host.
host_vars beats group_vars for the same variable name — a host-specific override always wins over
whatever its groups set, which is exactly what you want for the one host that needs a different AMI user
or a different resource size than the rest of its group.
4. Variable precedence — the full ladder¶
The course page's simplified precedence (command-line beats playbook beats role beats host/group beats role defaults) is the 80% you'll use daily. The table below condenses Ansible's full, 22-level precedence order to the 9 that matter once a fleet outgrows one inventory file, lowest to highest:
| # | Source | Notes |
|---|---|---|
| 1 | Role defaults/main.yml |
Lowest. Meant to be overridden. |
| 2 | Inventory group vars (group_vars/all) |
Applies broadly. |
| 3 | Inventory group vars (other groups) | More specific groups still lose to host_vars. |
| 4 | Inventory host_vars |
Beats every group-level source. |
| 5 | Playbook group vars / host vars (inline, rare) | Same idea, defined inside the playbook instead of inventory files. |
| 6 | Play vars: |
Set directly on the play. |
| 7 | Role vars/main.yml |
Higher precedence than defaults — harder for a consumer to override. |
| 8 | set_fact / registered variables |
Computed during the run; wins over anything set before the task ran. |
| 9 | Command-line -e (extra-vars) |
Highest. Always wins, by design — it's the explicit "I mean it" override. |
flowchart BT
L1["1 · role defaults"]
L2["2 · group_vars/all"]
L3["3 · group_vars/<group>"]
L4["4 · host_vars"]
L5["5 · play vars"]
L6["6 · role vars"]
L7["7 · set_fact / register"]
L8["8 · -e extra-vars"]
L1 --> L2 --> L3 --> L4 --> L5 --> L6 --> L7 --> L8
style L8 stroke-width:3px
Two practical consequences worth internalizing:
host_varsalways beatsgroup_vars, at any group specificity — there's no "more specific group wins" rule to reason about; host-level is simply above every group level.-eon the command line always wins, over even rolevars/. This is intentional: it's the escape hatch for "just this one run, override everything" (a hotfix, a debug session) without editing any file.
5. How this connects¶
The course page's single inventory/hosts.yml plus one group_vars/all.yml is the smallest version of
this system: one source, one group-level file, no host overrides, no dynamic plugin. This page is what to
reach for once a fleet grows past that — splitting variables by group and host instead of duplicating them,
and knowing exactly which file wins when two of them disagree.
Common gotchas
- Typo'ing a group or host name in a
group_vars/host_varsfilename. Fix: nothing errors — the file just silently never matches any host; if a variable "isn't applying," check the filename against the exact group/host name first. - Expecting a more specific group to beat a less specific one. Fix: group-level precedence isn't
about specificity — every
group_varssource loses to anyhost_varssource, regardless of nesting. - Reaching for a dynamic inventory plugin on a small, stable fleet "to be safe." Fix: a static file is diffable and reviewable in a PR; a dynamic source is only as trustworthy as the tags/metadata it reads — don't pay that cost without an autoscaling or too-large-to-hand-maintain fleet to justify it.
- Forgetting
-eon the command line beats everything, including rolevars/. Fix: that's intentional — it's the one-off override for a hotfix or debug session — but it also means a stray-ein a saved script can silently mask every other precedence source.
Check yourself — you set a variable in group_vars/web.yml and override it in group_vars/prod.yml for a host in both groups. Which value wins, and why doesn't 'more specific group' decide it?
Ansible doesn't rank groups by specificity — both are the same precedence tier (inventory group vars),
so the result depends on group evaluation order, not which group "sounds" more specific. The only
variable-precedence fact you can rely on here is that a host_vars entry would beat either of them
outright.
Check yourself — a teammate swears they set a variable in host_vars/web-03.yml, but the playbook uses the group default instead. Nothing errored. What's the most likely cause?
The filename doesn't exactly match the inventory hostname (a typo, wrong case, or a .yaml vs .yml
mismatch) — Ansible loads host_vars/group_vars files purely by filename match, so a mismatch means
the file is silently never read, with no error.
You can defend this when you can say why a host-level override always beats a group-level one
regardless of group nesting, explain what a dynamic inventory plugin buys you over a static file (and what
it costs in inspectability), and place -e, role defaults, and host_vars correctly on the precedence
ladder without guessing.