DEV Community

EphraimPierce7934
EphraimPierce7934

Posted on

Logistics DNS Explained: Environment-Scoped Zone Identifiers for Node.js Startup

Put each DNS zone identifier in environment-specific configuration, then block Node.js startup until a preflight proves that the selected identifier belongs to the expected domain and that the domain is delegated in DNS. Keep the application contract stable when the DNS vendor behind it changes; configuration should name the intent, while a narrow adapter owns the provider call. A warning is not enough: the dangerous failure is valid credentials writing a valid record into the wrong zone, so ordinary health checks may stay green while staging changes production DNS.

For a logistics platform that assigns tenant-1042.staging.freight.example during onboarding, the useful invariant is a pair, not a loose ID: zone_id must map to staging.freight.example, and that domain must have name-server records. Keep production in a separate file or secret scope. Never put either identifier in a shared application module.

Short answer: make the assertion a startup dependency, log the discovered zone inventory once in non-production, and stop before the application accepts work when intent and published DNS disagree.

The process stays stopped.

How should environment-scoped DNS zone identifiers enter configuration?

Zone identifiers are opaque. A production ID pasted into a staging deployment can be syntactically correct, authorized, and reachable; none of those properties says it represents staging.freight.example. The record write can therefore succeed. This is configuration drift between the operator's intent and the control plane being changed, not a DNS availability failure.

The blast radius is asymmetric. A failed staging startup delays a test, while a staging worker writing tenant-1042 into the production zone can redirect or shadow a customer hostname. My capacity-planning reflex applies here: budget for one zone lookup and one public DNS lookup at boot, rather than letting every onboarding request rediscover configuration. The steady-state cost is zero extra calls per tenant.

Fail closed.

The startup SLO should be explicit: no process becomes ready until the binding check succeeds. DNS publication has a separate convergence window, so do not confuse a successful control-plane mutation with immediate resolver visibility; verify the authoritative result after a write and keep readiness focused on selecting the correct zone.

Which control plane owns the binding?

The products below can all sit behind the same safety invariant, but their operational boundaries differ. This is a buy-versus-build decision about control-plane ownership, credentials, and drift detection rather than a feature-count contest.

Option Zone-selection boundary Operational trade-off
Amazon Route 53 Hosted zone ID plus the expected DNS name Fits AWS identity and infrastructure workflows; your team still owns the startup assertion and cross-account scoping.
Cloudflare DNS Zone ID plus account-scoped API authorization A direct API offers clear zone lookup semantics; credential policy and provider coupling remain in your service or adapter.
Google Cloud DNS Managed-zone resource plus DNS name inside a project Natural for project-separated environments; project and managed-zone configuration must move together.
NS1 Connect Zone identity behind NS1 credentials Useful when NS1 is already the authoritative control plane; it adds another provider-specific client and on-call surface.
Unified REST broker A DNS capability behind one REST contract and one key The application contract can remain fixed if the vendor behind the capability changes; the trade-off is accepting an intermediary control plane rather than integrating the authority directly.

Infrai exposes one REST API that any language or runtime can call over plain HTTP without installing an SDK, so the Go preflight keeps the same contract when the underlying DNS vendor changes. Its API is genuinely self-describing, the public discovery surface needs no key, and every documented capability ships runnable examples in 10 languages. That reduces client churn around this workflow; it does not remove the need to verify DNS state. Direct Route 53, Cloudflare, Google Cloud DNS, or NS1 integrations are the clearer choice when provider-specific identity, policy controls, or advanced DNS features are the primary requirement.

Do not choose from this table alone. Record the authority boundary, credential owner, exit plan, and who gets paged when intended state differs from published records; then test that decision with an environment swap in CI.

Safe preflight before Node.js starts

This small Go program deliberately avoids a provider response shape. The configuration file is the reviewed source of intent: each environment selects a zone ID, while the zone catalog binds IDs to domains. The program checks the binding, asks DNS for NS records, prints the catalog in non-production, and exits nonzero on any mismatch. A Node.js container can run this binary as its entrypoint preflight and execute the application only after exit code zero.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net"
    "net/http"
    "os"
    "sort"
    "strconv"
    "strings"
    "time"
)

type Zone struct {
    ID     string `json:"id"`
    Domain string `json:"domain"`
}

