TTechclick ⚡ XP 0% All lessons
Ansible · Inventory · Inventory & Dynamic InventoryInteractive · L1 / L2 / L3

Ansible Inventory & Dynamic Inventory: — Host Lists That Never Go Stale

Sneha hand-maintains a hosts file of 200 servers. By Monday lunch three are gone, five are new, and her playbook is configuring boxes that no longer exist. Dynamic inventory fixes that: Ansible asks the source of truth — AWS, NetBox, Azure — for the live host list at run time, every run. This lesson takes you from a static INI file to a self-updating inventory.

📅 2026-06-11 · ⏱ 13 min · 3 live demos · 4 infographics · 🏷 10-Q assessment + AI Tutor inline

⚡ Quick Answer

Ansible inventory for L1/L2 engineers and RHCE EX294: static INI vs YAML, groups/children/ranges, host_vars/group_vars, and dynamic inventory plugins (aws_ec2, azure_rm, nb_inventory) that pull a live host list.

🎯 By the end you will be able to

Read as:

Pick where you want to start

1

Static inventory

INI vs YAML, groups, children, ranges, host_vars.

2

Why it goes stale

Cloud hosts change hourly; hand lists rot fast.

3

Dynamic plugins

aws_ec2, azure_rm, nb_inventory pull the live list.

4

A real setup

Auto-group, cache, verify with --graph.

🧠 Warm-up — 3 questions, no score

Just notice which ones make you pause. We answer all three inside the lesson.

1. You manage 12 access switches named sw01 to sw12. How do you add them to an inventory without typing 12 lines?

Answered in Static inventory.

2. Your AWS account spins up and kills EC2 instances all day. Which inventory keeps up with that automatically?

Answered in Dynamic plugins.

3. You want to run a play on hosts that are in BOTH the webservers group AND the production group. Which pattern?

Answered in Why it goes stale.

Most engineers think…

Most engineers think "inventory" just means a text file of IP addresses you keep next to your playbook — write it once, point Ansible at it, done. So they keep editing hosts.ini by hand forever.

Wrong — and that habit quietly breaks automation at scale. An inventory is a source of truth about which hosts exist and how to reach them. For a fixed lab, a static file is fine. But in cloud and dynamic networks the truth lives in AWS, Azure, GCP or NetBox — not your text file — and it changes hourly. The real skill is a dynamic inventory plugin that asks that source for the live host list at run time, then auto-builds your groups with keyed_groups and compose. The text file is the training wheels; the plugin is the bike.

① Static inventory — the INI/YAML host list, groups, children & ranges

Start where everyone starts. Inventory is just Ansible's answer to one question: which machines do I manage, and how do I reach each one? The simplest answer is a static inventory — a text file you write by hand. Rahul, an L1 engineer at Infosys, manages 12 access switches and 6 servers in one branch, so a static file is perfect for him. The host list rarely changes, and he can read the whole thing at a glance.

There are two formats, and both are first-class. INI is the classic — flat, fast to read, great for a small estate. YAML is the structured one — it handles nested groups and typed variables (lists, dicts, booleans) far more cleanly. Same inventory, two spellings:

INI inventory — hosts.ini (flat, simple)
# hosts.ini
[webservers]
web1.infosys.local
web2.infosys.local

[dbservers]
db1.infosys.local

# nested group: a group OF groups, via :children
[branch_pune:children]
webservers
dbservers

# group variables
[webservers:vars]
ansible_user=svc_ansible
Expected output
$ ansible-inventory -i hosts.ini --list --yaml | head
all:
  children:
    branch_pune:
      children:
        webservers: {hosts: {web1.infosys.local: ..., web2.infosys.local: ...}}
        dbservers: {hosts: {db1.infosys.local: ...}}
YAML inventory — hosts.yml (same thing, structured)
# hosts.yml
all:
  children:
    branch_pune:
      children:
        webservers:
          hosts:
            web1.infosys.local:
            web2.infosys.local:
          vars:
            ansible_user: svc_ansible
        dbservers:
          hosts:
            db1.infosys.local:
Expected output
$ ansible-inventory -i hosts.yml --graph
@all:
  |--@branch_pune:
  |  |--@webservers:
  |  |  |--web1.infosys.local
  |  |  |--web2.infosys.local
  |  |--@dbservers:
  |  |  |--db1.infosys.local

