<- Back to BlogDevOps

Ansible Inventory Files Explained: INI, YAML, Groups, and Variables (2026)

Need an inventory file without hand-editing groups?

Use the Ansible Inventory Generator to create INI or YAML structure, then validate connectivity and variables with Ansible itself before a playbook run. This guide covers both formats, host patterns, variable precedence, dynamic sources, and how inventory connects to real runs.

An Ansible inventory answers a practical question: which machines belong to which groups, and how should Ansible connect to them? It is not merely a list of IP addresses. Inventory structure controls targeting, group inheritance, connection variables, and the names a playbook can use. A readable inventory reduces the risk of deploying to the wrong environment.

1. What an inventory describes

An inventory contains hosts and groups. A host can be named by DNS name, IP address, or an alias with connection variables. Groups let a play target a role such as web, database, staging, or production. Child groups create larger logical sets, while host and group variables supply connection or application settings.

Inventory conceptExample purpose
HostOne machine or connection alias
GroupTarget all web servers
Child groupCombine web and worker groups
Host variableOverride a port or interpreter
Group variableSet shared environment values

Group names are labels used by playbooks. Use stable names that describe responsibility and environment, not temporary machine names. Avoid ambiguous labels such as new or server1 once the inventory becomes shared.

It helps to think of the inventory as the answer to one question: what do I manage? A good inventory states which machines exist, how each one is reached, and which slice of configuration applies to it. Everything else in Ansible hangs off that map — plays select from it, roles parameterize from it, and templates read it. When the map is wrong or vague, every layer below inherits the confusion, which is why inventory review deserves the same care as application code review.

2. INI inventory syntax

[web]
+web-1 ansible_host=203.0.113.10
+web-2 ansible_host=203.0.113.11
+
+[db]
+db-1 ansible_host=203.0.113.20
+
+[production:children]
+web
+db

INI is compact and easy to scan for simple host lists. Key-value pairs after a host define host variables. Group relationships use special sections such as :children, and group variables use :vars. Keep complex nested data out of inline host declarations when it becomes difficult to review.

[db:vars]
ansible_python_interpreter=/usr/bin/python3
db_port=5432

[loadbalancers]
lb-1 ansible_host=203.0.113.30 http_port=443
lb-2 ansible_host=203.0.113.31 http_port=443

Host variables are written directly after the host name as key=value pairs; group-wide values live in a :vars section. Values are strings unless Ansible converts them, so a port written as 443 and a port written as "443" should be understood the same way by every module that reads it — quoting consistently avoids the surprise.

3. YAML inventory syntax

all:
  children:
    web:
      hosts:
        web-1:
          ansible_host: 203.0.113.10
    production:
      children:
        web:

YAML expresses nesting explicitly and is often easier to extend with group variables and structured data. It is also sensitive to indentation, duplicate keys, and scalar types. A value such as an unquoted number may be parsed differently from a string that a module expects, so validate the result with Ansible. Before the file even reaches Ansible, a lint pass with the YAML Validator catches duplicate keys and indentation slips, and YAML to JSON Converter shows you the exact structure the parser will build — useful when a nested value silently became a string.

4. Variables and precedence

Ansible has several variable sources, including inventory host variables, group variables, play variables, role defaults, and extra variables. When the same name appears in several places, precedence decides the winner. Avoid creating a maze of overrides. Put connection facts near inventory, role configuration near the role, and secrets in a vault or external secret manager.

Connection variables are common: ansible_user, ansible_port, ansible_ssh_private_key_file, and ansible_python_interpreter. A private key path is not the private key itself, but it still reveals a local filesystem assumption. Do not put private key contents or passwords in a public inventory.

Use group variables for shared non-secret settings. For credentials, use Ansible Vault or an approved secret backend. An inventory generator can create the shape, but it cannot decide which access policy is appropriate for a production fleet.

Three rules of thumb keep precedence manageable. The most specific source wins: a host's own variables beat the groups it belongs to, and a deeper child group beats its parent. Extra variables passed with -e always win, which makes that flag right for a one-off override and wrong for permanent configuration. And if remembering where a value comes from takes a table, move the value: connection details belong in inventory, role behavior belongs in role defaults, and secrets belong in a vault — detailed file placement is covered in section 9.

5. A safe workflow

  1. Create separate inventories for development, staging, and production, or use clearly isolated groups.
  2. Generate a first draft with the Ansible Inventory Generator.
  3. Inspect every host and connection variable before saving.
  4. Run ansible-inventory -i inventory.yml --graph to inspect group membership.
  5. Run ansible-inventory -i inventory.yml --host web-1 to inspect resolved host data.
  6. Test with a read-only play or ansible all -m ping before a change playbook.
  7. Keep secrets outside the inventory and review changes like code.

For SSH details, pair the inventory with the SSH Config Generator. For service configuration, keep generated systemd or Nginx files separate from inventory so each tool has one clear responsibility.

