AnsibleXXIX · Dynamic InventoryOperating a dynamic source
Merging static and dynamic inventory
What you'll learn
- Predict which source wins when two of them set the same host variable
- Bind group_vars to groups that only exist because a plugin created them
- Keep a hand-maintained exclusion list authoritative over a dynamic source
- Order an inventory directory so the merge result is intentional rather than alphabetical
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
Almost nobody runs a purely dynamic inventory. Real estates end up with a mixture: a provider API for the machines that come and go, and a hand-maintained file for the things the API does not know — the network appliance with no cloud record, the legacy box in a cupboard, and the list of hosts that must never be automated regardless of what any tag says.
Merging is the mechanism that makes that mixture one inventory. You met the basic form in Part IV; this lesson is about the part that only matters once one of the sources is dynamic, and about the exclusion list that has to survive the merge intact.
Groups union, variables overwrite
One sentence, and everything else in this lesson follows from it.
Group membership from all sources is combined. Variables from a later source replace the same variable from an earlier one.
Here is the merge in a directory. 10-first.yml places a host in all
with two variables; 90-last.yml places the same host in a group and
sets one of the same variables:
$ ansible-inventory -i inventory/ --host web01.example.com{
"ansible_host": "192.0.2.11",
"tier": "from-last"
}Reverse the order of the sources and the variable flips, while membership does not:
$ ansible-inventory -i inventory/90-last.yml -i inventory/10-first.yml --host web01.example.com{
"ansible_host": "192.0.2.11",
"tier": "from-first"
}Two ways to supply the order, and both are worth knowing:
| Form | Order |
|---|---|
-i a.yml -i b.yml | Left to right; the rightmost -i is the last word |
-i inventory/ | Lexicographic by filename within the directory |
Which is why real repositories number their inventory files. 00-,
10-, 90- is not tidiness, it is the merge order made explicit.
group_vars bind to dynamic groups too
This is the question people expect to be complicated and it is not: a
group_vars file matching a group the plugin invented works exactly
like one matching a group you wrote.
service_port: 8443
managed_by: platform-team$ ansible-inventory -i inventory/ --graph --vars@all:
|--@ungrouped:
|--@role_web:
| |--web01.example.com
| | |--{managed_by = platform-team}
| | |--{provider_tags = {'role': 'web', 'env': 'prod'}}
| | |--{service_port = 8443}
| |--{managed_by = platform-team}
| |--{service_port = 8443}
|--@role_db:
| |--db01.example.com
| | |--{provider_tags = {'role': 'db', 'env': 'prod'}}The catch is location, and it is the same catch as in Part IV: the
group_vars directory must sit beside the inventory source, and a
second group_vars tree beside the playbook is a different tree with
different precedence. Putting group_vars next to an inventory
directory — inventory/group_vars/ — is the arrangement that keeps
one answer to “where do this group’s variables live”.
The operational consequence is worth stating: a group name from a
provider tag is now also a filename in your repository. Rename the
tag, and group_vars/role_web.yml matches nothing. No error, no
warning; the variables just stop being applied, and the first symptom is
a service configured on the wrong port.
The exclusion list that must survive everything
The reason merging matters for safety is this one file.
Every estate has machines that must not be touched by general automation: a database mid-migration, a system under a vendor support contract that voids on unexpected changes, an appliance whose configuration is managed by another team, a host frozen for a forensic investigation.
That list is the opposite of dynamic. It is decided by people, it changes rarely, and it must not be overridable by an API.
all:
children:
do_not_automate:
hosts:
db01.example.com:
exclusion_reason: 'vendor support contract - ticket OPS-4471'
exclusion_review: '2026-11-01'
legacy-app01.example.com:
exclusion_reason: 'pending forensic hold'
exclusion_review: '2026-09-15'Membership unions, so those hosts are in do_not_automate no matter
what any dynamic source says about them. They will also still be in
role_db, env_prod and everything else the plugin put them in — the
static file adds a group, it does not remove any.
Which means the exclusion does nothing until a pattern uses it:
$ ansible -i inventory/ 'production:!do_not_automate' --list-hosts hosts (6):
web01.example.com
web02.example.com
web03.example.com
web04.example.com
db02.example.com
cache01.example.comOrdering a real inventory directory
inventory/
00-static-core.yml # hosts no API knows about
10-cloud-eu.yml # plugin config: provider, European estate
11-cloud-us.yml # plugin config: provider, American estate
20-constructed.yml # derived groups; sees 00-11 above
90-do-not-automate.yml # the exclusion list, read last
group_vars/
role_web.yml
env_prod.yml
do_not_automate.yml # e.g. a marker variable the guardrail asserts onFour properties that layout buys:
- The merge order is readable without running anything. Somebody new can tell which file has the last word by looking at the names.
- Constructed groups come after the sources they derive from, so the visibility rule is satisfied structurally rather than by luck.
- The exclusion list is unambiguously last, so no dynamic source can overwrite a variable it sets.
- group_vars sits inside the inventory directory, so there is one place a group variable can live and one answer when somebody asks where it came from.
Knowledge check
Knowledge check · 4 questions
Q1. A dynamic source puts db01 in role_db and env_prod. A later static file puts db01 in do_not_automate. What is db01 a member of?
Q2. A group_vars file only applies to groups that are defined in a static inventory file.
Q3. A constructed config produces no groups at all, with strict: false and no warnings. What is the most likely cause?
Q4. What turns a do_not_automate group from a convention into an enforced control? Select all that apply.
Passing score: 75%. Answers are checked in this browser.