Skip to content
SRE & DevOpsDeep Dive Published Updated 11 min readViews unavailable

Ansible Inventory in Production: Host Selection, Dynamic Sources, and Precedence

Design Ansible inventories with composable groups, predictable source order, deliberate variable scope, dynamic inventory checks, and safe host targeting.

Ansible inventory is more than a list of machine names. It defines which systems exist for an automation run, how they are grouped, where Ansible connects, and which inventory variables become inputs to the play. A playbook can be syntactically correct and still cause an outage if a production host is assigned to the wrong group, a later source overrides a variable, or a host pattern selects more machines than its author intended.

A production inventory should make host identity, topology, environment, and variable ownership easy to inspect. It should also make the final target set repeatable. Treat inventory sources and their variable directories as versioned configuration, review dynamic inventory refreshes, and verify the compiled hosts and variables before any task that changes a system.

Model groups as overlapping dimensions

A host can belong to several groups at once. That allows an operator to select systems by role, environment, region, or maintenance state without cloning a host entry for every combination. Use group names that represent stable operational dimensions and combine them for targeting. Avoid maintaining many nearly identical groups whose names encode every possible combination of those dimensions.

A small YAML inventory can express those relationships explicitly:

all:
  children:
    production:
      children:
        webservers:
          hosts:
            web-01.example.net:
              ansible_host: 10.20.4.11
            web-02.example.net:
              ansible_host: 10.20.4.12
        databases:
          hosts:
            db-01.example.net:
              ansible_host: 10.20.8.21
    staging:
      children:
        webservers_staging:
          hosts:
            web-stg-01.example.net:
              ansible_host: 10.30.4.11

This example separates the host’s inventory identity from its network address. The name in hosts is the Ansible inventory hostname; ansible_host tells the connection plugin where to connect. That distinction is useful when DNS labels, cloud instance names, and stable operational identities differ. Keep the naming scheme consistent, and do not duplicate one physical host under different inventory aliases unless there is a deliberate reason and the execution consequences are understood.

Groups may have multiple parents and children, but circular relationships are invalid. A host in a child group is automatically a member of each parent. Ansible still runs a host once per play even if it belongs to multiple groups; it merges the applicable data into that host’s variables. This makes group structure powerful, but it also means that overlapping groups can introduce variable collisions.

For example, group hosts by a deployment role and by an environment, then target the intersection rather than creating a separate production_webservers copy of every machine. Use parent groups for common properties, such as a production package mirror or a regional connection policy, and keep host-specific exceptions in a narrow host variable only when the exception is real and documented.

Keep variable ownership predictable

Ansible flattens inventory variables to each host before running a play. The inventory-specific precedence is generally: all, parent groups, child groups, then host variables. A host-specific value overrides the group value; a child group’s value overrides its parent. At the same parent/child level, groups are merged alphabetically by default, and the last group loaded wins when variable names conflict.

This is separate from the larger playbook variable-precedence system. Play variables, role variables, task variables, registered values, and especially --extra-vars can override inventory values. A deployment operator who passes -e app_version=... may override the inventory value even when the inventory appears correct. Keep each important value in one agreed source wherever possible. Use role defaults for overridable defaults, inventory for environment-specific host facts, and explicit role parameters or play inputs when a deployment intentionally needs a different release value.

Do not use precedence as an implicit merge framework. Two unrelated groups should not define a variable with different meanings and rely on alphabetical order to select one. Use namespaced variables such as web_package_channel and database_backup_window, or define a clear precedence convention for the few shared keys that legitimately vary by environment. If a group-level order exception is unavoidable, ansible_group_priority can change same-level merge priority, but it must be set in the inventory source itself, not in group_vars.

For example, a parent group can define common behavior while its children override only documented environment differences:

all:
  vars:
    monitoring_endpoint: https://monitoring.example.net
  children:
    production:
      vars:
        app_channel: stable
      children:
        webservers:
          hosts:
            web-01.example.net:
              ansible_host: 10.20.4.11
    staging:
      vars:
        app_channel: candidate
      children:
        webservers_staging:
          hosts:
            web-stg-01.example.net:
              ansible_host: 10.30.4.11

