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

Source: https://ai.techclick.in/blog_ansible_inventory_dynamic
Markdown: https://ai.techclick.in/blog_ansible_inventory_dynamic.md
Publisher: Techclick Infosec Pvt Ltd

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.

Ansible Inventory &amp; Dynamic Inventory: — Host Lists That Never Go Stale student learning map
                     A visual study map for Ansible Inventory &amp; Dynamic Inventory: — Host Lists That Never Go Stale showing learning path, evidence, traps, and practice sequence.

                     TECHCLICK STUDY MAP
                     Ansible Inventory &amp; Dynamic Inventory: — Host Lists...
                     Ansible · learn the flow, prove with evidence, avoid unsafe shortcuts

   1. Start
   🎯 By the end you will be able to

   2. Understand
   Pick where you want to start

   3. Prove
   ① Static inventory — the INI/YAML...

   4. Practice
   ② Why static goes stale — and what...

                     How to use this page
                     First build the mental model, then connect the concept to a realistic production decision. Finish by testing yourself.
                     Techclick Infosec Pvt Ltd | ai.techclick.in | Training Contact: WhatsApp +91 92772 29456

             Content-specific feature visual for this lesson: use it as the 60-second map before reading the full detail.

             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 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)

- @webservers web1, web2 @dbservers db1 @switches sw[01:12] → 12 hosts group_vars/ host_vars/ ansible_host · ansible_user ansible_network_os ⚠ the catch every new/removed host = a manual edit so the file drifts out of date stale / wrong inventory structure cost / decision where vars live tidy / 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. Reveal answer 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? a) Use sw[01:24] for all 24, then a separate range/group for the even ones b) Type all 24 hostnames — ranges only work up to 10 hosts c) Use sw* which auto-discovers the switches d) Ranges are an INI-only feature, so she must convert to YAML first 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:00 hosts.ini = 20 instances ; file is 100% correct the instant it is saved ▼ ② Mon 13:00 sale traffic spikes; ASG launches 8 new instances — not in the file ▼ ③ Mon 22:00 traffic drops; ASG terminates 11 instances — still listed in the file ▼ ④ Tue 09:00 play runs against hosts.ini : hits 11 ghosts , misses 8 real hosts Press Play to step through the healthy path. Then press Break it . ▶ Play Next ▶ ⚠ Break it ↺ Reset 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 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.yml plugin config AWS / NetBox API (truth) query at run time plugin builds the list fresh inventory, every run ✓ new hosts appear automatically ✓ dead hosts drop off automatically no 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. Reveal answer 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? a) It makes ansible-inventory --graph run slower b) Static files cannot be encrypted with ansible-vault c) YAML inventories expire after 24 hours by design d) A terminated host's IP can be reused by another account, so your play may configure a stranger's box 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: &#123;&#123; public_ip_address | default(private_ip_address) &#125;&#125; 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: ram@netauto:~/inventory$ ansible-inventory -i prod.aws_ec2.yml --graph . (Recreated for clarity — your terminal matches this.)"> 🖥️ 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: "&#123;&#123; lookup('env', 'NETBOX_TOKEN') &#125;&#125;" # 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/.yaml — prod.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. Reveal answer 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? a) keyed_groups, because it sets connection variables b) groups, with a Jinja2 conditional c) compose: ansible_host: public_ip_address | default(private_ip_address) d) The regions: setting, by choosing the nearest region 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 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 NetBox device source of truth AWS API EC2 source of truth nb_inventory plugin aws_ec2 plugin cache jsonfile · 1h merged inventory @device_roles_edge_router @sites_mumbai_dc @role_web groups built themselves ansible-inventory --graph the 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 collision empty tree, error, or two sources fighting for the same group name failure mode plugin / cache verify / decide source of truth verified 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 &#123;&#123; ... &#125;&#125; 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 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 dynamic static = hand INI/YAML, fixed labs dynamic = plugin queries live source children = nested groups dynamic never goes stale The 4 plugins amazon.aws.aws_ec2 → AWS azure.azcollection.azure_rm → Azure google.cloud.gcp_compute → GCP netbox.netbox.nb_inventory → NetBox Auto-build groups keyed_groups: 1 group / tag value compose: set ansible_host etc. groups: custom Jinja2 bucket no host lists to maintain Verify commands ansible-inventory --graph ansible-inventory --list ansible-inventory --host NAME --flush-cache (ignore cache) Pattern targeting web:db → union (OR) web:&prod → intersection (AND) prod:!mumbai → exclude order: : then & then ! · use --limit Gotchas file must end *.aws_ec2.yml enable_plugins (keep defaults!) distinct prefix → no group clash 2.19: ext-source vars not trusted The one sentence to remember Static inventory = a file you must edit; dynamic inventory = a plugin that asks the live source 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?" a) Trust the play's hosts: line — dynamic inventory is always right b) Run ansible-inventory --graph --limit role_web:&prod --list-hosts and read exactly which hosts match before running c) Disable caching so it's slower but safer d) Edit hosts.ini to lock the list first 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. INI or YAML for my inventory — which should I actually use? What's the difference between a dynamic inventory plugin and the old inventory scripts? Why does my aws_ec2.yml inventory file get ignored with "Could not parse"? keyed_groups vs compose — when do I reach for each? A new branch of switches appeared in NetBox but my play skipped them — why? How much of RHCE EX294 does inventory actually cover? 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? a) ungrouped b) all c) webservers d) localhost 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? a) sw[01:20].infosys.local b) sw01,sw02,...,sw20 on one comma-separated line c) sw* (let Ansible discover them) d) You must use a dynamic inventory plugin; ranges are not allowed in static files 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? a) keyed_groups:\n - key: public_ip_address b) filters:\n instance-state-name: running c) compose:\n ansible_host: public_ip_address | default(private_ip_address) d) regions:\n - ap-south-1 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? a) The AWS credentials expired mid-run b) Caching returned an empty list c) YAML inventories cannot define a webservers group d) The plugin built the group as role_web (from keyed_groups prefix), so the group "webservers" does not exist in this inventory 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? a) Both plugins auto-built a group literally called "web"; give each a distinct prefix (e.g. role_ vs device_roles_) so they cannot collide b) Ansible randomly merged groups; restart the control node c) NetBox overwrote the AWS inventory; delete the NetBox config d) The cache is corrupt; run --flush-cache and the groups separate 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? a) A — a nightly file is simpler and good enough b) B — the plugin queries the source at run time, so the inventory is correct for THIS run and groups self-build; the nightly file is stale all day after midnight c) A — cron is more reliable than API calls d) Identical — both end up with the same host list 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. Submit all answers Try again 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. Compare with expert answer 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. Generate my one-liner 📩 Quiz me on this in 7 days. Opt in and we'll email 3 micro-questions on Inventory & Dynamic Inventory at Day 1, Day 7 and Day 30 — spaced repetition is how this sticks. Un-tick any time. ### 📖 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 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

- 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

- 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

- 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/

- 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

- 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

- 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.

                 Next · Ansible for Config Backup & Compliance: Catch Drift Nightly →
                 Practice on exam.techclick.in →

---
Cite this Techclick lesson with the source URL. Do not invent fees, batch dates, or job guarantees.
Browse all lessons: https://ai.techclick.in/blogs
AI index: https://ai.techclick.in/llms.txt