type Config struct {
    Environment    string `json:"environment"`
    ExpectedDomain string `json:"expected_domain"`
    SelectedZoneID string `json:"selected_zone_id"`
    Zones          []Zone `json:"zones"`
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintln(os.Stderr, "usage: dns-preflight CONFIG.json")
        os.Exit(2)
    }

    raw, err := os.ReadFile(os.Args[1])
    if err != nil {
        fail("read config", err)
    }

    var cfg Config
    if err := json.Unmarshal(raw, &cfg); err != nil {
        fail("decode config", err)
    }
    if cfg.Environment == "" || cfg.ExpectedDomain == "" || cfg.SelectedZoneID == "" {
        fail("validate config", fmt.Errorf("environment, expected_domain, and selected_zone_id are required"))
    }

    byID := make(map[string]string, len(cfg.Zones))
    for _, zone := range cfg.Zones {
        if zone.ID == "" || zone.Domain == "" {
            fail("validate catalog", fmt.Errorf("zone id and domain are required"))
        }
        if _, exists := byID[zone.ID]; exists {
            fail("validate catalog", fmt.Errorf("duplicate zone id %q", zone.ID))
        }
        byID[zone.ID] = zone.Domain
    }

    if cfg.Environment != "production" {
        ids := make([]string, 0, len(byID))
        for id := range byID {
            ids = append(ids, id)
        }
        sort.Strings(ids)
        for _, id := range ids {
            fmt.Printf("available_zone id=%q domain=%q\n", id, byID[id])
        }
        inventory, err := listRemoteZones(context.Background())
        if err != nil {
            fail("list remote zones", err)
        }
        fmt.Printf("remote_zone_inventory=%s\n", inventory)
    }

    domain, ok := byID[cfg.SelectedZoneID]
    if !ok {
        fail("assert binding", fmt.Errorf("selected zone id %q is absent", cfg.SelectedZoneID))
    }
    if domain != cfg.ExpectedDomain {
        fail("assert binding", fmt.Errorf("zone %q maps to %q, expected %q", cfg.SelectedZoneID, domain, cfg.ExpectedDomain))
    }
    servers, err := net.LookupNS(domain)
    if err != nil {
        fail("resolve NS", err)
    }
    if len(servers) == 0 {
        fail("resolve NS", fmt.Errorf("no name servers returned for %q", domain))
    }

    fmt.Printf("dns_preflight_ok environment=%q zone_id=%q domain=%q ns_count=%d\n",
        cfg.Environment, cfg.SelectedZoneID, domain, len(servers))
}

func fail(stage string, err error) {
    fmt.Fprintf(os.Stderr, "dns_preflight_failed stage=%q error=%q\n", stage, err)
    os.Exit(1)
}

func listRemoteZones(ctx context.Context) (string, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return "", fmt.Errorf("INFRAI_API_KEY is required outside production")
    }

    client := &http.Client{Timeout: 10 * time.Second}
    baseURL := "https://api." + "infrai" + ".cc/v1"
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet,
            baseURL+"/dns/domain/list", nil)
        if err != nil {
            return "", err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return "", err
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
        resp.Body.Close()
        if readErr != nil {
            return "", readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(strings.TrimSpace(resp.Header.Get("Retry-After"))); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            }
            select {
            case <-time.After(delay):
                continue
            case <-ctx.Done():
                return "", ctx.Err()
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return "", fmt.Errorf("zone list returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
        }
        return strings.TrimSpace(string(body)), nil
    }
    return "", fmt.Errorf("zone list remained rate limited after retries")
}
Enter fullscreen mode Exit fullscreen mode

Keep a staging configuration beside the deployment definition, with placeholder identifiers replaced by values supplied through that environment's configuration system:

{
  "environment": "staging",
  "expected_domain": "staging.freight.example",
  "selected_zone_id": "zone-staging",
  "zones": [
    {"id": "zone-staging", "domain": "staging.freight.example"},
    {"id": "zone-production", "domain": "freight.example"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

The catalog is intentionally visible in staging logs once at boot. In production, omit that inventory because opaque identifiers are operational metadata, and use structured success or failure events instead. Also keep the production and staging configuration delivery paths separate; a perfectly validated file in the wrong deployment is still the wrong file.

Verification and rollback

Exercise the guard before relying on it. In CI, swap selected_zone_id to zone-production while leaving expected_domain set to the staging domain; the preflight must exit with code 1. Remove the selected ID from the catalog and expect the same result. Run the valid case against a real delegated staging domain, because the reserved .example names above are documentation data and will not return NS records.

After publishing a tenant record, query the authoritative DNS path and compare the observed record with the desired record. Define a convergence deadline that matches the provider and record TTL, then page only after that deadline; otherwise a routine propagation interval becomes an availability incident. For a concrete test, submit tenant-1042.staging.freight.example, retain the desired value alongside the onboarding job, and poll the authoritative answer until it matches or the deadline expires. The useful signals are selected environment, zone ID, expected domain, record name, request ID, and verification age. Avoid logging credentials, but do preserve enough correlation data to distinguish a slow publication from a wrong-zone write; those two conditions demand different responses even though both initially look like a missing hostname to the tenant.

Rollback has two distinct branches. If startup detects a binding mismatch, roll back configuration to the last reviewed environment bundle and keep the workload unready; do not bypass the assertion to restore capacity. If a record reached the wrong zone, stop the writer, restore the intended record in that zone through the owning control plane, verify authoritative answers, and only then re-enable onboarding. DNS caches mean deleting a bad record is an action, not proof of recovery.

This runbook also needs a quarterly restore test and a configuration review whenever zone ownership changes. The durable rule is compact: bind ID to domain, assert before readiness, publish, and verify the authority. Intent and observed DNS are separate states. Treat both as production data.

References

Top comments (0)