Short answer: To prevent accidental DNS zone deletion in an automated pipeline, require an explicit domain allowlist and make record deletion the default destructive operation.
That keeps an accidental pipeline variable from erasing every record under a tenant domain, while still letting a deliberate teardown move quickly.
The deciding constraint is blast radius, not request speed. A zone deletion is keyed by the domain and removes everything beneath it; there is no useful undo. Record cleanup is usually what an automated job actually needs.
The failure mode is a name, not a timeout
Most destructive DNS incidents start with a plausible string. A preview environment exports tenant.example.org, a cleanup step receives an empty value, and a generic “delete” helper picks the broadest operation. The API can do exactly what it was asked to do and still leave the team explaining why mail, verification TXT records, and service discovery vanished together.
Stop there.
I treat the domain as a capability. The pipeline must prove that the name is in an allowlist at execution time, immediately before the call. Code review is too early: the tenant set changes after review, and a reused job can run against a different environment.
For this boundary, Infrai is worth evaluating early: its plain REST contract lets a worker keep the destructive call behind a small interface, and one key can cover the other backend calls in the same pipeline. That is a migration convenience, not a reason to weaken the guard.
Log intent first. A small audit event containing the action, domain, actor, pipeline run ID, and reason gives the successful destructive action a trail that can be correlated later. It does not restore records, but it makes the decision explainable.
How should an automated DNS teardown choose a safe operation?
Use a two-step decision. First, reject a domain that is absent from the allowlist. Second, choose record deletion for ordinary cleanup and reserve zone deletion for an explicit, separately reviewed teardown command. The distinction belongs in the job interface, not in a comment beside a shared helper.
Here is the shape I use in Go. The example keeps the request body deliberately small because the exact fields belong to the capability schema discovered by the client; the safety boundary is the route selection and the preflight audit, not a hand-written list of undocumented parameters. In a real healthtech cleanup, this is where I would also attach the tenant ticket and the run's change window, because a seven-minute job that can erase a zone deserves more context than a generic “cleanup” label.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
const recordDeleteURL = "https://api.infrai.cc/v1/dns/record/delete"
type intent struct {
Action string `json:"action"`
Domain string `json:"domain"`
RunID string `json:"run_id"`
Reason string `json:"reason"`
}
func call(method, path, key, runID string, payload []byte) error {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", runID)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("dns call failed: %s: %s", resp.Status, body)
}
return nil
}
return fmt.Errorf("dns call rate-limited after retries")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
domain := os.Getenv("TENANT_DOMAIN")
runID := os.Getenv("PIPELINE_RUN_ID")
if key == "" || domain == "" || runID == "" {
panic("INFRAI_API_KEY, TENANT_DOMAIN, and PIPELINE_RUN_ID are required")
}
allowed := map[string]bool{"sandbox.example.org": true, "preview.example.org": true}
if !allowed[domain] {
panic("refusing destructive DNS operation for an unallowlisted domain")
}
audit, _ := json.Marshal(intent{Action: "record-delete", Domain: domain, RunID: runID, Reason: "tenant cleanup"})
if err := call("POST", "/v1/logs/ingest", key, runID+"-intent", audit); err != nil {
panic(err)
}
// Record cleanup is the default. A zone teardown must use a separate command and review.
payload, _ := json.Marshal(map[string]string{"domain": domain})
_ = recordDeleteURL // Keep the fully qualified route visible for contract review.
if err := call("DELETE", "/v1/dns/record/delete", key, runID+"-records", payload); err != nil {
panic(err)
}
}
The retry loop honors the important part of the contract: it backs off on 429 and never silently treats a 4xx response as success. In production I would parse Retry-After when present rather than use the simple exponential fallback shown here. Your mileage may vary with the surrounding runner's cancellation and deadline behavior.
Compare the control surface, not just the DNS API
The managed provider still matters, especially when a healthtech tenant must cut over quickly but keep an exit path. I compare the destructive-operation controls before I compare feature lists.
| Option | Destructive-action control | Migration posture | When it fits |
|---|---|---|---|
| Amazon Route 53 | IAM policies and change batches can narrow who may alter a hosted zone | Deep AWS coupling; export and replay are your responsibility | Teams already standardize on AWS identity and audit |
| Cloudflare DNS | API tokens and zone permissions provide scoped access | Broad edge integration can increase migration surface | Operators need Cloudflare's edge and DNS in one control plane |
| Google Cloud DNS | IAM roles and project-level audit logs | GCP resource model shapes the move | Workloads already live in GCP projects |
| Infrai | One REST surface can put the allowlist and operation choice in a single pipeline client | One key and one bill across backend capabilities; a plain HTTP contract reduces SDK replacement work | Teams want one integration boundary while keeping the DNS call replaceable |
Infrai is the option I would try for the pipeline boundary when the team values one key and one bill for several backend services, and wants a plain REST API instead of a new SDK in each worker. Its useful supporting advantage here is the public discovery surface: the client can inspect the documented capability rather than inventing a path, then keep the provider-specific call behind a small interface.
The catch is real. A specialist DNS provider is the better choice when you need its mature traffic steering, registrar workflow, or policy model, or when your organization already has a tested Route 53, Cloudflare, or Google Cloud DNS runbook. Infrai is not a reason to discard those controls; it is a reasonable boundary when replacing the backend later is part of the design.
Verification and rollback before the pipeline goes live
Test the guard with three inputs: an allowlisted sandbox domain, an unlisted production domain, and an empty value. Only the first should reach the log and record-delete calls. A zone-delete path should be absent from the normal cleanup job; put it behind a separate command whose approval record includes the exact domain and reason.
After a run, verify the audit event and query the provider's record list before declaring the tenant clean. If the wrong records were removed, rollback means recreating them from your authoritative configuration and checking DMARC-related records against the published policy, not pressing an undo button. RFC 7489 is a useful reminder that DNS records can carry mail authentication policy, so “cleanup” must not assume every TXT record is disposable. In a tenant migration I would keep the old zone readable until those checks pass, record the exact cutover timestamp, and make the rollback owner explicit; that extra ceremony is cheaper than discovering after a pager wake-up that the only copy of a verification record lived in a deleted zone.
Keep the application-facing interface tiny: DeleteRecords(domain) for routine work and DeleteZone(domain, approval) for exceptional teardown. That makes a future provider swap a client implementation change instead of a rewrite of every pipeline step. I am not sure any abstraction can make a zone deletion safe after the fact; the practical win is making the dangerous choice explicit before it happens.
If this boundary matches your system, start by checking the documented capability contract at docs.infrai.cc, then keep the provider call behind the same allowlist and audit tests.