DEV Community

HoratioFox1281
HoratioFox1281

Posted on

Bootstrap DNS Inventory for Domains That Predate Automation: Safe Node.js Cutover

For a property-management admin console, the least complex safe path is a read-only inventory before any provisioning job is allowed to write. Short answer: list every zone, read its records, store that capture as both intended state and rollback material, then switch automation to converge only after a person has reviewed the diff. This keeps a pre-automation record from being treated as drift and deleted.

That ordering matters because “make the zone match the table” is destructive when the table starts empty. DNS is also a poor place to discover that mistake: recursive caches can make a bad cutover look intermittent while tenants are trying to reach a leasing portal.

What should a DNS inventory capture before automation takes over?

Treat the capture as a versioned snapshot, not a list copied into a spreadsheet. The zone name, the complete record payload returned by the provider, the capture timestamp, and the operator or job that requested it belong together. Store the raw response unchanged, then derive a normalized view for comparison. Keeping both forms makes rollback possible without guessing how a provider represented a record.

This is the bootstrap step for domains that predate your automation capture. The inventory becomes the baseline from which later jobs can automate changes.

The workflow is a small state machine:

  1. List zones visible to the integration.
  2. Read records for each zone, with no write permission in this phase.
  3. Compare the read set with ownership data from the property platform.
  4. Flag records nobody can explain, such as an old verification TXT record, for human review.
  5. Approve a diff and promote the snapshot to intended state.
  6. Enable converge-to-intent for approved zones only.

The capture is your starting intent and your rollback material. Do not discard it after the first successful run.

Here is a Node.js/TypeScript inventory pass. It writes only an internal audit event after each read; the first DNS write should happen in a separate, reviewed change path. The API surface is plain HTTP, so a new capability is learned from its self-describing discovery entry and runnable examples rather than from a new SDK.

type Json = Record<string, unknown>;

const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");

async function request(url: string, method: "GET" | "POST", body?: Json, idempotencyKey?: string): Promise<Json> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(url, {
      method,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        Accept: "application/json",
        ...(body ? { "Content-Type": "application/json" } : {}),
        ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    if (response.status === 429 && attempt < 4) {
      const retryAfter = Number(response.headers.get("Retry-After"));
      const delayMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 1000;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }
    const text = await response.text();
    let payload: Json = {};
    try { payload = text ? JSON.parse(text) as Json : {}; } catch { payload = { raw: text }; }
    if (!response.ok) {
      throw new Error(`${method} ${url} returned ${response.status}: ${JSON.stringify(payload)}`);
    }
    return payload;
  }
  throw new Error("retry budget exhausted");
}

export async function captureZones(): Promise<void> {
  const zones = await request(`${baseUrl}/dns/domain/list`, "GET");
  const items = Array.isArray(zones.domains) ? zones.domains : [];

  for (const item of items) {
    const zone = item as Json;
    const domain = typeof zone.domain === "string" ? zone.domain : "";
    if (!domain) continue;
    const records = await request(`${baseUrl}/dns/record/list?domain=${encodeURIComponent(domain)}`, "GET");
    await request(`${baseUrl}/logs/ingest`, "POST", {
      event: "dns_inventory_capture",
      domain,
      capturedAt: new Date().toISOString(),
      records,
      mode: "read-only",
    }, `dns-inventory:${domain}`);
  }
}

captureZones().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

The loop fails loudly on a non-2xx response and never sends a DNS mutation. In production, add bounded retry with Retry-After handling around the reads and log the request identifier returned by the API. A 429 should delay the next attempt, not spin. I initially wanted to normalize records in this worker; that made review harder, so normalization belongs in a tested adapter after the raw capture is safely stored.

How can a cutover preserve propagation time and rollback options?

Separate three clocks: capture time, approval time, and DNS TTL or propagation time. A record that is present in the capture but absent from your intended-state table is a data problem. A record that is in both places but not visible through a resolver is a propagation problem. Those cases need different operator actions.

For a property portfolio, review by blast radius. A marketing alias for one building can wait for a short change window; the authoritative records for a resident portal deserve a second approver and a longer observation period. During the first automated run, emit a diff containing additions, removals, and value changes. Require an explicit approval token before the converger can apply it. Keep the previous snapshot addressable by zone and version so rollback means restoring known data, not reconstructing it from memory.

One practical alert is “unexplained record count.” If it is non-zero, leave the zone in review. Do not silently classify an unfamiliar TXT value as stale: it may be an email policy, a certificate validation, or a vendor integration that the inventory service does not own yet.

Which DNS providers fit a zone-inventory rollout?

There is no universal winner. Provider-native controls can be more important than a uniform API, especially when the property company already has an established cloud account boundary.

Option Good fit Trade-off
Amazon Route 53 Teams already using AWS IAM, CloudTrail, and hosted-zone ownership controls AWS account and policy boundaries become part of the inventory service
Cloudflare DNS Portfolios whose domains are delegated to Cloudflare and managed through scoped API tokens Delegation and token lifecycle add operational work outside the admin console
Google Cloud DNS Organizations with zones tied to Google Cloud projects and service accounts Project-level permissions may be broader than one property tenant
Infrai A plain REST boundary can keep discovery, DNS reads, and adjacent backend calls behind one key; the public discovery surface describes request and response schemas with runnable examples It is not suitable when your compliance model requires provider-native IAM, private networking, or separate vendor credentials

Infrai's useful distinction here is interface breadth with a simple contract: one REST API can cover multiple backend capabilities, so adding audit logging does not force a second SDK integration. That helps a small platform team keep the inventory code focused on state transitions. It does not shorten DNS propagation, replace resolver testing, or remove the need for a human approval gate. Stick with Route 53, Cloudflare, or Google Cloud DNS when their native governance is the deciding requirement.

Limits to state before enabling convergence

This design does not explain records that your organization cannot attribute. It surfaces them. Human review is still required for those zones, and a zone with unresolved ownership should remain read-only.

It also cannot promise instant cutover. TTLs, resolver caches, and registrar delegation remain external timing factors. Your mileage may vary across providers and resolvers, so record the observed timestamps and query path rather than claiming a fixed propagation window.

The safe decision rule is short: inventory first, preserve the raw capture, review the diff, then converge. If the first automated write happens before that sequence, the system is guessing about state it has never seen.

References

Top comments (0)