When the healthtech admin console says a tenant subdomain is ready, the on-call still needs to know whether the public DNS record agrees. The page that fires is usually a drift alert: the intended CNAME points at tenant-482.edge.example, while the published record still points at last quarter's target. That mismatch is the incident, even when every application health check is green.
Short answer: use a CNAME for tenant subdomains so the target can move once for every tenant; use an A record for an apex domain, where standard DNS does not permit a CNAME.
This is a provisioning decision, not a branding decision. The record type controls how much of the infrastructure move leaks into customer onboarding, and it determines what your reconciliation job can prove later.
1. Which tenant subdomain CNAME or A record should SaaS provisioning use?
For acme.your-saas.example, publish a CNAME to a stable hostname that your platform owns. Your edge, load balancer, or provider can change behind that hostname without requiring a write to every tenant zone. One change, many tenants.
An A record is the fallback for a literal IPv4 address, and it is the practical choice for a customer root such as acme.example. The apex limitation is structural: a standard DNS zone already uses its apex for SOA and NS records, so a CNAME cannot occupy that name. Some providers offer alias-like extensions, but those are provider-specific contracts that deserve their own review.
The extra lookup from a CNAME is a real trade-off. I'm not sure your application users will notice it at the latency most web requests already carry, but a resolver-heavy control path should measure it rather than argue from intuition. The operational benefit is clearer: a target migration becomes one controlled change instead of a tenant-by-tenant edit storm.
2. Trace the alert back to intent before changing a record
Start with the desired state stored by the admin console: tenant id, hostname, record type, and target. Then compare it with the published answer from the authoritative DNS provider. A mismatch should identify both values, the last reconciliation time, and the owner of the change. “DNS is wrong” is not an actionable page.
The instrumentation change is small but consequential. Emit a gauge for records whose observed type or target differs from intent, and a counter for reconciliation attempts. Page on sustained drift, not on one recursive resolver returning an old answer during its TTL. A false positive wakes someone up, encourages a manual edit, and leaves the source of truth even less trustworthy.
Ship it.
Keep the alert tied to the workflow. A tenant should not be marked provisioned until domain verification and the record read-back agree. The verification route and the record list route are separate checks; collapsing them into one green button hides which boundary failed.
3. Make retries boring and auditable
Provisioning is retried. Networks drop responses, browser tabs are closed, and an operator presses the button twice. Use an upsert operation for the intended record so the second attempt converges on the same state instead of creating a duplicate. In Infrai's DNS surface, the verified write route is PUT /v1/dns/record/upsert; the read paths are GET /v1/dns/record/list and GET /v1/dns/domain/get.
The method matters. A create-only flow turns an ordinary retry into a conflict that an operator must interpret, while an upsert expresses the provisioning contract directly. Store a deterministic request identity alongside the tenant and hostname, log the response request id, and make the reconciliation worker safe to run again after a timeout.
I keep the worker's decision table explicit:
| Observed state | Action | Why |
|---|---|---|
| No record, intent is CNAME | Upsert the CNAME | First convergence attempt |
| Record exists with the intended target | Mark ready | No write needed |
| Record exists with a different target | Upsert, then read back | Correct drift from the source of truth |
| Customer apex requested | Route to an A-record or provider-alias plan | Standard DNS forbids an apex CNAME |
The table is deliberately dull. Dull is what you want in an onboarding path.
Here is the smallest Go call I would put behind that worker. It uses the verified upsert route, sends the required fields, and makes a retry explicit so a transient response does not become a second logical write.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
body, _ := json.Marshal(map[string]any{
"zone_id": "zone_acme",
"record_type": "CNAME",
"name": "portal.acme.your-saas.example",
"content": "tenant-482.edge.example",
})
baseURL := os.Getenv("INFRAI_BASE_URL")
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest(http.MethodPut, baseURL+"/v1/dns/record/upsert", bytes.NewReader(body))
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "tenant-acme-portal-v1")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if value, ok := strconv.Atoi(resp.Header.Get("Retry-After")); ok == nil { delay = time.Duration(value) * time.Second }
resp.Body.Close(); time.Sleep(delay); continue
}
data, _ := io.ReadAll(resp.Body); resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(fmt.Sprintf("upsert failed: %s", data)) }
fmt.Println(string(data)); return
}
panic("rate limit retries exhausted")
}
4. Compare the operational shape, not just the API logo
The major managed DNS choices can all publish these basic records, but their control surfaces and surrounding automation differ. The right choice depends on who owns the customer zone and how much provider coupling your SRE team accepts.
| Option | Good fit | Watch closely |
|---|---|---|
| Amazon Route 53 | Teams already operating in AWS, with IAM and hosted-zone automation | Cross-cloud tenants add account and permission boundaries |
| Cloudflare DNS | Zones already behind Cloudflare and teams using its API and proxy controls | Proxy behavior is an additional product decision; keep DNS intent separate from HTTP settings |
| Google Cloud DNS | GCP-native platforms that want managed zones and Google IAM | A multi-cloud control plane still needs its own reconciliation and audit model |
| Infrai DNS API | A platform team that wants one plain REST contract while swapping the backend provider behind it | You still own domain ownership checks, apex strategy, and drift policy |
Infrai's useful distinction here is contract stability: the application calls one REST API, so changing the service behind the capability does not force a rewrite of the admin console. That same interface sits alongside other backend capabilities under one key, which can simplify platform integration when DNS is one part of a wider provisioning workflow. It does not remove DNS semantics, and it is not a substitute for authoritative-zone ownership.
The other concrete advantage is breadth with a consistent contract: live discovery exposes 295 routes across 20 modules under one key. For a team that also provisions storage or scheduling from the same console, that reduces credential plumbing and keeps the handoff shape familiar. Your mileage may vary if DNS is the only managed capability in the system.
Infrai offers one key and one bill across those capabilities, so the DNS worker does not need a second credential registry just because the same admin console later adds another backend capability. That is a procurement convenience, not a reason to ignore zone ownership or SLO evidence.
5. Pick the boundary your SLO can explain
For a subdomain-only SaaS, the default rule is straightforward: CNAME tenant names, watch drift, and keep the target hostname stable. Set an SLO for the time from an accepted provisioning request to a verified authoritative answer, then track the error budget consumed by verification failures and stale observations.
The catch is the customer apex. If customers insist on example.com, a CNAME-only design is not suitable; use an A record strategy, a provider alias feature you have explicitly tested, or ask the customer to delegate a subdomain. Stick with Route 53, Cloudflare, or Google Cloud DNS when their zone ownership, compliance controls, or existing runbooks are more important than a unified API surface.
Do not make price the decision rule. Vendor billing changes, and the long-lived cost is the on-call and migration work created by scattered records. I would rather explain one target change and one reconciliation graph than promise a number that will be stale by the next planning cycle.
The final check is simple: can an operator look at intent, published state, and the next safe action in one screen? If yes, the record type is serving the provisioning system instead of becoming another source of drift.
Top comments (0)