Chapter 2: Inventory
Topic 08

Static Inventory — INI and YAML

InventoryConfig

The simplest inventory is a file you write by hand, and Ansible accepts two formats for it: the terse INI style and the more structured YAML style. Both describe the same model — hosts, groups, children, and variables — so the choice between them is taste, not capability.

That parity holds until your fleet grows nesting and structured variables. A flat handful of hosts reads fine in INI; a deep hierarchy of groups with maps of settings starts to fight the INI format, and YAML's explicit structure begins to pay for its extra lines. The early Larkspur inventory below works either way, and the topic shows both.

INI vs YAML — same model, different fit
INI
Terse bracketed headers like [web] with one host per line. Reads at a glance for a small, flat fleet.
YAML
Nested tree rooted at all, structure spelled out. Wins once you have deep children or map-valued variables.

INI Format

The INI format writes a group as a bracketed header with one host per line beneath it. Group-of-groups uses a [name:children] section listing child group names, and group variables go in a [name:vars] section. It is compact and immediately readable for a small flat fleet like early Larkspur, where you can see the whole thing at a glance.

The inventory below defines the two Larkspur tiers, joins them under a larkspur parent, and sets a connection user for the whole estate. Every host in web and db is also a member of larkspur through the children section, so a variable set there reaches all of them.

inventory.ini — the early Larkspur fleet in INI format
# two role groups, joined under a larkspur parent
[web]
web1.larkspur.io
web2.larkspur.io

[db]
db1.larkspur.io

[larkspur:children]
web
db

[larkspur:vars]
ansible_user=deploy

YAML Format

The YAML format describes the same model as a nested tree rooted at all, with children groups and hosts maps spelled out at each level. It is more verbose than INI for a small fleet, but the structure is explicit rather than implied, and it is the only sane choice once you have deep group hierarchies or variable values that are themselves maps and lists.

The same Larkspur fleet in YAML makes the nesting visible: larkspur is a child of all, and web and db are children of larkspur. The shape on the page mirrors the shape of the model, which is what makes YAML hold up as the hierarchy deepens.

inventory.yml — the same fleet in YAML format
all:
  children:
    larkspur:
      vars:
        ansible_user: deploy
      children:
        web:
          hosts:
            web1.larkspur.io:
            web2.larkspur.io:
        db:
          hosts:
            db1.larkspur.io:

Host Patterns and Ranges

Both formats support numeric and alphabetic ranges so you do not list near-identical hosts one at a time. web[1:2].larkspur.io expands to web1.larkspur.io and web2.larkspur.io; a wider tier becomes web[1:8].larkspur.io on a single line. The range keeps a growing homogeneous tier from turning into a wall of near-duplicate entries that nobody wants to edit.

In INI the range is the whole point of a one-line group definition. The collapsed form below is the same web group as before, written once and trivially extended — bumping the tier to ten hosts is a single character change rather than eight new lines.

A host range collapses a homogeneous tier to one line
[web]
web[1:2].larkspur.io

[db]
db1.larkspur.io

Connection Variables Inline

Connection settings can be set right in the inventory, per host or per group: ansible_host overrides the address Ansible connects to, ansible_user sets the SSH user, ansible_port the port, and ansible_python_interpreter the path to Python on the target. These are the details of how Ansible reaches a box, and the inventory is a reasonable home for them.

Secrets are the exception. A password belongs nowhere near a plaintext, version-controlled inventory; the inventory carries the connection user and the port, and authentication rides on SSH keys with anything sensitive encrypted in Vault. The line below sets a non-standard port and interpreter for one host without putting a single credential in the file.

Per-host connection variables — never a password
[db]
db1.larkspur.io ansible_port=2222 ansible_python_interpreter=/usr/bin/python3

Where Inventory Lives

Ansible finds the inventory in one of three ways: a file at the default path, a path you pass explicitly with -i, or an inventory = line in ansible.cfg that names the default for a project. The -i flag wins when present, which is exactly the lever that swaps staging for prod without touching anything else.