Two groups exist automatically, in every inventory, whether you write them or not: all (every host) and ungrouped (hosts you didn't put in any group). That's why ansible-inventory --graph always starts at @all. Notice the children idea above — branch_pune contains no hosts directly, only the webservers and dbservers groups. Nested groups let you say "all of Pune" in one word.

👉 So far: inventory answers "which hosts, reached how?"; two formats (INI flat, YAML structured); the auto groups all/ungrouped; and nested children. Next: ranges so you never type 50 near-identical hostnames.

Now the trick that saves your fingers. When hosts follow a pattern — sw01 through sw12, or www01 through www50 — you don't list them one by one. You use a host range. The brackets hold start:end, and you can add a stride and even letters:

INI inventory — ranges expand to many hosts
[switches]
sw[01:12].infosys.local        # sw01 .. sw12  (12 hosts, zero-padded)

[webfarm]
www[01:50:2].example.com       # www01, www03, www05 ... (stride = 2)

[shards]
db-[a:f].infosys.local         # db-a, db-b ... db-f  (alphabetic)
Expected output
$ ansible switches -i hosts.ini --list-hosts
  hosts (12):
    sw01.infosys.local
    sw02.infosys.local
    ... (sw03 .. sw11) ...
    sw12.infosys.local
Figure 1 — A static inventory is one file you must keep editing by hand
A static inventory is one hand-written file: nested groups on top, host ranges below, host_vars and group_vars beside it — and every change is a manual edit A tree of a static Ansible inventory. The built-in all group contains a nested child group branch_pune, which contains leaf groups webservers, dbservers and switches. The switches group uses a range sw[01:12] that expands to twelve hosts. Beside the inventory file sit two directories, group_vars holding per-group variable files and host_vars holding per-host variable files, where ansible_host, ansible_user and ansible_network_os live. An amber callout marks the weakness: because the file is hand-written, every added or removed host needs a manual edit, so the file drifts out of date. Static inventory — the shape of a hand-written host list @all (auto) @branch_pune (children) @webserversweb1, web2 @dbserversdb1 @switchessw[01:12] → 12 hosts group_vars/ host_vars/ansible_host · ansible_useransible_network_os ⚠ the catchevery new/removed host = a manual editso the file drifts out of date stale / wronginventory structurecost / decisionwhere vars livetidy / correct
Read top to bottom: the all group splits into nested children, leaf groups hold hosts and ranges, and host_vars/group_vars sit beside the file. The amber note is the cost — every host change is a manual edit.

Where do the connection details go? Not jammed into the inventory file as a wall of inline vars — that gets unreadable fast. The tidy place is two directories beside your inventory: group_vars/ and host_vars/. A file named after a group applies to every host in it; a file named after a host applies only to that host. This is exactly where ansible_host, ansible_user and (for network gear) ansible_network_os belong.

group_vars/switches.yml — where network connection vars live
# group_vars/switches.yml  (applies to every host in @switches)
ansible_network_os: cisco.ios.ios   # tells network modules: this is IOS
ansible_user: svc_netauto
ansible_connection: network_cli

# host_vars/sw01.infosys.local.yml  (just this one box)
# ansible_host: 10.20.10.1          # the real IP to connect to
Expected output
$ ansible-inventory -i hosts.ini --host sw01.infosys.local
{
  "ansible_connection": "network_cli",
  "ansible_host": "10.20.10.1",
  "ansible_network_os": "cisco.ios.ios",
  "ansible_user": "svc_netauto"
}
Daily-life analogy — the society gate-pass register

A static inventory is your apartment society's gate-pass register — a printed book at the gate listing every flat, who lives there, and which vehicles belong. Groups are the wings (A-wing, B-wing); children is "all of Tower-2"; host_vars is the note beside one flat ("uses lift 3, dog on file"). It works fine while nobody moves. But the day three families shift out and five move in, the book is wrong until a human updates it — and that is precisely the weakness dynamic inventory removes.

Pause & Predict

Predict: you point a play at the group branch_pune from the example, which only contains the child groups webservers and dbservers. Which actual hosts run the play, and does Ansible mind that branch_pune has no hosts of its own? Type your guess.

Answer: All hosts inside the child groups run it — web1, web2 and db1. Ansible does not mind at all: a group that only contains other groups (via children) is normal and useful. Targeting branch_pune is shorthand for "every host in any of its descendant groups". This is the whole point of nested groups — say "all of Pune" once instead of listing every host.
Quick check · Q1 of 10

Sneha at TCS needs sw01 through sw24 in her inventory, but only the even-numbered ones are in production. What is the cleanest way to add ALL 24 as a range?

Correct: a. sw[01:24] expands to all 24 zero-padded hosts in one line; she can use a stride or a second group for the even ones. There is no 10-host limit; sw* is a targeting wildcard (matches existing hosts), not a way to define them; and ranges work in BOTH INI and YAML, so no conversion is needed.

② Why static goes stale — and what a dynamic inventory does instead

A static file is honest exactly once: the moment you save it. After that, reality drifts. Priya, an L2 engineer at Flipkart, owns an Auto Scaling group that adds EC2 instances when traffic spikes for a sale and kills them when it dies down. By the time she finishes editing hosts.ini, the IDs in it are already wrong — new instances launched, old ones terminated. Her playbook then configures ghost hosts and skips the real ones.

This isn't only cloud. A network team's switch fleet changes as sites open, gear gets RMA'd, and labs spin up and down. The source of truth for "which hosts exist right now" lives in AWS, Azure, GCP or NetBox — never in a text file a human edits. A static inventory is a photocopy of that truth, and photocopies go out of date.

▶ Watch a static inventory rot between Monday and Tuesday

Priya saves a perfect hosts file Monday morning. Follow what reality does to it over the next 24 hours while the file sits unchanged. Press Play for the healthy path, then Break it to see the failure.

① Mon 09:00hosts.ini = 20 instances; file is 100% correct the instant it is saved
② Mon 13:00sale traffic spikes; ASG launches 8 new instances — not in the file
③ Mon 22:00traffic drops; ASG terminates 11 instances — still listed in the file
④ Tue 09:00play runs against hosts.ini: hits 11 ghosts, misses 8 real hosts
Press Play to step through the healthy path. Then press Break it.

A dynamic inventory flips the model. Instead of reading a frozen file, Ansible runs a plugin that asks the source of truth — the AWS API, the Azure API, the NetBox API, DNS — "what hosts exist right now?" every single run. The list is generated fresh each time, so there is nothing to keep editing and nothing to go stale. Priya deletes her cron-job-to-edit-the-file and points Ansible at an aws_ec2.yml config instead.

Figure 2 — Static reads a frozen file; dynamic asks the live source every run
Static inventory reads a frozen, human-edited file that lags reality; dynamic inventory asks the live source of truth at run time so the host list is never stale A two-column comparison of how the host list is produced. On the left, the static path: a human edits a hosts file, Ansible reads that frozen file, and because reality (the cloud or NetBox) keeps changing, the file lags and contains ghost hosts and missing hosts. On the right, the dynamic path: an inventory plugin reading an aws_ec2.yml config calls the AWS or NetBox API at run time, receives the current list of hosts, and hands Ansible a fresh, correct inventory every run with no human edit. Red marks the stale frozen file; green marks the live always-fresh path. Where does the host list come from? Static — read a frozen file human edits hosts.ini(frozen) Ansible reads file reality kept changing…✗ ghost hosts (terminated, still listed)✗ missing hosts (new, not added)file lags the truth Dynamic — ask the live source prod.aws_ec2.ymlplugin config AWS / NetBoxAPI (truth) query at run time plugin builds the list fresh inventory, every run✓ new hosts appear automatically✓ dead hosts drop off automaticallyno human edit, never stale
Left (red): Ansible reads a file a human edits — it lags reality. Right (green): the plugin queries AWS/NetBox at run time, so the list is always live. Same playbook, different freshness.
Common mistake — "I'll just regenerate the static file with a nightly cron"

Symptom: someone writes a script that dumps the cloud into hosts.ini every night, and during the day the file is still wrong — instances launched at 11 a.m. are invisible until tonight's run. You've rebuilt a stale photocopy on a timer. The fix isn't a fresher photocopy; it's a dynamic inventory plugin that queries the source at the moment the playbook runs, so it is correct for this run, not for last midnight.

Pause & Predict

Predict: a teammate says "dynamic inventory means I never write any config — Ansible just magically finds my hosts." Is that right? What do you actually still have to provide? Type your guess.

Answer: Not quite. You still write a small config file (e.g. prod.aws_ec2.yml) that names the plugin, the regions/filters, and how to build groups — plus credentials for the source (AWS keys/role, a NetBox API token). What you stop doing is maintaining the host LIST itself; the plugin fetches that live. So: you describe HOW to query the truth once, and the WHAT (the hosts) updates itself.
Quick check · Q2 of 10

Karthik at HCL asks: "Why is a stale static inventory a security risk, not just an annoyance?" Best answer?

Correct: d. In cloud, a terminated instance's private IP can later be assigned to a different tenant's new instance; a stale entry can point your play at a host you no longer own. The graph speed is irrelevant; static files CAN be vaulted; and inventories do not auto-expire — that's exactly the problem dynamic inventory solves.

③ Dynamic inventory plugins — the modern way (aws_ec2, azure_rm, nb_inventory)

There are two ways to do dynamic inventory, and only one is current. The legacy way was an inventory script — you ran a Python file that spat out JSON. The modern way is an inventory plugin, configured by a tiny YAML file. Plugins win because they ship inside collections, are maintained for you, and hand you grouping and caching features out of the box. RHCE and every current guide teach plugins; treat scripts as legacy you only meet in old repos.

The plugin you reach for depends on your source of truth. The four you'll meet most:

The four dynamic inventory plugins you'll actually use

Tap each — same idea (query the live source), different source of truth. The string after the dots is the fully-qualified plugin name you put in your config.

☁️
amazon.aws.aws_ec2
tap to flip

Pulls live EC2 instances from the AWS API. Config file must end in aws_ec2.yml. So: cloud fleets that autoscale stay correct.

🔷
azure.azcollection.azure_rm
tap to flip

Pulls VMs from Azure Resource Manager. Groups by resource group, location, tags. So: Azure estates without hand-editing.

🟦
google.cloud.gcp_compute
tap to flip

Pulls Compute Engine instances from GCP. Groups by zone, network, labels. So: GCP fleets self-update too.

🗄️
netbox.netbox.nb_inventory
tap to flip

Pulls devices/VMs from NetBox (the network team's source of truth). Groups by role, site, tag. So: switches/routers stay live.

A plugin config is short. You name the plugin, say where to look, and then — the powerful part — tell it how to auto-build groups. Two features do the heavy lifting. keyed_groups turns a tag into groups: feed it the role tag and it makes role_web, role_db, one per value. compose sets host variables with a Jinja2 expression — most famously ansible_host.

prod.aws_ec2.yml — a real dynamic inventory config (note keyed_groups + compose)
# File MUST end in aws_ec2.yml (or .yaml) or the plugin ignores it
plugin: amazon.aws.aws_ec2
regions:
  - ap-south-1                      # Mumbai
filters:
  instance-state-name: running     # ignore stopped/terminated
keyed_groups:
  - key: tags.role                  # tag:role  ->  role_web, role_db ...
    prefix: role
  - key: placement.availability_zone
    prefix: az
compose:
  ansible_host: {{ public_ip_address | default(private_ip_address) }}
groups:
  prod: "tags.env == 'prod'"        # custom group from a Jinja2 test
Expected output
$ ansible-inventory -i prod.aws_ec2.yml --graph
@all:
  |--@aws_ec2:
  |  |--i-0a1b2c3d (10.0.12.7)
  |--@role_web:
  |  |--i-0a1b2c3d (10.0.12.7)
  |--@az_ap_south_1a:
  |--@prod:
🖥️ This is the terminal frame you'll live in — ram@netauto:~/inventory$ ansible-inventory -i prod.aws_ec2.yml --graph. (Recreated for clarity — your terminal matches this.)
ram@netauto:~/inventory
1
$ command
ansible-inventory -i prod.aws_ec2.yml --graph
2
plugin matched
amazon.aws.aws_ec2 (filename ends aws_ec2.yml)
3
auto group
@role_web ← from keyed_groups key: tags.role
auto group
@az_ap_south_1a ← from placement.availability_zone
ansible_host
10.0.12.7 ← compose: public|default(private)
4
result
live host list built from AWS, no hosts file edited
▶ run

For network teams the same idea points at NetBox instead of a cloud. The nb_inventory plugin reads your NetBox API and groups devices by the role/site/tag fields you already maintain there — so the inventory mirrors your DCIM exactly, with no second list to keep in sync.

netbox.yml — NetBox as the source of truth for switches/routers
# File ends in nb_inventory.yml (e.g. dc.nb_inventory.yml)
plugin: netbox.netbox.nb_inventory
api_endpoint: https://netbox.infosys.local
token: "{{ lookup('env', 'NETBOX_TOKEN') }}"   # never hardcode the token
group_by:
  - device_roles                  # -> groups like device_roles_edge_router
  - sites                         # -> groups like sites_mumbai_dc
device_query_filters:
  - has_primary_ip: 'true'        # skip patch panels / passive gear
compose:
  ansible_network_os: platform.slug
Expected output
$ ansible-inventory -i dc.nb_inventory.yml --graph
@all:
  |--@device_roles_edge_router:
  |  |--mum-edge-01
  |  |--mum-edge-02
  |--@sites_mumbai_dc:
  |  |--mum-edge-01
  |  |--mum-core-01
Common mistake — "Could not parse" because the plugin isn't enabled (or the filename is wrong)

Symptom: ansible-inventory -i prod.aws_ec2.yml --graph errors with "Could not parse … no inventory plugin succeeded". Two usual causes. (1) Filename: it must end in aws_ec2.yml/.yamlprod.yml alone won't match. (2) enable_plugins: in ansible.cfg the [inventory] enable_plugins list must include amazon.aws.aws_ec2 — and you must keep the defaults (host_list, script, auto, yaml, ini, toml) in that list, or your static files stop parsing too. Run -vvv to see which plugins were tried and skipped.

Pause & Predict

Predict: in the aws_ec2 config above, keyed_groups uses key: tags.role with prefix: role. If three instances are tagged role=web and two are tagged role=db, exactly which groups appear, and who is in them? Type your guess.

Answer: Two groups appear: role_web (the three web instances) and role_db (the two db instances), one group per distinct tag value, with the prefix prepended. You never listed any hosts — keyed_groups read the live tag off each instance and bucketed them. Tag a fourth instance role=cache later and a role_cache group appears on the next run, automatically.
Quick check · Q3 of 10

Meera at Wipro wants Ansible to connect to each EC2 box on its public IP when it has one, otherwise its private IP — without changing the inventory hostname. Which plugin feature does that?

Correct: c. compose sets host variables from a Jinja2 expression, and ansible_host = public_ip_address | default(private_ip_address) is the canonical pattern for exactly this. keyed_groups only builds groups; groups only creates a named bucket; regions only narrows which AWS regions are queried — none of them set the connection IP.

④ A real setup — auto-group, cache, verify with --graph, and the gotchas

Let's wire a believable production setup, the way an L2 at Airtel would. Source of truth: NetBox for the network gear, AWS for the cloud edge. Goal: zero hand-maintained host lists, groups that build themselves from role/site/tag, and a fast feedback loop. Three moving parts: the plugin configs (done in section ③), caching so you don't hammer the API, and ansible-inventory to verify.

First, point Ansible at a directory of inventory configs and turn on caching. A directory lets static and dynamic sources coexist — your fixed jump-hosts in a YAML file, your cloud and NetBox via plugins, all merged into one inventory.

ansible.cfg — enable the plugins and inventory caching
[defaults]
inventory = ./inventory          # a directory: mixes static + dynamic

[inventory]
# keep the DEFAULTS or your static files stop parsing:
enable_plugins = host_list, script, auto, yaml, ini, toml, amazon.aws.aws_ec2, netbox.netbox.nb_inventory
cache = true
cache_plugin = ansible.builtin.jsonfile
cache_connection = /tmp/ansible_inv_cache
cache_timeout = 3600             # seconds; re-query the source after 1h
Expected output
$ ansible-inventory --graph | head -1
@all:
# cache cold -> hits AWS+NetBox; warm runs read /tmp/ansible_inv_cache (fast)

Now verify — this is the command you run a hundred times a week. ansible-inventory --graph draws the tree (groups, children, hosts); --list dumps the full JSON with every variable; --host NAME shows one host's resolved vars. If the source changed and you cached, --flush-cache forces a fresh pull.

Verify the merged (static + dynamic) inventory — the recreation
ram@netauto:~$ ansible-inventory --graph
ram@netauto:~$ ansible-inventory --graph --flush-cache   # ignore cache, re-query
ram@netauto:~$ ansible-inventory --host mum-edge-01       # one host's vars
Expected output
@all:
  |--@aws_ec2:
  |  |--i-0a1b2c3d
  |--@role_web:
  |  |--i-0a1b2c3d
  |--@device_roles_edge_router:
  |  |--mum-edge-01
  |  |--mum-edge-02
  |--@sites_mumbai_dc:
  |  |--mum-edge-01
  |--@ungrouped:
Figure 3 — One verify command proves the whole merged inventory is correct before any play runs
A production inventory pipeline: NetBox and AWS feed two plugins through a cache, and ansible-inventory --graph is the one command that verifies the merged tree before any playbook runs A left-to-right pipeline. Two sources of truth on the left, NetBox for network devices and the AWS API for cloud instances, feed two inventory plugins, nb_inventory and aws_ec2. A cache layer in the middle (jsonfile, one hour timeout) avoids re-hitting the APIs on every run. The plugins merge into one inventory tree containing auto-built groups like device_roles_edge_router, sites_mumbai_dc and role_web. On the right, the command ansible-inventory --graph renders that tree so the engineer verifies it is correct before running any playbook. Amber marks the decision/verify point; green marks a verified live result; red marks the three classic failures: plugin not enabled, bad auth, group-name collision. From live source → verified inventory, in one pipeline NetBoxdevice source of truth AWS APIEC2 source of truth nb_inventoryplugin aws_ec2plugin cachejsonfile · 1h merged inventory@device_roles_edge_router@sites_mumbai_dc@role_webgroups built themselves ansible-inventory --graphthe one lens you check BEFORE any play runs the 3 classic failures --graph catches early✗ plugin not in enable_plugins ✗ bad/missing auth (token, AWS creds) ✗ group-name collisionempty tree, error, or two sources fighting for the same group name failure modeplugin / cacheverify / decidesource of truthverified live
Follow the flow left to right: two live sources feed two plugins, the cache sits in the middle, and ansible-inventory --graph is the single lens you check before running anything. Green = verified live.

Once the inventory is correct, you target the right slice with the pattern language. This is where union :, intersection & and exclusion ! earn their keep — and the order matters: Ansible processes : first, then &, then !.

Pattern targeting — union (:), intersection (&), exclusion (!)
# union: web OR db
ansible 'webservers:dbservers' --list-hosts

# intersection: hosts in BOTH webservers AND prod
ansible 'webservers:&prod' --list-hosts

# exclusion: all prod EXCEPT the mumbai site
ansible-playbook patch.yml --limit 'prod:!sites_mumbai_dc'
Expected output
# 'webservers:&prod'  ->
  hosts (2):
    i-0a1b2c3d
    i-0e9f8a7b
# only the web boxes that are also tagged env=prod

Arjun at Zomato faces this

Arjun, an L1 on-call, runs his nightly play and it touches ZERO hosts — "skipping: no hosts matched". But ansible-inventory --graph clearly shows the cloud boxes.

Likely cause

A group-name / pattern mismatch. His play targets hosts: webservers, but the live aws_ec2 plugin built the group as role_web (from keyed_groups prefix: role + key tags.role), not webservers. The group he typed simply doesn't exist in the dynamic inventory.

Diagnosis

He stops guessing and reads the actual group names the plugin produced, then checks one host's resolved vars to confirm it's in the group he expects.

ansible-inventory --graph (see the real group names) → ansible-inventory --host i-0a1b2c3d
Fix

Point the play at the real group: hosts: role_web. (Or add a groups: webservers: "tags.role == 'web'" line to the plugin config so the friendly name also exists.)

Verify

Re-run with --limit role_web --list-hosts → the 3 web instances appear; the nightly play now configures them. ansible-inventory --graph shows role_web populated.

Common mistake — two sources build the SAME group name and quietly merge

Symptom: a play runs on more hosts than you expected. Cause: your AWS plugin's keyed_groups and your NetBox plugin's group_by both produce a group literally called web, so Ansible merges them into one group spanning two clouds. Fix: give each plugin a distinct prefix (e.g. aws_ vs nb_) so the auto-groups can't collide — role_web from AWS, device_roles_web from NetBox stay separate.

Prove the inventory is right BEFORE the play touches anything

Cold habit to build: never run a playbook against a fresh dynamic source without first running ansible-inventory --graph (and --limit YOUR_TARGET --list-hosts). The graph proves the plugin parsed, auth worked, and the groups you're about to target actually exist and contain the right hosts. Thirty seconds of --graph saves you from a play that runs on the wrong boxes — or on a stranger's recycled IP.

One current, real-world note for 2025-2026. From ansible-core 2.19 the templating trust model was inverted (the "Data Tagging" change). Variables that come from inventory plugins and other external sources are no longer trusted as Jinja2 templates by default — a value pulled from a tag or a NetBox field that happens to contain {{ ... }} is treated as a literal string unless explicitly trusted. The takeaway for you as an inventory author: your compose/keyed_groups expressions still work, but you can't rely on attacker-controllable external data being re-templated — which is exactly the security behaviour you want.

Figure 4 — Static vs dynamic + the plugin cheat-sheet — one card for the whole lesson
Ansible inventory on one card: static versus dynamic, the four plugins, keyed_groups and compose, the verify commands and the pattern operators A six-tile cheat sheet. Tile one: static versus dynamic — static is a hand-written INI or YAML file good for fixed labs, dynamic is a plugin that queries the live source so it never goes stale. Tile two: the four plugins and their source of truth — amazon.aws.aws_ec2 for AWS, azure.azcollection.azure_rm for Azure, google.cloud.gcp_compute for GCP, netbox.netbox.nb_inventory for NetBox. Tile three: keyed_groups builds one group per tag value, compose sets host vars like ansible_host. Tile four: the verify commands ansible-inventory --graph, --list, --host and --flush-cache. Tile five: the pattern operators colon for union, ampersand for intersection, exclamation for exclusion, used with --limit. Tile six: the enable_plugins and filename gotchas. Ansible inventory — your one-glance card Static vs dynamicstatic = hand INI/YAML, fixed labsdynamic = plugin queries live sourcechildren = nested groupsdynamic never goes stale The 4 pluginsamazon.aws.aws_ec2 → AWSazure.azcollection.azure_rm → Azuregoogle.cloud.gcp_compute → GCPnetbox.netbox.nb_inventory → NetBox Auto-build groupskeyed_groups: 1 group / tag valuecompose: set ansible_host etc.groups: custom Jinja2 bucketno host lists to maintain Verify commandsansible-inventory --graphansible-inventory --listansible-inventory --host NAME--flush-cache (ignore cache) Pattern targetingweb:db → union (OR)web:&prod → intersection (AND)prod:!mumbai → excludeorder: : then & then ! · use --limit Gotchasfile must end *.aws_ec2.ymlenable_plugins (keep defaults!)distinct prefix → no group clash2.19: ext-source vars not trusted The one sentence to rememberStatic inventory = a file you must edit; dynamic inventory = a plugin that asks the livesource of truth at run time and auto-builds groups — so the host list never goes stale.verify everything with: ansible-inventory --graph
Your one-glance map: when to use static vs dynamic, the four plugins and their source of truth, the keyed_groups/compose idea, the verify commands, and the pattern operators. Keep it open in week one.
Daily-life analogy — Swiggy's live rider map, not a printed roster

A dynamic inventory is Swiggy's live rider map, not a printed roster pinned to the wall. The printed roster (static) is right at 9 a.m. and wrong by noon as riders log on and off. The live map (dynamic) asks the system who is online right now every time an order drops — so it routes to riders who actually exist this second. ansible-inventory --graph is you glancing at that live map before dispatching the order.

Next: point this inventory at a nightly config-backup & drift play
Quick check · Q4 of 10

An interviewer asks Neha: "You have a dynamic AWS inventory. Before running a destructive play on prod web servers only, what's the single safest verification step?"

Correct: b. Listing the exact matched hosts with the real pattern (intersection of role_web AND prod) proves who the destructive play will hit, on a live source, before it runs. Blind trust is how you nuke the wrong boxes; disabling cache changes speed not correctness; and editing a static file defeats the whole point of dynamic inventory.

🤖 Ask the AI Tutor

Tap any question — instant, scoped to this lesson. No login, no waiting.

Pre-curated from Ansible docs + community Q&A, scoped to this lesson. For a live prod issue, paste your export into chat.techclick.in.

📝 Wrap-up assessment — six more

You've answered 4 inline. Six left. 70% (7 of 10) marks the lesson complete on your profile. Tap Submit all answers at the end.

Q5 · Remember

In an Ansible inventory, which built-in group always contains every host, whether you define it or not?

Correct: b. all is the automatic group containing every host in the inventory — that is why ansible-inventory --graph starts at @all. ungrouped only holds hosts not placed in any defined group; webservers is a group you would define yourself; localhost is a single host, not a catch-all group.
Q6 · Apply

You manage switches sw01 through sw20 that all follow the same naming pattern. What is the cleanest single line to add them all to an INI inventory?

Correct: a. sw[01:20].infosys.local is a host range that expands to all 20 zero-padded hosts in one line, in both INI and YAML. Comma lists are not how INI defines hosts; sw* is a targeting wildcard for hosts that already exist, not a definition; and ranges are a core static-inventory feature, no plugin needed.
Q7 · Apply

In an aws_ec2.yml plugin config, you want Ansible to connect using the public IP when present, otherwise the private IP, without renaming hosts. Which block does this?

Correct: c. compose sets host variables from a Jinja2 expression, and ansible_host: public_ip_address | default(private_ip_address) is the canonical recipe for public-or-private connection IP. keyed_groups only creates groups; filters only narrows which instances are returned; regions only chooses which AWS regions are queried.
Q8 · Analyze

A dynamic play "skips: no hosts matched", yet ansible-inventory --graph clearly lists your cloud instances. The play targets hosts: webservers. Most likely cause?

Correct: d. If --graph shows the hosts, the plugin parsed and auth worked — so the failure is a name mismatch: keyed_groups with prefix: role created role_web, not webservers, so targeting webservers matches nothing. Expired creds or an empty cache would leave --graph empty; and any inventory can have a webservers group if you name it that.
Q9 · Analyze

You run two plugins: aws_ec2 and nb_inventory. After adding both, a patch play hits more hosts than expected. Inspecting --graph, one group named "web" contains both EC2 and NetBox devices. What happened and how do you fix it?

Correct: a. Two sources that both produce a group with the same name are merged into one group by Ansible — a group-name collision. The fix is distinct prefixes so the auto-groups stay separate (role_web vs device_roles_web). It is not random, NetBox did not overwrite anything, and flushing the cache re-fetches the same colliding names — it does not separate them.
Q10 · Evaluate

Two engineers describe their plan for a 500-host autoscaling estate. (A) "I'll regenerate hosts.ini from AWS with a nightly cron." (B) "I'll use the aws_ec2 plugin with keyed_groups so the list and groups are built live at run time." Which is stronger and why?

Correct: b. B is correct for THIS run: it queries AWS when the play executes and auto-builds groups, so instances launched at 11 a.m. are included immediately. A rebuilds a stale photocopy that is wrong for everything launched/terminated since midnight — the exact failure mode dynamic inventory exists to remove. Cron reliability is irrelevant to freshness, and the two do NOT end up equivalent: one lags, one is live.
Lesson complete — saved to your profile.
Almost! You need 70% (7 of 10) — re-read the path that tripped you up and tap "Try again".

🧠 In your own words

Type one line: In one line, why does a dynamic inventory never go stale the way a hand-edited hosts file does? Then compare to the expert version.

Expert version: Because a dynamic inventory plugin queries the source of truth (AWS, Azure, NetBox, DNS) for the live host list at run time on every run — so the list is regenerated fresh each time instead of being a snapshot you have to remember to edit.

🗣 Teach a friend

Best way to lock it in — explain it in one line to a teammate. Tap to generate a paste-ready summary.

📖 Glossary

Static inventory
A hand-written INI or YAML file of hosts and groups — the list of hosts Ansible manages. Simple, but you must edit it whenever hosts change.
Dynamic inventory
A plugin that generates the host list at run time by querying a live source (AWS, Azure, NetBox, DNS). Never goes stale.
Group / children
A named bucket of hosts. A group can contain other groups via :children (INI) or children: (YAML) — nested groups.
Host range
A pattern like sw[01:12] or www[01:50:2] that expands to many hosts without listing each one.
host_vars / group_vars
Directories (next to the inventory) holding per-host and per-group variable files in YAML — the tidy place for vars.
ansible_host / ansible_user
Connection variables: the real IP/name to connect to, and the login user. Set per host or group.
ansible_network_os
Tells network modules which platform a device runs (e.g. ios, nxos, eos) so the right CLI/API is used.
Inventory plugin
Modern code (inside a collection) that reads a config YAML and produces a dynamic inventory. Replaces legacy scripts.
keyed_groups
A plugin feature that auto-creates one group per distinct value of a host property (e.g. one group per tag).
compose
A plugin feature that sets host variables from Jinja2 expressions, e.g. ansible_host from public-or-private IP.
enable_plugins
The ansible.cfg [inventory] list of which inventory plugins are allowed to parse a source. Keep the defaults in it.
ansible-inventory --graph
The command that draws the inventory as a tree of groups and hosts — your verify step before any play runs.

📚 Sources

  1. Ansible Community Documentation — "How to build your inventory" (INI vs YAML, [group:children]/children:, host ranges www[01:50:2] and db-[a:f], default groups all/ungrouped, host_vars/group_vars layout, ansible_host/ansible_user/ansible_connection). docs.ansible.com/projects/ansible/latest/inventory_guide/intro_inventory.html
  2. Ansible Community Documentation — "Patterns: targeting hosts and groups" (: union, & intersection, ! exclusion; processing order : then & then !; --limit and --limit @retry_hosts.txt). docs.ansible.com/projects/ansible/latest/inventory_guide/intro_patterns.html
  3. Ansible Community Documentation — amazon.aws.aws_ec2 inventory plugin (filename suffix *.aws_ec2.yml, plugin/regions/filters/hostnames/keyed_groups key+prefix/compose ansible_host=public_ip_address|default(private_ip_address)/groups; enable in ansible.cfg [inventory] enable_plugins). docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ec2_inventory.html
  4. NetBox Ansible collection — netbox.netbox.nb_inventory (api_endpoint, token, group_by device_roles/sites, query_filters logical-OR, device_query_filters has_primary_ip; Constructable compose/keyed_groups). docs.ansible.com/projects/ansible/latest/collections/netbox/netbox/nb_inventory_inventory.html · netboxlabs.com/blog/how-to-use-netbox-as-a-dynamic-inventory-source-for-the-red-hat-ansible-automation-platform/
  5. Ansible Community Documentation — Inventory caching with cache plugins (ansible.cfg [inventory] cache=true, cache_plugin=ansible.builtin.jsonfile / community.general.redis, cache_connection, cache_timeout; ansible-inventory --graph --flush-cache). docs.ansible.com/projects/ansible/latest/plugins/cache.html
  6. Ansible-core 2.19 Porting Guide — Data Tagging / inverted template trust model: variables from inventory plugins and other external sources are no longer trusted as templates by default (must be marked trust_as_template) — a 2025 security-hardening change relevant to inventory authors. docs.ansible.com/projects/ansible/latest/porting_guides/porting_guide_core_2.19.html
  7. Red Hat — EX294 Red Hat Certified Engineer (RHCE) exam objectives: "Manage inventory variables", "Manage task execution", build static and dynamic inventories on RHEL 9. redhat.com/en/services/training/ex294-red-hat-certified-engineer-rhce-exam-red-hat-enterprise-linux

What's next?

Your inventory now lists the right routers and switches automatically. Next: point a nightly playbook at them, pull every running-config, diff it against last night, and get a Telegram ping the moment something drifts.