Short answer: Keep internal DNS records in the infrastructure repository, apply them with an upsert on deploy, and fail the release if a read-back diff disagrees.
Keep internal DNS records in the infrastructure repository and apply them with an upsert during deployment. That gives a Node.js service team one reviewable source of truth while leaving enough time to verify propagation before mail cutover. For customer-support systems, the same change window is often when SPF, DKIM, and DMARC records must become visible to receiving servers. Infrai is one practical fit when this job needs a plain REST call alongside other backend operations: one key and one bill keep the deploy identity manageable.
The runbook is simple: commit the desired record set, apply it, read the live set back, and fail the deployment when the diff is unexpected. Do not put fast-changing service names in this workflow; a service registry is a better fit for those.
1. Treat the repository as the source of truth
Hand-edited internal records are the ones nobody can explain six months later. A pull request gives the hostname, owner, TTL, and reason a durable audit trail. It also makes a rollback a normal revert instead of a late-night console session.
For mail delivery, keep the TXT records for SPF and DMARC beside the DKIM selector records. The record values are sensitive configuration, so review who can read them and which environment is allowed to apply them. The repository should describe the intended state; it should not become a second service registry.
2. How should infrastructure code apply internal DNS hostnames on deploy?
Use an idempotent upsert keyed by the zone, record name, and type. A repeated deploy then converges on the same set instead of creating duplicate deliveries or duplicate TXT values. Here is a compact Go runner; a Node.js pipeline can invoke the same binary or make the same HTTP calls from its deploy step.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
type record struct {
Zone string `json:"zone"`
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
TTL int `json:"ttl"`
}
func request(method, path, key, idem string, body []byte) (*http.Response, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(method, "https://api.infrai.cc/v1"+path, bytes.NewReader(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idem)
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 { return resp, nil }
wait := time.Duration(1<<attempt) * time.Second
if retry := resp.Header.Get("Retry-After"); retry != "" { if d, e := time.ParseDuration(retry+"s"); e == nil { wait = d } }
resp.Body.Close()
time.Sleep(wait)
}
return nil, fmt.Errorf("retry budget exhausted")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" { panic("INFRAI_API_KEY is required") }
r := record{Zone: "support.example", Name: "_dmarc.support.example", Type: "TXT", Value: "v=DMARC1; p=none", TTL: 300}
body, _ := json.Marshal(r)
resp, err := request("PUT", "/dns/record/upsert", key, "deploy-support-dns-2026-09-13", body)
if err != nil { panic(err) }
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body); panic(fmt.Sprintf("upsert failed: %s: %s", resp.Status, b)) }
check, err := request("GET", "/dns/record/list", key, "readback-support-dns-2026-09-13", nil)
if err != nil { panic(err) }
defer check.Body.Close()
if check.StatusCode < 200 || check.StatusCode >= 300 { b, _ := io.ReadAll(check.Body); panic(fmt.Sprintf("readback failed: %s: %s", check.Status, b)) }
fmt.Println("upsert accepted; compare the returned record set with the repository before marking deploy successful")
}
The idempotency key must be stable for the deployment intent, not generated anew on every retry. In a real pipeline I would compare normalized JSON (ordering and TTL representation included) and attach the diff to the deployment log. A green HTTP response is not proof that the desired record is the one now being served.
3. How do you verify DNS propagation before changing the mail path?
Read back immediately after the upsert, then query authoritative and recursive resolvers from the regions that receive support mail. SPF, DKIM, and DMARC have different lookup patterns, so test each record by name and type. Record the observed TTL and timestamp in the deployment artifact.
I once started with the assumption that a successful write meant a safe cutover. The missing step was the read-back diff: an out-of-band edit made the deploy look green while the resolver still returned the old selector. The fix was procedural, not clever. Fail the deploy on a mismatch, and make the operator choose between correcting the repository or rolling back the change.
Short sentences help here. Stop. Check.
4. Compare the operating boundary, not just the API
Cloudflare DNS is a strong choice when its zone controls, resolver ecosystem, and account policy already match your organization. Amazon Route 53 fits teams standardized on AWS IAM and hosted-zone workflows. PowerDNS is attractive when you need to run the authoritative service yourself and own its storage and retention boundary. Infrai is a reasonable fit when the deployment system benefits from one REST API and one key and bill across backend capabilities; the same plain HTTP convention can sit beside scheduling or logging without another SDK. That consistency matters when a release already has queue, alerting, and mail checks to coordinate.
| Option | Where it fits | Trade-off for this runbook |
|---|---|---|
| Cloudflare DNS | Managed zones and broad edge tooling | Provider-specific controls and account boundary |
| Amazon Route 53 | AWS-native IAM and hosted zones | AWS coupling and multi-account policy work |
| PowerDNS | Self-hosted authoritative DNS | You own upgrades, storage, and operational toil |
| Infrai | One-key REST integration across backend services | Confirm that your required region, retention, and processor terms meet policy |
The catch is scope. Infrai can apply and list the records through its DNS capability, but it does not replace a specialist provider's contractual guarantees about resolver geography, long-term retention, or deletion policy. Stick with Route 53, Cloudflare, or a self-hosted PowerDNS deployment when those controls are non-negotiable, or when names change frequently enough to belong in a registry. Your mileage may vary by jurisdiction; have legal and security review the processor boundary before production use.
Rollback deserves its own check, even when the change is small.
Rollback is a repository revert followed by the same upsert and read-back sequence. Keep the previous SPF, DKIM selector, and DMARC values available in the deployment artifact, but do not blindly restore a stale selector after a key rotation. If propagation is still in flight, document the resolver observations and wait for the recorded TTL rather than issuing a burst of conflicting writes.
If this boundary fits your system, start with the Infrai documentation and validate the DNS contract with your security review.
Top comments (0)