6. Common inventory errors

  • Wrong file format: Ansible may parse an INI-like file differently from YAML.
  • Targeting the wrong group: a play pattern can match more hosts than expected.
  • Duplicate YAML keys: the last value may win silently depending on the parser.
  • Bad indentation: a host becomes a sibling of a group rather than a child.
  • Connection mismatch: DNS, SSH user, port, key, and Python interpreter must all agree.
  • Secret in source control: rotate exposed credentials and move them to a protected store.

Before production, print the graph, verify the selected group, and run a harmless command against one known host. A successful parser run proves syntax, not that the target is the machine you intended.

7. INI vs YAML, side by side

Both formats describe the same data model, so the choice is really about what stays readable as the fleet and the variable set grow. The same small fleet in each format:

# inventory.ini
[web]
web-1 ansible_host=203.0.113.10 http_port=80
web-2 ansible_host=203.0.113.11 http_port=80

[web:vars]
env=staging

[production:children]
web
# inventory.yml
all:
  children:
    web:
      vars:
        env: staging
      hosts:
        web-1:
          ansible_host: 203.0.113.10
          http_port: 80
        web-2:
          ansible_host: 203.0.113.11
          http_port: 80
    production:
      children:
        web:
ConcernINIYAML
StructureFlat sections such as [web]Nested maps under all:
Host variablesInline pairs after the host nameIndented keys beneath the host
Group inheritance[group:children] sectionchildren: key
Structured valuesStrings only; lists and maps are awkwardNative lists and dictionaries
CommentsYes, with #Yes, with #
Usual editing riskA typo in a section name creates a new groupWrong indentation silently moves a host
Best forSmall, flat estates that rarely change shapeGroups of groups, rich variables, shared reviews

The practical rule: start in INI while the inventory is a list of hosts with scalar variables, and move to YAML the moment you need parent groups beyond one level, nested values, or variables long enough to deserve comments. Migration is mechanical — the structure is identical, only the spelling changes — and running the result through ansible-inventory --graph proves nothing was lost.

8. Host patterns: targeting exactly the right machines

Patterns appear wherever Ansible selects hosts: the hosts: line of a play, the --limit flag of a run, and the target of an ad-hoc command. They form a small expression language over group and host names:

ansible all -m ping                        # every host, grouped or not
ansible webservers -m ping                 # one group
ansible 'dbservers:!prod-db' -m ping       # group minus one host
ansible 'webservers:&dbservers' -m ping    # hosts in both groups
ansible 'webservers dbservers' -m ping     # union of both groups
ansible 'web*' -m ping                     # wildcard on names
ansible 'web[1:10]' -m ping                # numeric range
ansible 'webservers:!staging' --limit web-1  # --limit intersects further
OperatorMeaningExample
:, space, or ,Union of the two sideswebservers:dbservers
&Intersection, hosts in bothprod:&debian
!Exclusionall:!localhost
*Wildcard on host and group namesweb-*
[x:y]Inclusive numeric range after a prefixweb[1:10]

Four habits prevent the classic targeting accidents. Quote any pattern containing : or ! so the shell does not reinterpret it. Remember that patterns match inventory names, not addresses — a host reached by IP still selects by its alias. Expect an error when a pattern matches nothing, which is a useful warning rather than an inconvenience. And preview before you act: ansible-inventory -i inventory.yml --graph --vars prints the same tree the run will use, so a wrong pattern is visible before a single module executes.

9. Groups, children, and variable files on disk

Inline variables work until the inventory carries real configuration. The standard layout moves them into files beside the inventory, where Ansible discovers them automatically by matching filenames to group and host names:

inventory/
  inventory.yml          # hosts and groups only
  group_vars/
    all.yml              # applies to every host
    web.yml              # applies to the web group
    production.yml       # applies to the production group
  host_vars/
    db-1.yml             # applies to one host

Two implicit groups are always present. all contains every host in the inventory, and ungrouped contains hosts that belong to no explicit group — both are reachable in patterns, so all:!ungrouped is a precise way to target only the hosts someone actually organized. A host may belong to several groups at once, and its effective variables are a merge: start from group_vars/all, layer each group the host belongs to with more specific groups overriding general ones, then apply host_vars for that host last.

Order (lowest wins least)Source
1Role defaults
2Inventory group variables (group_vars/all, then parent groups)
3Inventory host variables (host_vars/)
4Playbook group_vars/ and host_vars/
5Role variables and task-level vars
6Extra variables passed with -e, always the highest

Parent groups in turn are declared once and inherited everywhere: [staging:children] listing web and db means variables set on staging reach every member, while members keep their own overrides. Organizing this way keeps one question answerable at a glance — which settings apply to this host, and from where — instead of buried inside a playbook.

10. Dynamic inventory and plugins

Static files answer for a fleet you can enumerate. When hosts appear and disappear — cloud instances, autoscaling groups, a CMDB — Ansible can query the source at run time. Anything that answers two questions works as an inventory source: what are the groups and hosts, and what variables does each host carry? The answer is a single JSON document:

{
  "_meta": {
    "hostvars": {
      "web-1.example.com": {
        "ansible_host": "10.0.0.11",
        "instance_type": "t3.medium"
      }
    }
  },
  "webservers": { "hosts": ["web-1.example.com"] },
  "production": { "children": ["webservers"] }
}

Two forms produce that document. A dynamic inventory script is any executable — Python, bash, anything — that accepts --list for the whole topology and --host plus a name for a single host; putting everything under _meta.hostvars makes the per-host calls unnecessary. A dynamic inventory plugin is the packaged version of the same idea: shipped in Ansible collections, configured by a small YAML file, usually cached, and selected by filename — an inventory path ending in .yml that names the plugin on its first line.

Plugins cover the common clouds and structured sources, plus constructed inventories that reshape host lists with filters such as splitting machines by availability zone. Use a dynamic source when the API is genuinely the source of truth; keep a static file when the estate is fixed, when the runner should not hold cloud credentials, or when humans reviewing an inventory diff are part of your safety story. Mixing is normal — a directory passed with -i can hold a static file and a plugin side by side, and Ansible merges the results.

11. Connecting inventory to playbooks

The inventory reaches a playbook through one flag:

ansible-playbook -i inventory/production.yml site.yml
ansible-playbook -i inventory/ site.yml              # merge every file inside
ANSIBLE_INVENTORY=./inventory/staging.yml ansible-playbook site.yml
ansible all -m ping -i inventory/development.yml     # ad-hoc uses the same flag

-i accepts a file, a directory (all files inside are combined), or a script and plugin source. When it is omitted, resolution follows a fixed order: the ANSIBLE_INVENTORY environment variable, then the inventory entry under [defaults] in ansible.cfg, then the classic /etc/ansible/hosts. ansible-inventory --list resolves the same way, so debug with the command line you intend to run rather than an idealized one.

Inside playbooks and templates the inventory becomes data. The key each host uses to name itself is {{ inventory_hostname }}, and {{ group_names }} is the list of groups it belongs to, which makes conditional configuration a one-liner:

# templates/site.conf.j2
server {
  server_name {{ inventory_hostname }};
  {% if 'production' in group_names %}
  error_page 500 /ops.html;
  {% endif %}
}

The same pattern writes per-host entries in /etc/hosts, picks a listen port from a group variable, or selects a package mirror by environment. Treat group names as a shared API: renaming one silently breaks every play, template, and role that referenced it. Generated service and server definitions belong next to the inventory rather than inside it — the systemd unit file guide and the Nginx and systemd configuration guide show where those files live.

Multi-environment structure comes down to one decision: groups when you want a single source of truth, separate inventory files when you want a wall between environments.

ConcernGroups in one inventorySeparate inventories
Shared structureDefined once, reused through child groupsRepeated in every file
Targetinghosts: production or a patternChoose the file with -i
Cross-environment mistakesA loose pattern can match two environments at onceOnly one file is ever loaded
CredentialsOne runner must hold every environment's secretsEach environment keeps its own runner and secrets
Fixes and driftA correction applies everywhere immediatelySame playbook, variable files that must stay aligned

The pragmatic middle is one inventory with the environment as a top-level group, plus the habit of printing ansible-inventory --graph before every run. Reach for separate files when production credentials must not exist on the machine that runs staging jobs, or when environments have different owners. Patterns slice within one source of truth; separate inventories are separate sources of truth — identify which problem you have before choosing. Either way, gate changes with ansible-playbook --syntax-check and a --check dry run, ideally from CI as described in the GitHub Actions CI/CD guide, and lint YAML early with the YAML Validator.

Conclusion

Ansible inventory is an executable map of infrastructure. Use stable groups, explicit environments, deliberate variable placement, and validation commands before changes. Generate boilerplate locally, but let Ansible and a human review determine whether the inventory is safe to run.

Frequently Asked Questions

What is an Ansible inventory?
It describes hosts, groups, relationships, and variables that Ansible uses to target machines.
Should I use INI or YAML?
INI is compact for simple lists; YAML is easier to extend with nested groups and structured variables.
Where should secrets go?
Use Ansible Vault or an approved secret backend rather than putting passwords or private key contents in inventory.
How do I inspect group membership?
Run ansible-inventory with the --graph option against the exact inventory file.
Can a generator validate connectivity?
No. It can create structure; Ansible must validate parsing, connection, and permissions.
How do I avoid deploying to the wrong environment?
Separate environments, use explicit group names, print the graph, and test a known host with a read-only command first.
When should I split inventories instead of using groups?
Split them when environments need different credentials, networks, or owners; use groups plus patterns when you want one source of truth and careful targeting.
What is a dynamic inventory?
A script or plugin that queries a live source such as a cloud API and returns the standard groups-and-hostvars JSON document that Ansible expects.