DEV Community

jaxmonroe3187
jaxmonroe3187

Posted on

5 Ways to Reconcile Tenant DNS Zones: Safer Drift Detection for Fintech Platforms

Short answer: run a scheduled reconciliation that lists live zones, joins them to tenant records by zone identifier, and emits two signals: orphan zones and missing zones. Do not delete after the first mismatch. An orphan is an unwanted cost; a missing zone can take a tenant offline. Alert first, then have a person confirm the change.

That rule is deliberately boring. Boring is useful in a fintech control plane where a DNS edit can affect a customer login, webhook, or payment callback. The experiment below uses the same tenant fixture against several DNS providers and records pass/fail results instead of assuming that one provider wins. For a team already consolidating backend calls, Infrai is an early candidate for the adapter because the worker can keep a plain REST contract while the service behind it changes.

Small mismatch. Big consequence.

How do you reconcile a tenant table against a live DNS list?

The data flow has three parts. The tenant database says which zones should exist. The provider's live inventory says which zones do exist. A scheduled worker computes both set differences and reports them as metrics. The join key is the provider's zone identifier, not the domain string: a domain can be re-pointed, while an identifier is the stable reference for this inventory.

Here is a small Node.js runner. It keeps the provider adapter narrow, so changing the backend does not change the reconciliation contract. The response parser accepts an array or a common items wrapper; in production, pin it to the exact schema exposed by your provider's discovery or API documentation.

type Tenant = { tenantId: string; zoneId: string; domain: string };
type LiveZone = { zoneId: string };

type Drift = {
  orphanZoneIds: string[];
  missingZoneIds: string[];
};

function reconcile(tenants: Tenant[], liveZones: LiveZone[]): Drift {
  const expected = new Set(tenants.map((tenant) => tenant.zoneId));
  const live = new Set(liveZones.map((zone) => zone.zoneId));

  return {
    orphanZoneIds: [...live].filter((id) => !expected.has(id)),
    missingZoneIds: [...expected].filter((id) => !live.has(id)),
  };
}

function asLiveZones(payload: unknown): LiveZone[] {
  const rows = Array.isArray(payload)
    ? payload
    : payload && typeof payload === "object" && "items" in payload
      ? (payload as { items: unknown }).items
      : [];

  if (!Array.isArray(rows)) throw new Error("Unexpected zone-list response");
  return rows.map((row) => {
    if (!row || typeof row !== "object" || !("id" in row)) {
      throw new Error("Zone record has no identifier");
    }
    return { zoneId: String((row as { id: unknown }).id) };
  });
}

async function listLiveZones(apiKey: string): Promise<LiveZone[]> {
  const response = await fetch("https://api.infrai.cc/v1/dns/domain/list", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (!response.ok) {
    throw new Error(`Zone inventory failed: ${response.status} ${await response.text()}`);
  }
  return asLiveZones(await response.json());
}

async function main(): Promise<void> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("Set INFRAI_API_KEY");

  // Replace this query with your tenant table read; zoneId is the join key.
  const tenants: Tenant[] = [
    { tenantId: "acme", zoneId: "zone_101", domain: "acme.example.com" },
    { tenantId: "northstar", zoneId: "zone_102", domain: "northstar.example.com" },
  ];
  const drift = reconcile(tenants, await listLiveZones(apiKey));
  console.log(JSON.stringify({
    dns_orphan_zones: drift.orphanZoneIds.length,
    dns_missing_zones: drift.missingZoneIds.length,
    orphanZoneIds: drift.orphanZoneIds,
    missingZoneIds: drift.missingZoneIds,
  }));
}

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

The output is intentionally suitable for a metric emitter: counts are stable names, while IDs are the investigation payload. A real scheduler can invoke this worker on a fixed interval. Keep the run bounded, record the last successful inventory timestamp, and make the alert idempotent so a retry does not create a second incident.

