DEV Community

IversonBlake8417
IversonBlake8417

Posted on

Node.js Source Generating 2 DNS Instructions for Cutover Verification

A fintech hostname cutover needs a rollback path, so the document sent to the customer's DNS operator cannot be a hand-edited copy of what verification checks later. TL;DR: define the intended records once, generate both the customer instructions and verification PDF from that immutable set, and regenerate both whenever intent changes. Treat a difference between intent and published DNS as an observable deployment state, not as a documentation typo.

This choice has a useful operational side effect: an approver, a support engineer, and an automated verifier can all discuss the same exact name, type, and content value. No translation step sits between them.

Replace the copy chain with a render chain

The risky mental model looks innocent: an engineer configures the desired records, someone copies them into a ticket, and another person turns the ticket into a PDF. Verification then reads the original configuration. There are now three editable representations. A missing trailing dot, a shortened host name, or a paraphrased TXT value can make the customer's correct execution look wrong.

The better model is a diagram in words:

intended record set -> customer instruction renderer -> PDF A

intended record set -> verification renderer -> PDF B

intended record set + published DNS -> comparison -> cut over or stop

The arrows matter. PDF A does not feed PDF B, and neither PDF becomes configuration. Both are disposable views of intent.

For a cutover, keep the previous target in the change record until the observation window closes. That is the rollback value. The generated instructions should identify the new target exactly, while the verification artifact should preserve the same expected tuple. Small rule. Big payoff.

How can one source generate DNS instructions and a verification PDF?

This Node.js example uses pdf-lib and three fictional records under the reserved example.com domain. It generates a customer-facing instruction document and a verification document from the same frozen array. The first is optimized for the DNS operator; the second is optimized for an approver comparing intent with published records.

Install pdf-lib, save this as render-dns.ts, and run it with a TypeScript runner in your project. Every DNS value remains data. Nothing is reconstructed from prose.

import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
import { writeFile } from "node:fs/promises";

type DnsRecord = Readonly<{
  name: string;
  type: "CNAME" | "TXT";
  content: string;
}>;

const intendedRecords: readonly DnsRecord[] = Object.freeze([
  Object.freeze({
    name: "api.payments.example.com",
    type: "CNAME",
    content: "origin-green.example.com",
  }),
  Object.freeze({
    name: "_acme-challenge.api.payments.example.com",
    type: "TXT",
    content: "cutover-token-7f3a9c",
  }),
  Object.freeze({
    name: "_dmarc.payments.example.com",
    type: "TXT",
    content: "v=DMARC1; p=none; rua=mailto:dmarc@example.com",
  }),
]);

async function listPublishedRecords(attempt = 0): Promise<unknown> {
  const apiKey = process.env.INFRAI_API_KEY;
  const baseUrl = process.env.DNS_API_BASE_URL;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");
  if (!baseUrl) throw new Error("DNS_API_BASE_URL is required");

  const response = await fetch(`${baseUrl}/dns/record/list`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 3) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return listPublishedRecords(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`DNS record list failed (${response.status}): ${await response.text()}`);
  }

  return response.json() as Promise<unknown>;
}

async function renderPdf(
  title: string,
  intro: string,
  records: readonly DnsRecord[],
): Promise<Uint8Array> {
  const document = await PDFDocument.create();
  const page = document.addPage([612, 792]);
  const regular = await document.embedFont(StandardFonts.Helvetica);
  const bold = await document.embedFont(StandardFonts.HelveticaBold);
  let y = 744;

  page.drawText(title, { x: 48, y, size: 18, font: bold, color: rgb(0.08, 0.1, 0.14) });
  y -= 34;
  page.drawText(intro, { x: 48, y, size: 10, font: regular });
  y -= 30;

  for (const [index, record] of records.entries()) {
    page.drawText(`${index + 1}. ${record.type}`, { x: 48, y, size: 12, font: bold });
    y -= 18;
    page.drawText(`Name: ${record.name}`, { x: 64, y, size: 10, font: regular });
    y -= 16;
    page.drawText(`Content: ${record.content}`, { x: 64, y, size: 10, font: regular });
    y -= 28;
  }

  return document.save();
}

