Inventory ๐๏ธยถ
How an Ansible inventory is assembled from the per-role meta/services.yml declarations, what gets resolved at build time, and what stays as an unrendered Jinja template until play runtime.
For general documentation rules such as links, writing style, RFC 2119 keywords, and Sphinx behavior, see documentation.md.
For the matrix-variant folder-per-round model that wraps this pipeline, see variants.md. For the on-disk shape of a roleโs metadata see layout.md.
Pipeline overview ๐ค๏ธยถ
The build pipeline runs entirely on the operatorโs host, before any Ansible play starts. Three stages produce the inventory file plus the matching host_vars/<host>.yml:
Stage |
Code path |
Output |
|---|---|---|
1. Variant resolution |
applications.py (cache) |
Per-app payload = |
2. Group materialisation |
cli.administration.inventory.devices, filters.py |
Inventory file lists one group per included |
3. Host-vars baking |
cli.administration.inventory.provision |
|
After stage 3 the inventory is fully variant-resolved on disk. Ansible loads it as plain YAML, no rendering yet.
What decides which roles enter the inventory ๐ฏยถ
The include set passed to stage 2 is built per round by the matrix planner: deploy/cli.py and init.py call plan_dev_inventory_matrix in planner.py, which resolves each roundโs include via _resolve_round_include in legacy_resolver.py using the CombinedResolver with follow_run_after=False. The swarm test path computes its include separately via derive_includes.py, reading the active variant map from INFINITO_APP_VARIANTS. Both expand the selection along two transitive paths:
Ansible meta
dependencies:from each roleโsmeta/main.yml.Shared-service auto-include from each roleโs
meta/services.yml.<entity>.<service>block, when bothenabledANDsharedpassis_explicit_truthin registry.py: the literalTrueor the dynamic form"{{ '<role>' in group_names }}". A literalfalse(as baked by a variant override) does NOT qualify.
run_after: from meta/services.yml.<entity>.run_after is an ordering hint only (see base.md); it MUST NOT pull roles into the include set.
This is the mechanism that lets variants control whether a shared service is pulled in: a variant override pins services.<service>.{enabled,shared} to a literal boolean; the roundโs include resolution reads the variant-merged services map; the provider roleโs group materialises in (or drops out of) the inventory accordingly.
What lives in the inventory file vs. host_vars ๐ยถ
Artifact |
Contents |
|---|---|
|
Group-per-application map plus host membership. Source of truth for |
|
Connection vars, the variant-baked |
host_vars/<host>.yml is where variant data ends up after the deep-merge. The strings inside are still raw, including any Jinja templates from meta/services.yml that no variant override replaced.
When Jinja gets rendered โฑ๏ธยถ
Rendering happens at play runtime, never during the build pipeline above. The render call is centralised in applications.py (cache) get_merged_applications, which:
Loads variant 0 as the default application payload.
Layers on inventory
variables['applications'](the host_vars block) as overrides via deep merge.Calls
_render_with_templarwith the playโs full variable scope, includinggroup_names,inventory_hostname, and any other facts.Returns the fully rendered payload.
get_merged_applications is invoked transitively by lookup('applications', ...) and by lookup('config', application_id, 'services.<x>.<y>'). Tasks that read service flags through these lookups always see real Python values, never raw Jinja strings.
The render runs once per play. The constructor stage parks the payload and its cache key in the host fact _INFINITO_APPLICATIONS_RENDERED, and every taskโs worker process (Ansible forks one per task) serves that payload instead of rendering again. See applications.md for the carrier contract.
A roleโs task that reads its own service flag therefore picks up the runtime-resolved value, even when the inventory carried a Jinja string for that flag: the conditional evaluates against the playโs actual group_names, which reflects which other application groups the host joined.
Interaction between the three inclusion mechanisms ๐งฌยถ
A role can be pulled into the inventory through any of three paths and they compose, not exclude one another:
Path |
When it fires |
|---|---|
Operator-supplied |
Always. The named roles enter the include set verbatim. |
|
When a role in the include set declares |
Shared-service auto-include |
When a role in the include set declares |
The matrix-variant planner (see variants.md) calls the expansion once per round, so each roundโs inventory gets the right transitive closure for that roundโs variant set.
Worked example: WordPress and Discourse ๐งชยถ
The wp-discourse plugin install in roles/web-app-wordpress/tasks/addons/wp-discourse.yml does container exec discourse rake api_key:create_master[...] during the WordPress play. The Discourse container therefore has to exist by the time WordPress runs.
The wiring that makes this work is purely in the services declaration, not in run_after:
roles/web-app-wordpress/meta/services.yml declares
services.discourse.shared: trueplus a fallbackenabled: "{{ 'web-app-discourse' in group_names }}".roles/web-app-wordpress/meta/variants.yml variant 0 (canonical Single-Site) overrides
services.discourse.enabled: true. Variant 1 (Multisite) overrides it tofalse.The variant override is baked as a real boolean into the inventory, so the shared-service auto-include picks it up at build time. Variant 0 therefore pulls
web-app-discourseinto the include set. Variant 1 does not.Discourseโs role itself has no
run_after, so deploying just--apps web-app-discoursedoes not drag WordPress along.
The fallback Jinja string in the base meta/services.yml is the safety net for the case where neither variant overrides the flag and the operator still adds web-app-discourse to the inventory groups manually: the runtime lookup('config', application_id, 'services.discourse.enabled') then renders the conditional against the real group_names and the wp-discourse plugin install enables itself accordingly.
Reference files ๐ยถ
File |
Purpose |
|---|---|
|
|
|
|
|
|
Package root re-exporting the public API. Per-submodule split: |
|
Stage 2 group materialisation: emits one group per invokable |
|
|
|
Connection vars, vars-file/JSON overlays, become-password handling. |
|
|
|
|