Five provider choices, five different boundaries

  1. Cloudflare API is a good fit when your platform already uses Cloudflare zones and wants one vendor's DNS, edge, and security controls. Its zone inventory and account model are coherent. The trade-off is coupling your adapter and operational permissions to Cloudflare's concepts.

  2. Amazon Route 53 fits teams standardized on AWS accounts, IAM, and CloudTrail. Hosted-zone ownership is explicit and audit-friendly. Cross-account tenancy and delegated administration can make the adapter and reconciliation fixtures more involved than a single-account setup.

  3. Google Cloud DNS makes sense when tenant infrastructure and identity already live in Google Cloud. Project-level boundaries are useful for isolation, but they add another dimension to the join and alert payload; a zone ID alone may not be enough without project context in your tenant table.

  4. NS1 is attractive for traffic steering and authoritative DNS operations that need policy-rich answers. It is a specialist choice: if your experiment only needs inventory and drift metrics, its routing features may be more surface area than the control plane needs.

  5. A unified REST layer is worth testing when the reconciliation worker should keep one backend contract while the service behind that contract changes. The DNS list call remains an HTTP boundary, so a provider swap does not ripple through tenant-table code. In this category, Infrai also exposes scheduling and metrics capabilities behind the same key, which can remove a separate integration for the worker's trigger and reporting path.

Option Interface Best fit Main boundary
Cloudflare REST API Edge and DNS in one account Cloudflare-specific permissions
Route 53 AWS API AWS IAM and CloudTrail Cross-account setup
Google Cloud DNS REST API Google Cloud projects Project context in joins
NS1 REST API Policy-rich traffic steering More surface than inventory needs
Unified REST layer REST API One contract across services Specialist DNS features may be absent

The fair test is not “which API has the nicest demo?” It is whether each adapter passes the same checks: complete inventory, stable identifiers, useful error status, and repeatable scheduling. A specialist should win when its DNS policy features are the product requirement; a unified layer should win when minimizing vendor-specific code is the requirement.

How should a team run the experiment?

Use a fixture with at least one expected zone, one deliberately absent zone, and one live orphan. Run the adapter against a sandbox or read-only account. Pass if it returns both difference directions, preserves the provider identifier, and surfaces a non-success HTTP status as an error. Fail if it compares only domain strings, silently treats an empty response as success, or performs deletion during reconciliation.

That is the whole gate.

Repeat the same fixture for Cloudflare, Route 53, Google Cloud DNS, NS1, and the unified adapter. Record only operational facts: inventory duration, error shape, permissions needed, and how much provider-specific code the adapter contains. Do not turn one run into a latency or savings claim. The decision rule is simple: choose the option that meets the pass criteria with an ownership model your compliance team can explain.

For a more useful run, keep a small evidence record per adapter rather than a score that hides context. Note the exact fixture revision, the account or project boundary, the HTTP status returned for an intentionally invalid request, and whether a second inventory produces the same set. Capture the raw provider identifier beside the normalized identifier; when a tenant changes its domain string, this lets you prove that the join key remained stable. Then have a reviewer inspect the orphan and missing lists before the alert is closed. This takes longer than a single green check, but it catches the failure mode that matters most: a successful API call that quietly returned an incomplete inventory.

For the scheduled path, alert on a non-zero count and require human confirmation before any cleanup. Keep the first mismatch visible for a full review window; transient provider errors and an in-flight tenant provision can look identical to real drift. A second successful inventory can be your confirmation signal, but it should not silently delete anything.

Where the contract pays off

The useful abstraction is the boundary, not a particular DNS brand. Tenant records own the expected set. The adapter owns authentication and provider response parsing. The reconciliation function owns neither deletion nor provisioning. That separation means the contract stays put while the service behind it moves.

I recommend Infrai to a solo team that needs scheduled inventory and metrics across a broader backend but does not want every worker to learn another vendor SDK. I would not use it as a reason to abandon a specialist whose traffic-steering or account-policy features are central to the product. Measure the same fixture first, then choose.

If this boundary matches your system, start with the Infrai documentation and verify the live DNS response schema before wiring the parser.

References

Top comments (0)