This small example keeps the release channel aligned with the environment. In a larger repository, place most structured variables in group_vars/ and host_vars/ files instead of embedding complex structures in the main host list. Keep the directory structure next to the inventory source or playbook according to the host_group_vars plugin’s search behavior, and use YAML where lists, dictionaries, booleans, and numbers should keep their types.

Understand source order and directory loading

Ansible can load multiple static and dynamic inventory sources. It processes sources in the order supplied on the command line; when the same host or variable appears more than once, later definitions can add information or overwrite conflicting values. When an inventory directory is used, Ansible loads eligible source files in alphabetical order. Prefix filenames when order is part of the intended merge, and make that order obvious in review:

inventory/production/
  01-cloud.yml
  02-region-overrides.yml
  03-static-exceptions.yml
  group_vars/
    all.yml
    production.yml
  host_vars/
    web-01.example.net.yml

This layout is illustrative, not a requirement that every inventory have three sources. Use the fewest sources that keep ownership clear. If two files assign different values to the same host variable, document which source is authoritative and why the other value exists. Do not rely on filesystem ordering that changes across deployment controllers or on an operator remembering which -i argument came last.

For frequently changing cloud infrastructure, prefer supported inventory plugins over legacy inventory scripts. Plugins integrate with current Ansible inventory behavior and can expose provider metadata as groups or host variables. Pin the collection and plugin version used by the automation environment, and validate permissions, filters, tags, and query behavior against the actual cloud account. A dynamic source is only as accurate as its refresh and caching settings; stale results can target a terminated host or miss a newly created one.

When combining a dynamic source with a static exception source, inspect the merged host list. Keep exceptional hosts clearly identified, and avoid defining a host under multiple names merely to attach different variables. If an inventory plugin uses cache, decide how and when it is refreshed, especially before a change that can destroy, replace, or migrate hosts.

Use host patterns as a change-control boundary

The hosts: field in a playbook is an Ansible pattern. Patterns can select a host or group, form a union, require an intersection, or exclude a subset. A command-line --limit further restricts the playbook target set. Learn the syntax and inspect the result instead of treating a pattern as a descriptive comment.

For example, a rollout may target production web servers while excluding a maintenance group:

ansible-playbook deploy.yml \
  --inventory inventory/production \
  --limit 'webservers:&production:!maintenance' \
  --list-hosts

The exact pattern must match the groups in the compiled inventory. Here webservers selects the role group, &production keeps only its production members, and !maintenance excludes hosts in that group. Use a quoted argument so the shell does not interpret special characters. A typo or an empty intersection may produce a warning or an unexpected empty selection, so verify the displayed host set before executing state-changing tasks.

Ansible’s ansible-inventory --graph is useful for inspecting group relationships, and --host or --list can show the inventory variables Ansible has loaded. These are inventory-inspection tools, not a replacement for checking a particular play’s hosts: pattern and --limit. The ansible-inventory graph and host display options ignore limit behavior; use ansible-playbook --list-hosts with the exact inventory and limit to inspect a real play target set.

Inventory commands can display sensitive variables. Do not paste an unredacted --list or --host result into a ticket, chat, or CI log if it contains credentials, tokens, internal addresses, or encrypted values that are being decrypted for the run. Restrict access to deployment output and use encrypted variable mechanisms for secrets instead of treating a private inventory repository as a secret store.

Organize group_vars and host_vars deliberately

The default host_group_vars vars plugin can load YAML, JSON, or extensionless files under group_vars/ and host_vars/. Files and directories are searched relative to the inventory source and playbook directory, and the exact context matters for commands that do not run through ansible-playbook. If both locations provide the same variable, understand which one wins rather than assuming the nearest-looking file is the only source.

For example:

inventory/production/
  hosts.yml
  group_vars/
    all.yml
    production.yml
    webservers/
      packages.yml
      monitoring.yml
  host_vars/
    web-01.example.net.yml

