Short answer: bootstrap a DNS inventory for domains that predate your automation capture, attach evidence and timestamps to every record, then schedule changes only after a second observation confirms ownership. The deciding constraint is deliverability: a hostname that answers today can still route mail or web traffic for a tenant you have not mapped yet.
I've been paged for missed jobs and duplicate deliveries. DNS work creates the same kind of surprise when an old zone is treated as empty because the automation database starts in 2026. The safe move is to make discovery a durable dataset, not a one-off import, and to make every later mutation idempotent.
How should a zone inventory capture domains before automation?
Start with the authoritative zone exports you already control, then query each discovered name from at least two independent resolvers. Store the owner name, record type, normalized value, TTL, resolver, response code, and observed time. A snapshot without provenance is just a guess with a timestamp attached.
The inventory needs an explicit state machine. observed means a query returned data; claimed means a tenant mapping has been reviewed; managed means the automation controller owns future writes; retired means the record is intentionally left in place but no longer reconciled. Do not jump from observed to managed in one run.
Pause here.
For mail, preserve DMARC, DKIM, and SPF records as first-class evidence. DMARC policy and reporting destinations can reveal an operational dependency even when the application database has no matching tenant. RFC 7489 defines the policy and reporting model; it does not tell you which internal team owns a domain, so that ownership check remains yours.
A small, repeatable discovery job
The following Go example keeps the discovery layer read-only. It records NXDOMAIN and SERVFAIL instead of treating either response as proof that a name is safe to delete. The input list can come from a zone export, certificate inventory, or a manually reviewed file.
package main
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"strings"
"time"
)
type Observation struct {
Name string `json:"name"`
Type string `json:"type"`
Addresses []string `json:"addresses,omitempty"`
Error string `json:"error,omitempty"`
Observed time.Time `json:"observed"`
}
func observe(ctx context.Context, name string) Observation {
name = strings.TrimSuffix(strings.ToLower(name), ".")
answers, err := net.DefaultResolver.LookupHost(ctx, name)
o := Observation{Name: name, Type: "A/AAAA", Observed: time.Now().UTC()}
if err != nil {
o.Error = err.Error()
return o
}
o.Addresses = answers
return o
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
names := os.Args[1:]
if len(names) == 0 {
fmt.Fprintln(os.Stderr, "provide at least one domain")
os.Exit(2)
}
for _, name := range names {
data, err := json.Marshal(observe(ctx, name))
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
}
This is intentionally boring. A production collector should also query the record types that matter to its service, retain the resolver identity, and rate-limit requests. It should never infer tenant ownership from an IP address alone. Shared ingress, CDN changes, and stale glue records make that shortcut unsafe. In a real backfill, the difficult record is often the one that looks harmless: an old CNAME still points at a shared edge, the tenant row was renamed, and a certificate inventory contains a second spelling of the same name. Keep those variants as separate observations until a reviewer can connect them. Collapsing them during import destroys the evidence needed to explain a later change. That extra detail is what lets an on-call engineer distinguish “never existed” from “not visible through this resolver.”
Turning observations into an idempotent schedule
Use a content hash over the normalized record set as the reconciliation key. A scheduled run can then ask, “Has the desired state changed since the last acknowledged write?” If the answer is no, it exits without making a DNS request. If the answer is yes, it creates a change proposal containing the old evidence, the proposed values, and an expiry for human review.
The controller should have one writer per zone. Queue retries must carry the same operation identifier, and the worker should record an acknowledgement before releasing the next operation for that zone. This is how you avoid duplicate deliveries when a timeout occurs after the provider accepted a change but before the worker received its response.
Keep discovery and mutation on separate credentials. The discovery job can run frequently; the mutator should run on a slower schedule with an approval gate for mail-related records. That split also gives you a clean rollback boundary: disabling the mutator does not erase the inventory.
Verification, rollback, and the uncomfortable cases
After a write, query authoritative nameservers and at least one recursive resolver. Compare the returned record set with the proposal, wait through the prior TTL where relevant, and check application probes from more than one network. A green API response is not deliverability evidence.
Watch for NXDOMAIN, SERVFAIL, unexpected CNAME chains, and DMARC reports that continue to reference an unmapped domain. Alert on drift, not just failed jobs. A missing observation is a reason to pause reconciliation, not a reason to delete a record.
The catch is that this workflow is not suitable when you need instant, bulk edits across zones with no ownership review. In that case, stick with a provider-native migration process and accept its change window; the operational risk is lower than inventing tenant mappings during an outage. I'm not sure a single resolver view can ever prove global propagation, so your verification should state which vantage points it covers and what remains uncertain.
The useful boundary is the point where evidence becomes a reversible intent. Automate collection, normalization, hashing, drift reports, and retry handling. Keep ownership claims, mail policy changes, and destructive cleanup behind review until the inventory has survived several scheduled runs.
That design costs a little more bookkeeping up front, but it gives on-call engineers a precise answer to three questions: what existed before automation, what changed, and which tenant approved the change. Those answers matter more than a fast first sync when a legacy domain is still carrying production traffic.
Top comments (0)