Inventory Plugins and Composition
Behind both static files and cloud queries sits one extensible mechanism: inventory plugins. A static YAML inventory is itself parsed by a plugin; an aws_ec2.yml is parsed by another. Once you see that everything is a plugin, combining sources stops being a special case and becomes the default capability.
Understanding how Ansible discovers and merges those sources — and how the constructed plugin layers computed groups on top — is what lets you combine a static core, a cloud query, and derived groups into a single coherent model. This is the topic that turns the chapter's pieces into one inventory.
Plugins vs Scripts
Inventory plugins superseded the old executable scripts. A plugin is YAML-configured, runs in-process, and is cache-aware; a script was an executable that printed a blob of JSON and that you maintained in Python. New work uses plugins, and you will only meet scripts in legacy repositories that predate the plugin model.
The practical difference is maintenance. A plugin config is a few declarative lines; a script is code you own, test, and keep working as the cloud's API shifts. The plugin model removed that burden, which is why reaching for a custom script today usually means re-implementing something a plugin already does.
A Directory of Sources
Point -i at a directory and Ansible runs every inventory source inside it, then merges the results into one model. A static hosts.yml, an aws_ec2.yml cloud query, and a constructed.yml can all live side by side, and the play that follows sees one combined set of hosts and groups.
hosts.yml — the long-lived Larkspur boxes, written by hand.aws_ec2.yml — autoscaled instances discovered at run time.constructed.yml — derived groups computed over the two above, then all merged into one model.This is how static and dynamic hosts coexist without ceremony. The long-lived Larkspur boxes stay in a hand-written file; the autoscaled instances come from the cloud plugin; both are just hosts by the time a play targets them. The layout below is a single inventory directory holding three sources.
inventories/prod/ hosts.yml # static: the long-lived Larkspur boxes aws_ec2.yml # dynamic: autoscaled instances from the cloud constructed.yml # composed: derived groups over the above
The constructed Plugin
The constructed plugin builds groups and variables from Jinja2 expressions over the host facts and variables that already exist. It does not query anything — it composes. You can create a webservers_in_prod group from hosts that are in both web and prod without that combined distinction existing in any underlying source.
That is composition rather than duplication: the membership is computed from data the hosts already carry, so it stays correct as the underlying groups change. The config below derives a group for the production web tier from the env and tier variables each host already has.
plugin: constructed groups: # membership computed from existing host vars webservers_in_prod: tier == 'web' and env == 'prod'
Caching
Cloud inventory plugins can cache their results so every playbook run does not re-hit the cloud API. The trade is freshness for speed: a cached inventory is faster to load but may not reflect an instance that launched seconds ago. You control it with a cache timeout set to match how fast the fleet actually churns.
A fast-scaling cloud wants a short timeout so the cache stays close to reality; a stable estate can cache for much longer and save the API calls. The config below enables caching with a timeout chosen for a moderately churning fleet.
plugin: amazon.aws.aws_ec2 cache: true cache_plugin: jsonfile cache_timeout: 300 # seconds; tune to the fleet's churn
Merge Order and Precedence
When several sources define the same host or the same variable, load order and group-variable precedence decide the winner. A value set statically can be overridden by a later cloud source, or the reverse, depending on the order Ansible reads the directory and the precedence rules between group levels.
That outcome is predictable only if you know the rules, which is exactly why ansible-inventory --graph exists. Run it to see the resolved model — every host, every group, every winning value — rather than reasoning about merge order in your head. The command below prints the combined result of the whole directory.
# the merged result of every source in the directory ansible-inventory -i inventories/prod/ --graph
- Writing or maintaining a custom inventory script for something a plugin already does, taking on Python maintenance the plugin model was built to remove.
- Combining static and dynamic sources in a directory without understanding merge order, so a variable you set statically is silently overridden by the cloud source — or the reverse — and the resolved value surprises you.
- Setting an aggressive inventory cache and then debugging a "missing host" that the cache simply has not refreshed yet — a stale cache looks exactly like a plugin bug until you remember it exists.
- Recreating derived groupings by hand across runs instead of using
constructedto compute them once from existing facts, so the duplicated membership drifts out of sync with its source. - Reasoning about multi-source precedence in your head instead of running
--graph, then acting on a model that does not match what Ansible actually resolved.
- Prefer inventory plugins over scripts, and reach for a directory of sources to combine a static core with cloud-discovered hosts in one model.
- Use the
constructedplugin to derive composite groups like the production web tier from existing data, instead of duplicating membership that then has to be kept in sync. - Tune inventory caching to the fleet's churn — short for fast-scaling clouds, longer for stable estates — and remember the cache first whenever a host seems to be missing.
- Always confirm the merged result with
ansible-inventory --graphor--list, since multi-source precedence is only obvious once you have seen the resolved model. - Keep each source doing one job — static for the fixed core, a cloud plugin for the dynamic hosts,
constructedfor derived groups — so the directory stays legible as it grows.
Knowledge Check
Why did inventory plugins replace the legacy scripts?
- Plugins are YAML-configured, run in-process, and are cache-aware, removing the Python maintenance that executable scripts required
- Scripts could only emit a flat list of hosts and had no way to produce groups at all
- Plugins run out on each managed node rather than on the control node, spreading the inventory resolution load across the whole fleet
- Scripts were dropped because they had no way to query a cloud provider's API for hosts at all
What does the constructed plugin compose, and why?
- Groups and variables computed from existing host facts via Jinja2, so a derived group like the prod web tier needs no duplicated membership
- A fresh query against the cloud provider to discover hosts that no other inventory source managed to find
- An encrypted bundle of every inventory secret, gathered into one file for safe long-term storage
- A single merged cache file that overrides and then completely replaces every one of the other sources listed in the inventory directory on disk
What is the trade-off in inventory caching?
- Freshness for speed — a cache avoids re-hitting the API but may miss an instance launched seconds ago, so you tune the timeout to the churn
- Security for speed — a cache writes the cloud provider credentials straight to local disk in plaintext so that later runs load that much faster
- Accuracy for groups — caching keeps the host entries but quietly drops their group membership to save space
- There is no real trade-off here; a cache is always strictly better than re-querying the API every run
How do multiple sources in a directory combine?
- Ansible runs every source and merges them into one model, with load order and group precedence deciding conflicts — confirmed with
--graph - Ansible reads only the first source in load order and silently ignores all of the rest
- Each source becomes a separate inventory that you must pick one at a time with a flag per run
- The sources are concatenated as raw text before parsing, so a host that is listed in two of them ends up appearing twice in the final model
You got correct