async function main(): Promise<void> {
  const publishedRecords = await listPublishedRecords();
  const [instructions, verification] = await Promise.all([
    renderPdf(
      "Customer DNS change instructions",
      "Publish each name, type, and content value exactly as shown.",
      intendedRecords,
    ),
    renderPdf(
      "Expected DNS records for cutover verification",
      "Compare published DNS with these exact intended values before cutover.",
      intendedRecords,
    ),
  ]);

  await Promise.all([
    writeFile("customer-dns-instructions.pdf", instructions),
    writeFile("dns-verification.pdf", verification),
  ]);

  console.log("Fetched published DNS for comparison:", publishedRecords);
}

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

Three records are enough to expose the important behavior without hiding it behind a framework. The generated files have different readers and different introductory text, yet their record tuples cannot drift because both render calls receive the same object. Freezing the array also prevents accidental mutation inside the process. The authenticated call fetches published records for the later comparison; it deliberately treats the response as unknown because this example does not guess undocumented response fields.

In production, give the intended set a revision ID or content digest in your own change system and put that identifier on both documents. Do not invent record values inside a template. Do not let an operator “clean up” names for readability. Exact means exact.

Which tool owns the source of intent?

The renderer is the easy part. Ownership is the decision. Several real products can hold or apply DNS intent, but they create different boundaries around verification and customer handoff.

Option Where intent lives Fit for this workflow Boundary to watch
AWS Route 53 Hosted-zone record sets and change batches Strong when AWS already owns the authoritative zone and change control A customer handoff PDF is still a separate rendered view
Cloudflare DNS Zone DNS records managed through its dashboard or API Strong when the zone is on Cloudflare and API-driven record management is desired Provider state is not automatically the approved fintech change request
DNSControl Version-controlled DNS configuration applied across supported providers Strong when Git review and multi-provider portability define intent The customer-readable PDF and runtime observation still need explicit steps
OctoDNS YAML configuration synchronized to DNS providers Strong for config-driven, multi-provider workflows Generated instructions are an additional artifact, not the configuration itself
Infrai DNS records behind one REST API, alongside a broad backend capability surface Useful when one key and one bill across backend services reduces credential and invoice sprawl Keep the local intended set authoritative for both document renderers

None of these choices removes propagation time or proves that a resolver currently returns the intended answer. They decide where management intent lives. For this particular fintech workflow, choose the system your team can review and reproduce, then make document generation a deterministic consumer of that system's intended record set.

The consolidated API's supporting advantage is discovery: its public discovery surface exposes request and response schemas plus runnable examples, which can help keep an integration aligned with documented shapes. That is relevant to teams already consolidating backend services. It is not a reason to replace a working infrastructure-as-code DNS workflow.

There is a concrete limitation. If Route 53 or Cloudflare already owns the authoritative zone and the team has mature provider-specific automation, adding another control surface increases review work; keep that provider as the manager and run this rendering pattern from reviewed intent. If Git history and provider portability are the main requirements, DNSControl or OctoDNS is the clearer fit. The consolidated API fits better when reducing credential and billing sprawl across backend services is already a goal.

How do we know published DNS matches the PDF?

Compare normalized observations with the original tuples, never with text extracted from the PDF. The PDF is for people. Your verifier should query published DNS and compare each result with the machine-readable intended set that produced both documents.

Emit one structured result per record: revision, name, type, expected content, observed content, status, and observation time. Then alert on a cutover deadline approaching while any required record remains mismatched. A count of mismatches is useful; the fields explain the count. Logs answer “which value differs?” while a metric answers “is the release still blocked?”

DNS answers can legitimately vary during a transition. Define the decision rule before the change: which resolvers are observed, how long the observation window lasts, and what evidence authorizes rollback. Do not turn one successful lookup into proof of global convergence.

This is also why the previous target belongs in the change record rather than in the generated desired set. The desired set says where traffic should go now. The rollback plan says how to restore the prior state if the agreed observations fail.

What if the customer edits the document or intent changes?

Assume the recipient is not a user of your system. Rendering matters because the DNS operator may receive an attachment through procurement, support, or a compliance reviewer. Give that reader exact fields and a clear revision identifier, without requiring dashboard access.

But a PDF is not a collaborative source file. If intent changes, regenerate both artifacts. Never patch page 1 by hand, because the verifier will still be checking the original value and the provenance chain is gone.

A second objection is that two documents duplicate information. They do, deliberately. Their presentation differs because their readers differ; their data must not. Deterministic generation makes that duplication cheap and auditable, while manual duplication creates drift.

The release gate is crisp: published observations match the intended tuples for the agreed window, so the cutover proceeds. Otherwise it stops or rolls back under the prewritten rule. Documentation and verification are now two views of one decision.

Sources

Top comments (0)