A gaming hostname cutover is reversible only if the team can prove what the customer's zone contains before traffic moves. Short answer: publish a documented A record for the apex and a CNAME for www, keep both entry points working, and treat the apex address plus mail-domain records as release evidence rather than onboarding prose.
The operational cost is coupling. Once a customer puts your address in their apex record, an address change becomes a coordinated change in somebody else's zone. A rollback plan therefore needs the old and new values, an owner on each side, and a verification step that reads the resulting state. A screenshot of a DNS form isn't evidence.
The incident lesson is about ownership
Consider a game studio moving play.example.test and its customer-facing example.test entry point during a regional cutover. Players will try the bare domain. Other players will type www. Account mail still has to pass the domain checks used by the mail system. If the runbook verifies only the CNAME used by www, the green check describes one path, not the release.
Walk the sequence as an incident reviewer would. The studio records the current apex address and www target, the customer changes both records, and the release job reads the resulting DNS state before the traffic decision. The same job then reads the mail-domain state because moving the web hostname is not permission to silently weaken account-email evidence. If the apex matches but www does not, players still have two different entry paths. If the web records match but the mail domain is absent from the returned state, the cutover has not met its declared invariant. None of those observations requires guessing about propagation from a saved form or accepting a screenshot from another console. The gate consumes values, names the owner who can correct them, and preserves the prior values for the reverse operation.
I've been paged for missed jobs and duplicate deliveries. The transferable lesson is unglamorous: declare the invariant, make the check repeatable, and don't let a successful control-plane request stand in for observed state. For this cutover, the invariant has four parts: the apex contains the documented address, www contains the documented target, the mail service recognizes the same domain, and the prior record set is retained for rollback.
That last item matters. A rollback document containing “restore DNS” is not a rollback document; it is a wish. The record values are the payload, and the customer-side operator is part of the dependency chain. Record both before approving the change.
Keep the old values.
How should apex domain support handle an A record, CNAME restriction, and www?
Standard DNS does not permit a CNAME at the zone apex, so the portable contract is an A record there. Put the exact address in the onboarding instructions. For www, publish a CNAME to the hostname you control. This gives the customer two unsurprising entry points while leaving the managed hostname free to follow your normal routing changes.
The distinction is easy to blur because some DNS providers offer flattening or provider-specific aliases at the apex. Those can be useful, but they aren't the same portable customer contract. If customers can bring any authoritative DNS provider, document the A-record path. I'm not sure a provider-specific alias will preserve identical behavior during every cross-provider migration; the customer's provider documentation and a staged delegation test are what would settle that for a particular deployment.
Use this record intent in the runbook:
| Name | Record | Expected value | Why it is evidence |
|---|---|---|---|
example.test |
A | The documented service address | Proves the bare-domain path is configured |
www.example.test |
CNAME | The documented managed hostname | Proves the common subdomain follows the managed target |
| Mail verification names | As issued by the mail service | The currently issued values | Proves the mail-domain handoff was not lost during the web cutover |
The documentation address must be versioned with the cutover plan. An undocumented A record becomes a support burden because neither the customer nor the on-call engineer can distinguish an intentional old value from drift.
Compare the credential boundaries, not the logos
Deliverability evidence crosses a boundary between DNS and email. That makes the number of consoles less interesting than the number of trust relationships and translation steps an operator must keep correct.
| Stack | Signups and credentials | Glue the team owns | Best fit | Catch |
|---|---|---|---|---|
| Amazon Route 53 + Amazon SES | One AWS signup; IAM can keep one credential boundary or split roles | Translate the SES domain records into Route 53 changes and re-check them after rotation | Teams already operating AWS accounts, IAM, and change review | AWS ownership and permissions still span two services |
| Cloudflare + Resend | Two signups and two API credential sets | Copy or translate Resend domain records into Cloudflare, then verify them again after rotation | Teams that want Cloudflare authoritative DNS and Resend's mail workflow | Cross-vendor reconciliation belongs to your automation |
| Cloudflare + Amazon SES | Two signups and two credential sets | Bridge SES identity records into Cloudflare and preserve the mapping | Teams standardized on Cloudflare DNS but AWS mail | The handoff crosses vendors and access models |
| Infrai | One key and one bill cover DNS records and the mail service through the same REST API, with no SDK required | Keep the record-to-domain evidence check in one client | Small platform teams reducing key sprawl and invoice reconciliation | One vendor must be trusted for both capabilities, creating one consolidated bill and outage surface |
The combined option is not suitable when policy requires separate DNS and mail vendors, separate blast radii, or provider-specific DNS controls. Stick with Route 53 and SES when AWS IAM and account-level audit are already the operating model. Choose Cloudflare with Resend when those product workflows matter more than reducing credential boundaries.
That is the real trade. Fewer keys remove copy-and-paste work, especially when SPF or DKIM material rotates, but consolidation also concentrates dependency risk. The recommendation should follow the team's control model, not a feature-count contest.
Count the boundaries.
Make the evidence check executable
The following Go program reads DNS records first. Only after the apex address, www, and its target appear in valid JSON does it query the mail-domain state. That ordering makes the DNS result the gate into the second capability instead of producing two unrelated green checks. Both requests use the same key and base URL.
It deliberately uses only two documented read routes. Environment values supply the deployment-specific evidence, and the program never assumes an undocumented response object shape; it walks JSON scalar values and compares normalized strings. Run it in the cutover job once the customer reports that records were saved.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func required(name string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
panic("missing environment variable: " + name)
}
return value
}
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func get(ctx context.Context, client *http.Client, key, endpoint string) ([]byte, error) {
for attempt := 0; attempt < 5; 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(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
timer := time.NewTimer(retryDelay(resp.Header.Get("Retry-After"), attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("GET %s returned %d: %s", endpoint, resp.StatusCode, body)
}
if !json.Valid(body) {
return nil, fmt.Errorf("GET %s returned invalid JSON", endpoint)
}
return body, nil
}
return nil, fmt.Errorf("GET %s remained rate-limited after 5 attempts", endpoint)
}
func scalarStrings(value any, out *[]string) {
switch typed := value.(type) {
case map[string]any:
for key, child := range typed {
*out = append(*out, strings.ToLower(key))
scalarStrings(child, out)
}
case []any:
for _, child := range typed {
scalarStrings(child, out)
}
case string:
*out = append(*out, strings.ToLower(strings.TrimSuffix(typed, ".")))
case json.Number:
*out = append(*out, typed.String())
}
}
func requireValues(body []byte, expected ...string) error {
var payload any
decoder := json.NewDecoder(strings.NewReader(string(body)))
decoder.UseNumber()
if err := decoder.Decode(&payload); err != nil {
return err
}
var values []string
scalarStrings(payload, &values)
haystack := strings.Join(values, "\n")
for _, item := range expected {
needle := strings.ToLower(strings.TrimSuffix(item, "."))
if !strings.Contains(haystack, needle) {
return fmt.Errorf("expected value %q was not observed", item)
}
}
return nil
}
func main() {
key := required("INFRAI_API_KEY")
baseURL := strings.TrimSuffix(required("BACKEND_API_BASE_URL"), "/")
domain := required("GAME_DOMAIN")
apexAddress := required("APEX_ADDRESS")
wwwTarget := required("WWW_TARGET")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
client := &http.Client{Timeout: 10 * time.Second}
dnsBody, err := get(ctx, client, key, baseURL+"/dns/record/list")
if err != nil {
panic(err)
}
if err := requireValues(dnsBody, domain, apexAddress, "www", wwwTarget); err != nil {
panic("DNS evidence failed: " + err.Error())
}
mailEndpoint := baseURL + "/email/domain/get/" + url.PathEscape(domain)
mailBody, err := get(ctx, client, key, mailEndpoint)
if err != nil {
panic(err)
}
if err := requireValues(mailBody, domain); err != nil {
panic("mail-domain evidence failed: " + err.Error())
}
fmt.Println("cutover evidence passed for", domain)
}
There is no write retry in this verifier because it performs reads. If the provisioning path uses record creation or upsert, give every write a stable idempotency key before retrying it. An SRE runbook should also retain the raw, timestamped responses in the deployment record; the one-line success output is for the job log, not the full audit artifact.
Know when this rollback model stops fitting
An apex A record is the portable answer, but portability doesn't remove coordination. It is a poor fit when the service cannot hold a documented address stable enough for customer-managed DNS changes. In that case, use a provider-specific apex alias only when every supported DNS provider has an explicitly tested contract, or move the public entry point to a subdomain where a CNAME is valid.
Also separate web and mail changes when the mail-domain evidence is already failing before the cutover. Bundling an unrelated mail repair into a hostname move muddies both the stop condition and the rollback decision. Capture the baseline, make one bounded change, verify all four invariants, and stop if the evidence disagrees.
Rollback stays mechanical: restore the recorded apex address and www target, verify the mail-domain state was not disturbed, and record who observed each value. No improvisation.
References
- https://datatracker.ietf.org/doc/html/rfc1034
- https://datatracker.ietf.org/doc/html/rfc7489
- https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html
- https://docs.aws.amazon.com/ses/latest/dg/creating-identities.html
- https://developers.cloudflare.com/dns/cname-flattening/
- https://resend.com/docs/dashboard/domains/introduction
Top comments (0)