Use the parent directory for environment-wide values, a group-specific directory for a role’s structured settings, and a host_vars file only for a narrow exception. Ansible reads files in group or host variable directories in lexicographical order. Split a large file by domain, but avoid creating arbitrary numbered fragments unless their load order is intentional and documented.

Do not assume INI host-line values preserve YAML types. INI inventory uses parsing rules that can treat host variables differently from YAML; Ansible recommends YAML when type consistency matters. A string such as false, a list-like value, or a numeric port can become an unexpected type if it is parsed differently than the author expects. Verify complex values with ansible-inventory and use YAML inventory or variable files for booleans, lists, and dictionaries.

Validate the compiled inventory before a run

Review the inventory that Ansible actually sees, not just the source file you edited. A useful preflight for a specific environment can include:

ansible-inventory --inventory inventory/production --graph --vars
ansible-inventory --inventory inventory/production --host web-01.example.net
ansible-playbook deploy.yml \
  --inventory inventory/production \
  --limit 'webservers:&production:!maintenance' \
  --list-hosts

The graph helps find unexpected parent/child relationships, and the host view helps detect variable conflicts. The playbook preflight verifies the actual pattern and limit for the selected play. For a deployment that depends on one value such as application version, inspect the effective host variable and where it came from before changing systems. Be careful to redact any secrets from diagnostic output.

For dynamic inventory, validate plugin configuration and authentication using read-only discovery first. Check that expected groups and hosts exist, that test or deleted instances are filtered intentionally, that hostnames are stable, and that the cache reflects the current source. Where a provider API is rate-limited, bound queries and cache lifetime deliberately instead of triggering repeated full account scans for every operator command.

Treat a zero-host result as a failed precondition for a production change unless an empty run is explicitly expected. Treat an unexpectedly broad result as a stop condition. CI can assert a non-empty but bounded selected host count before launching a rollout, and a human review can compare the list with the change’s intended region, environment, and role.

Keep the inventory as a reviewable interface

Version-control static inventory sources and their associated variable directories together. For dynamic inventory, version the plugin configuration, collection requirements, filters, and any static overlays. Record the source of truth for host lifecycle so that a manually added exception is not silently overwritten by cloud discovery or a configuration-management controller.

Design inventory variables as an interface between the inventory owner and the roles that consume them. Use clear names, documented types, and safe defaults. Avoid defining the same operational parameter in inventory, play vars, role vars, and --extra-vars unless the precedence is intentional. Each extra place to override a value increases the number of ways two apparently identical automation runs can behave differently.

Keep credentials out of plain inventory wherever possible. Use an encrypted secret store or Ansible Vault for values that automation must consume, scope access to inventory and rendered output, and avoid emitting secrets through debug tasks. This is separate from host selection: a correct group pattern does not make the variables attached to those hosts safe to disclose.

Production inventory checklist

  • Every host has one stable inventory identity and a deliberate connection address.
  • Groups represent clear dimensions such as environment, role, or region and avoid duplicate host aliases.
  • Parent/child relationships and overlapping membership are visible and free of cycles.
  • Each important variable has one documented source; conflicts do not depend on accidental alphabetical order.
  • YAML is used where data types matter, and variable files are organized by owner and domain.
  • Multiple inventory sources have explicit, reviewed load order, including alphabetically loaded directory sources.
  • Dynamic inventory filters, caches, plugin versions, and credentials are tested against the intended account.
  • Host patterns and --limit are reviewed with the exact playbook target list before changes.
  • Inventory output containing secrets or internal topology is protected and redacted.
  • CI or release checks stop on empty, stale, or unexpectedly broad production host sets.

An inventory is production-ready when the operator can answer three questions before running a playbook: which exact systems are selected, which variables each system will receive, and which source owns each value. Make those answers inspectable, and inventory becomes a reliable change-control boundary instead of an invisible source of deployment drift.

Related:

Sources:

Comments