Your DNS zone is already an inventory: the log0u7.dns collection
Every static inventory I have ever seen started as truth and became fiction within a quarter. The machines move, the inventory file does not. Cloud plugins solve this by asking the cloud, but my fleets live across datacenters and ISPs, behind whatever DNS the company already runs. Which leads to an uncomfortable observation: the DNS zone already contains the inventory. Every host has a record; what is missing is Ansible reading it.
So I wrote log0u7.dns (v2.0.1, MIT): an Ansible collection whose inventory plugin builds hosts from DNS records. The design constraint that shaped everything: no zone transfers (AXFR). AXFR is the door you do not leave open on a public zone and requiring it would make the plugin useless against exactly the DNS servers you do not control.
Four discovery methods, combinable
- TXT manifest:
_ansible-hosts.<zone> TXT "web-01,web-02,db-01", the historical norm, one record that lists the fleet. - SRV records:
SRV _ansible._tcp.<zone>with targets as hosts; a non-22 port in the target becomesansible_portautomatically. - DNS-SD (RFC 6763): a PTR walk at the service name to enumerate instances, then one SRV per instance. This is the part I could not find anywhere: I wrote a prior-art table in the architecture doc and the honest summary is that none of the no-AXFR implementations (2016 Node.js, 2016 Python, 2019 Go) use DNS-SD. It is also the one that scales: one record per instance, standard queries, nothing bespoke.
- Naming patterns:
prod-web-{:02d}probed over a numeric range, for the fleets that were provisioned by script and therefore have script-shaped names.
All four can be enabled together; their host lists merge, deduplicated.
Metadata: TXT records are your hostvars
Per-host resolution happens in parallel: A record, then AAAA fallback, then a TXT record at <host>.<zone> parsed as key=value pairs. The parser types what it finds: true/false booleans, integers, JSON arrays, comma lists, bare flags. Reserved keys (ansible_host, ansible_port, dns_hostname) cannot be overridden by TXT, so a zone record cannot hijack connectivity.
Grouping comes from the naming convention itself: prod-web-01 ends up in prod, web, prod_web and whatever your group_mappings says (web: [webservers, frontend]). The convention you already enforce on hostnames becomes the grouping you get in Ansible, for free.
The concurrency detail worth publishing
Host resolution runs on a ThreadPoolExecutor (default 10-15 workers) and every worker gets its own dnspython Resolver. The reason is boring and important: dnspython resolvers are not thread-safe and the failure mode of sharing one is intermittent corruption that looks like a DNS server problem. One resolver per thread; the parallelism comes from the pool, not from sharing state.
Caching is not invented either: the plugin uses Ansible’s native inventory_cache fragment, so any cache backend (jsonfile, redis
) and its TTL just work.
The reverse path: inventory to zone
The collection also ships scripts/inventory_to_dns.py (432 lines): it reads any inventory, static or dynamic, through ansible-inventory --list and emits a BIND-compatible zone file (SOA/NS, discovery TXT/SRV, A/AAAA per host, a TXT record per variable). Migrating to DNS-as-inventory is therefore a one-way door you can open slowly: generate the zone from what exists today, let both inventories coexist, cut over when they agree.
The converter also runs secret-leak heuristics on variable names before publishing them (a var called api_key gets warned about; vault:// and openbao:// references are exempt because they are pointers, not material).
The honest caveat
If your zone is public, your inventory is public: hostnames, IPs and every metadata TXT record become readable by anyone who queries the resolver. The security doc says it bluntly: DNS-as-inventory is for hosts you are comfortable publishing, or for zones that live on private resolvers with the same plugin pointed at them. The resolvers are configurable; the defaults are public on purpose, because the integration tests run against a local dnsmasq and public fleets against public DNS.
DNS knew already
- The inventory already exists; it is called DNS. Stop maintaining a second copy of it in YAML.
- AXFR is a privilege, not a dependency. Design the read path so it works with zero server-side changes.
- Prior art first, novelty second. The DNS-SD walk is the only genuinely new bit and it exists because I tabulated what 2016 and 2019 did not do.
- Threads + shared state = the bug that blames the network. Per-thread resolvers are cheaper than intermittent mysteries.
[edit: 2026] The Ansible implementation of all this: the unbound role .