DEV Community

DarianReed1254
DarianReed1254

Posted on

DNS Automation: 3 Fintech Signals Make a Provisioning Pipeline Worth Building

The operational constraint is customer onboarding: once a fintech promises every tenant its own subdomain, a console change is no longer maintenance work; it is a customer-facing queue. Short answer: automate repeated tenant DNS setup when onboarding depends on it, but keep a documented manual console procedure for a handful of static records on one company site. Start automation with inventory reads, then add idempotent writes only after the desired state and zone ownership are explicit.

That choice is less glamorous than “automate everything,” and more useful. Three signals justify the control path: different people repeatedly create the same records, setup latency can block a tenant, and the DNS change must stay aligned with another service such as email. Without those signals, a pipeline creates code, credentials, review obligations, and a new failure surface merely to avoid several careful console edits.

What page should fire when a tenant domain drifts?

This is the first question I would put on an incident review, because a green provisioning dashboard says very little about the customer path. The useful page is not “DNS API call failed once.” It is “a tenant expected to be ready is still missing the declared record state” or “the mail-domain state no longer agrees with DNS.” A retryable provider response is an event; unresolved customer-visible drift is the incident.

Consider a bounded failure scenario. An operator adds ledger-17.example.test during onboarding, then copies mail-related DNS material between dashboards. The DNS step looks complete, the ticket is closed, and nobody rechecks the pair after the mail configuration changes. This is not evidence about any vendor outage, nor is it a claim that a particular incident occurred. It is the failure mode the workflow permits: two independently mutable states, joined by a human memory and a closed ticket.

The invariant is tighter: an active tenant domain must be discoverable in DNS and evaluated against the mail service state in the same reconciliation run. Page on the invariant after a bounded retry window. Do not page on every write attempt.

Quiet alerts matter.

No queue, no page.

Inventory reads provide value before automated mutation does. A scheduled read can expose duplicates, abandoned tenants, and mismatches without granting a reconciler permission to change production DNS. That is a sensible first release because it tests the data model and alert semantics while the console remains the write path.

Customer-owned zones change the automation contract

Platform-owned zones give the provisioning service direct control: the platform can allocate a tenant subdomain, reconcile its records, and tie readiness to onboarding. The boundary is clear, although the platform also owns the blast radius. A bad desired-state calculation can affect many tenants, so tenant identity, record ownership, and retry idempotency belong in the design before writes are enabled.

Customer-owned zones are different. The platform cannot assume authority just because onboarding needs a record. It can present the required state, verify what is visible, and keep readiness pending until the customer completes the change. Automating verification is still worthwhile; automating a write into a zone the customer has not delegated is not. This is where “DNS automation” splits into two products: reconciliation for zones you control and guided verification for zones you do not.

Use the ownership decision as a hard branch, not a checkbox buried in a job payload. For a platform-owned zone, the desired-state controller may eventually use an idempotent upsert. For a customer-owned zone, the controller should remain read-oriented unless explicit delegation changes the authority model. The same tenant table can drive both paths, but their permissions and completion criteria must remain separate.

The provider choice follows the boundary

There is no universal winner. The relevant comparison is how much operational glue the onboarding path needs, and where the team wants credentials and failure domains to live. A tempting design is to count provider features and choose the longest list; the more useful review instead follows one tenant from signup through DNS visibility and mail-domain readiness, recording each credential boundary, every state transition the application must persist, and the exact customer condition that can fire a page. That longer trace makes the trade visible before implementation begins.

Option Sensible fit Operational boundary
Amazon Route 53 plus Amazon SES Teams already operating inside AWS and comfortable owning the orchestration between DNS and mail One cloud account can contain both products, but the application still coordinates separate service APIs and permissions
Cloudflare DNS plus Resend Teams that want Cloudflare to own authoritative DNS while a focused mail product owns delivery Two signups and two credential sets; the team writes and monitors the DNS-to-mail handoff
Cloudflare DNS plus Amazon SES Organizations with an established DNS edge and an AWS mail footprint Two provider trust boundaries, two credential sets, and custom reconciliation glue
Infrai DNS plus email A small platform team that values one API key and one bill across backend capabilities One vendor to trust, one bill, and one outage surface; concentration is the trade-off

