Skip to main content
RunBook Academy

AnsibleXXIX · Dynamic InventoryOperating a dynamic source

Merging static and dynamic inventory

Advanced⏱ ~25 minbash

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

Not yet marked complete on this device.

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:

Read-only / Safethe merged result
$ 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:

Read-only / Safesame two files, opposite order
$ 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:

FormOrder
-i a.yml -i b.ymlLeft 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.

Read-only / Safeinventory/group_vars/role_web.yml
service_port: 8443
managed_by: platform-team
Read-only / Safethe variables land on the members
$ 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 directoryinventory/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.

Read-only / Safeinventory/90-do-not-automate.yml
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:

Read-only / Safethe exclusion, applied
$ 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.com

Ordering a real inventory directory

Read-only / Safea layout that survives contact with a team
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 on

Four properties that layout buys:

  1. The merge order is readable without running anything. Somebody new can tell which file has the last word by looking at the names.
  2. Constructed groups come after the sources they derive from, so the visibility rule is satisfied structurally rather than by luck.
  3. The exclusion list is unambiguously last, so no dynamic source can overwrite a variable it sets.
  4. 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

  1. 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?

  2. Q2. A group_vars file only applies to groups that are defined in a static inventory file.

  3. Q3. A constructed config produces no groups at all, with strict: false and no warnings. What is the most likely cause?

  4. 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.