AnsibleIV · Inventory FundamentalsInventory mechanics
INI and YAML inventories, side by side
What you'll learn
- Write the same fleet as an INI inventory and as a YAML inventory
- Prove two inventories equivalent by diffing ansible-inventory --list output
- Explain why /etc/ansible/hosts as the default is a hazard for production work
- Set the inventory in a project ansible.cfg so every run in the repository agrees
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
The inventory is the list of machines Ansible is allowed to touch. Every question this course cares about — which hosts will this change, how many, can I stop it — is answered here first, and answered nowhere else.
If you have met Ansible inventory before, you have probably seen a short INI file and moved on. This part goes past that. It owns the resolution rules: how groups nest, where variable files are searched for, how multiple sources merge, and which value wins when two of them disagree. Those rules are where real incidents come from, and none of them are visible in a ten-line example.
The same fleet, twice
Five hosts: two web servers, two databases, one cache. The web servers and databases are production; the cache is not. The databases have addresses that differ from their names.
In INI:
# inventory/hosts.ini
[webservers]
web01.example.com
web02.example.com
[databases]
db01.example.com ansible_host=192.0.2.31
db02.example.com ansible_host=192.0.2.32
[cache]
cache01.example.com
[production:children]
webservers
databases
[production:vars]
ntp_server=ntp.example.com
environment_name=production
In YAML:
# inventory/hosts.yml
all:
children:
cache:
hosts:
cache01.example.com:
production:
children:
webservers:
hosts:
web01.example.com:
web02.example.com:
databases:
hosts:
db01.example.com:
ansible_host: 192.0.2.31
db02.example.com:
ansible_host: 192.0.2.32
vars:
ntp_server: ntp.example.com
environment_name: production
Read the structural difference, because it is the thing that trips people converting between them.
INI is flat with cross-references. Groups are declared at the top
level in any order, and [production:children] names groups that
already exist elsewhere in the file. The nesting is a relationship
between independently declared groups.
YAML is a tree. A group is nested inside its parent. There is no
separate declaration and reference — a group appears once, in its place
in the hierarchy. webservers is written inside production because
that is what being a child of production means.
That difference is why a mechanical INI-to-YAML conversion so often changes the group tree without anyone noticing. Which is why you prove it rather than eyeball it.
Proving they are the same
ansible-inventory --list renders the fully resolved inventory as
JSON: every group, every host, every variable. Two inventories that
produce identical --list output are identical as far as Ansible is
concerned, regardless of format.
ansible-inventory -i inventory/hosts.ini --list > /tmp/ini.json
ansible-inventory -i inventory/hosts.yml --list > /tmp/yml.json
diff /tmp/ini.json /tmp/yml.json && echo "IDENTICAL"$ ansible-inventory -i inventory/hosts.ini --list{
"_meta": {
"hostvars": {
"db01.example.com": {
"ansible_host": "192.0.2.31",
"environment_name": "production",
"ntp_server": "ntp.example.com"
},
"db02.example.com": {
"ansible_host": "192.0.2.32",
"environment_name": "production",
"ntp_server": "ntp.example.com"
},
"web01.example.com": {
"environment_name": "production",
"ntp_server": "ntp.example.com"
},
"web02.example.com": {
"environment_name": "production",
"ntp_server": "ntp.example.com"
}
},
"profile": "inventory_legacy"
},
"all": {
"children": [
"ungrouped",
"cache",
"production"
]
},
"cache": {
"hosts": [
"cache01.example.com"
]
},
"databases": {
"hosts": [
"db01.example.com",
"db02.example.com"
]
},
"production": {
"children": [
"webservers",
"databases"
]
},
"webservers": {
"hosts": [
"web01.example.com",
"web02.example.com"
]
}
}Three things in that output are worth noticing now, because each one becomes a lesson later in this part.
cache01.example.comhas no variables at all. Thentp_serverandenvironment_nameset onproductiondid not reach it, because it is not inproduction. Group variables flow to members, and only to members.allcontainsungrouped, a group nobody wrote. So does every inventory ever written.- Group variables are already flattened onto the hosts. By the time
--listrenders, the question “which group did this value come from” has been answered and discarded. When two groups disagree, this output shows you the winner and not the argument.
Which format to choose
Both are supported, both are current, and neither is deprecated.
YAML, for a new repository. It is the same language as your playbooks and your variable files, so there is one syntax to know rather than two. It nests naturally, which matters once the group tree is more than one level deep. It handles structured variables — lists, dicts — that INI cannot express without workarounds. And it is what the documentation examples increasingly use.
INI, when you inherit it. It is more compact for a flat fleet, non-programmers read it comfortably, and there is no reason to convert a working inventory for aesthetics. You will meet it; be able to read it.
The one place the choice is forced is structured data. In YAML a group
variable can be a list or a mapping directly. In INI every value is a
string unless you write it as inline JSON, which works and is unpleasant
to read. If your group variables are anything but scalars, that is a
reason to prefer YAML — but the better answer is usually to move them
out of the inventory file entirely and into group_vars/, which is the
next lesson but one.
Never rely on the default inventory
Ansible has a default inventory path, and it is a hazard for production work:
$ ansible-config dump | grep DEFAULT_HOST_LISTDEFAULT_HOST_LIST(default) = ['/etc/ansible/hosts']That file is outside your repository. It is therefore outside your pull requests, outside your review, outside your history, and outside whatever the last person who touched the controller did to it.
The project config is the better of the two, because it makes the correct behaviour the default for everyone rather than a thing each person must remember:
# ansible.cfg, at the root of the repository
[defaults]
inventory = inventory/production
ansible --version | head -2
ansible-config dump --only-changed | grep DEFAULT_HOST_LISTRemember from the controller part that a project ansible.cfg is
ignored entirely if the directory is world-writable, and that the
fallback when it is ignored is exactly the /etc/ansible/hosts problem
above. The two failures compose, and the symptom is a run that targets
either nothing or the wrong thing.
Knowledge check
Knowledge check · 4 questions
Q1. You have converted an INI inventory to YAML by hand. What is the correct way to confirm the conversion is faithful?
Q2. Which statements about the default inventory path are accurate? Select all that apply.
Q3. A file named inventory/hosts.yml actually contains INI-formatted content. What happens?
Q4. An inventory typo that drops a host out of a group is more dangerous than one that adds a host which does not exist.
Passing score: 75%. Answers are checked in this browser.