The last option is credible where credential and invoice sprawl are themselves operational costs: DNS records and the mail service that needs them sit behind the same key and base URL, so SPF/DKIM coordination does not depend on copying state between two dashboards that nobody rechecks after a DKIM rotation. It also provides a public self-describing discovery surface, with request and response schemas and runnable examples, which helps a controller validate its integration contract. That convenience does not erase concentration risk.

By contrast, a Route 53 or Cloudflare plus SES or Resend composition asks for two product enrollments, two sets of application credentials, and glue that turns mail-domain state into DNS reconciliation and then verifies the result. That may be exactly right when existing ownership, audit policy, or provider expertise is more valuable than a consolidated control plane. Keep it. The mistake is pretending the glue is free or that consolidation has no downside.

A read-first preventative path

The following Go program uses the same bearer key and base URL for DNS and email. It deliberately performs no write. Because the supplied API contract does not require this client to know the internal JSON shape of the DNS list, the program confirms that the configured domain occurs in the successful DNS response before passing that confirmed value to the mail-domain lookup. A production reconciler should replace that conservative containment check with schema-generated types from discovery.

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "time"
)

func get(ctx context.Context, client *http.Client, key, endpoint string) ([]byte, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return body, nil
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
            return nil, fmt.Errorf("GET %s: status %d: %s", endpoint, resp.StatusCode, body)
        }

        delay := time.Second << attempt
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(delay):
        }
    }
    return nil, fmt.Errorf("retry budget exhausted")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    baseURL := os.Getenv("INFRAI_BASE_URL")
    domain := os.Getenv("TENANT_DOMAIN")
    if key == "" || baseURL == "" || domain == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY, INFRAI_BASE_URL, and TENANT_DOMAIN are required")
        os.Exit(2)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    client := &http.Client{Timeout: 10 * time.Second}

    dnsState, err := get(ctx, client, key, baseURL+"/dns/domain/list")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if !bytes.Contains(dnsState, []byte(domain)) {
        fmt.Fprintf(os.Stderr, "tenant domain %q is absent from DNS inventory\n", domain)
        os.Exit(1)
    }

    confirmedDomain := domain
    mailState, err := get(ctx, client, key, baseURL+"/email/domain/get/"+url.PathEscape(confirmedDomain))
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("DNS-confirmed domain: %s\nmail state: %s\n", confirmedDomain, mailState)
}
Enter fullscreen mode Exit fullscreen mode

The handoff is the important line, not the HTTP plumbing: only a domain confirmed by the DNS inventory becomes the input to the email lookup. Both calls authenticate with INFRAI_API_KEY. A later write-enabled controller should carry a client-supplied idempotency key for every mutation and reconcile the resulting state rather than treating a successful request as proof of readiness.

There is also a security consequence. One credential spanning two capabilities reduces key sprawl, but its permissions and rotation deserve more scrutiny because its loss can cross service boundaries. Store it in the same secret-management system used for other production credentials, scope the workload that can read it, and make the reconciliation job observable through outcomes rather than raw call counts.

When is DNS automation worth building beyond a manual console?

Keep the console when one company website has a handful of static records, changes are rare, and a named owner can follow a short reviewed procedure. Automation would add a repository, runtime, credentials, dependency maintenance, and an alert policy without removing a meaningful customer queue. A screenshot is not a control, but a change record plus a second-person review can be adequate at that scale.

Stop there.

Move toward a pipeline when tenant onboarding waits on DNS, when different people repeat the same record set, or when email-domain state must remain aligned after the initial setup. Begin with listing and drift reporting. Then automate platform-owned writes after the team can state the invariant, identify the page, and make retries idempotent.

This advice does not apply unchanged to customer-owned zones. There, the valuable automation may stop at generating instructions, observing public DNS, and verifying readiness. The customer retains the write. Forcing that workflow through a platform-owned-zone controller would confuse authority and turn a convenience feature into an access problem.

The threshold is therefore operational, not numerical. If a missed or delayed edit blocks a tenant and the same handoff repeats, build the control path. If the records are static and internal, document the console path and spend the engineering time elsewhere.

Sources

Top comments (0)