A directory works in place of a single file. Point -i at inventories/prod/ and Ansible reads every file inside it — a static hosts.yml, a group_vars/ tree, a dynamic source — and merges them into one model. The command below runs the ping against the web group using an explicit inventory path.

Selecting an inventory at run time with -i
# one file
ansible web -i inventory.ini -m ansible.builtin.ping

# a whole directory, merged into one model
ansible web -i inventories/prod/ -m ansible.builtin.ping
Common Mistakes
  • Hardcoding ansible_password=... in a plaintext inventory and committing it to git — credentials in the inventory are a leak waiting to be found. Use SSH keys for auth and Vault for anything secret.
  • Mixing INI and YAML conventions in one file, like INI-style key=value host vars inside a YAML inventory, which earns a parse error or, worse, silently ignored variables that leave you debugging a value that was never set.
  • Listing forty near-identical hosts by hand instead of a web[1:40] range, so adding a host means editing many lines and the file rots into something nobody trusts to be complete.
  • Setting ansible_python_interpreter wrong, or leaving it unset on a host where python resolves oddly, then chasing module failures that are really interpreter-path problems in disguise.
  • Reaching for YAML's verbose nesting on a flat three-host fleet that INI would describe in six lines, paying structure tax before the structure exists to justify it.
Best Practices
  • Start in INI for a small flat fleet and move to YAML once you have nested groups or structured group variables, rather than forcing either format before the fleet's shape calls for it.
  • Use host ranges (web[1:2].larkspur.io) for homogeneous tiers so the inventory scales by editing one line instead of growing a column of duplicates.
  • Keep connection details like ansible_user and ansible_port in group vars, and keep every secret out of the inventory entirely — SSH keys for access, Vault for anything that must stay private.
  • Set ansible_python_interpreter explicitly whenever a target's Python path is non-standard, so module runs do not fail on interpreter discovery you could have pinned.
  • Confirm the parsed result with ansible-inventory --graph after editing either format, so a typo'd header or stray indent shows up as a missing host before a play does.
Comparable tools A hosts file plus a shell loop the pre-Ansible way to list and iterate machines terraform output the dynamic source you'd feed instead of hand-listing cloud hosts /etc/hosts unrelated despite the name — name resolution, not an Ansible inventory

Knowledge Check

When does YAML inventory earn its extra verbosity over INI?

  • Once you reach deep group hierarchies or structured variable values, where its explicit nesting holds up and INI starts to fight you
  • Always — INI has no syntax at all for groups, :children sections, or group variables, so the moment you need any of those you are forced onto YAML
  • Only when a cloud plugin generates the inventory at run time, since INI is reserved for hand-written files
  • Never — YAML and INI compile to the same parse tree in every case, so the extra verbosity is pure cost

Why must credentials like passwords stay out of the inventory file?

  • A plaintext, version-controlled inventory leaks any secret in it; auth should use SSH keys, with anything sensitive encrypted in Vault
  • Ansible refuses to parse any inventory that contains an ansible_password line and errors out on load
  • Passwords in inventory slow down parsing because Ansible must hash and salt each one on every single load
  • Inventory files are world-readable by design on every system, so by that rule they can never hold any variable at all, whether secret or not

What problem do host ranges like web[1:8].larkspur.io solve?

  • They collapse a homogeneous tier to one line, so a growing fleet scales without a wall of near-duplicate entries
  • They tell Ansible to connect to those eight hosts in parallel as one batch, rather than one strictly after another
  • They restrict a play to a positional slice of the matched hosts each time the playbook runs
  • They provision the eight hosts in the cloud on the fly if they do not already exist yet

What is the role of ansible_python_interpreter in an inventory?

  • It pins the path to Python on the target, avoiding module failures when python resolves to an unexpected or missing interpreter
  • It installs Python on the managed node before the first task runs if none is present there yet
  • It sets the Python version that the control node uses to run the ansible command itself, independent of any target
  • It chooses between the INI and YAML parser that Ansible uses to read that particular host's variables out of the inventory file

You got correct