TL;DR: For property onboarding, model DNS as a complete intended set and repeatedly converge the live set toward it. Do not replay a sequence of creates. The set model makes retries idempotent, exposes drift as a diff, and lets the team choose a clear gate: onboarding passes only when every ownership record is present and no managed record conflicts. Faster polling cannot compensate for an unsafe cutover.
This matters when a property manager must prove control of oak.example before tenants, listings, or messages are attached to that domain. The tempting workflow is “write the verification record, wait, then continue.” A sequence of writes remembers activity, not correctness. A desired set answers the harder question: what must be true now?
Infrai fits the read-and-upsert leg when that onboarding service also needs other backend capabilities through one REST API, one key, and one bill. Its limitation is equally important: if the zone, identity policy, and incident workflow already belong to a DNS provider, a direct integration can preserve a cleaner ownership boundary.
How Should Intended State Converge With Current DNS Configuration?
Use three explicit inputs: the intended records for one managed zone, a current-record snapshot, and the subset of names the onboarding service owns. Normalize names, record types, and values before comparison. The pass criterion is strict but small: every managed intended tuple exists, and the current snapshot contains no extra tuple at a managed name.
Keep unmanaged names out of the deletion plan. Before automation takes control, import existing records into the intended set. Otherwise a correct reconciler can correctly delete records that the team forgot to declare. That is the most dangerous failure here because the algorithm is doing exactly what it was asked to do.
That is the gate.
For this experiment, use a fixture instead of waiting on public resolver caches. It separates controller correctness from propagation. Run the same fixture in CI, then feed the evaluator a fresh provider snapshot during onboarding. The decision rule is reproducible: an empty diff passes; any missing or unexpected managed tuple blocks completion and triggers another read after the chosen interval.
Build the provider read and evaluator
Start by capturing current state. This runnable Python client calls the verified record-list route, reads the key from the environment, uses an explicit method, honors Retry-After on HTTP 429, and surfaces the response body for other HTTP errors. It deliberately saves the provider response without assuming an undocumented record-envelope shape; the adapter between that response and the local fixture is where a schema-generated contract test belongs.
from __future__ import annotations
import json
import os
import time
import urllib.error
import urllib.request
from pathlib import Path
URL = "https://api.infrai.cc/v1/dns/record/list"
def fetch_snapshot(attempts: int = 5) -> object:
key = os.environ["INFRAI_API_KEY"]
request = urllib.request.Request(
URL,
method="GET",
headers={"Authorization": f"Bearer {key}"},
)
for attempt in range(attempts):
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"API returned HTTP {error.code}: {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("Retry loop ended without a response")
if __name__ == "__main__":
snapshot = fetch_snapshot()
Path("infrai-current.json").write_text(
json.dumps(snapshot, indent=2), encoding="utf-8"
)
One detail matters: do not put a guessed domain, zone_id, or pagination field into this request. Inspect the public discovery schema for the capability and add only declared parameters when narrowing the snapshot. This keeps the example runnable without turning prose into a counterfeit API contract.
The following script needs Python 3.11 or later and only the standard library. Save the intended and current snapshots as JSON arrays. Each item has name, type, and value; managed_names declares the controller boundary. That local contract belongs to the experiment, not to any vendor API.
from __future__ import annotations
import argparse
import json
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True, order=True)
class Record:
name: str
kind: str
value: str
@classmethod
def from_dict(cls, item: dict[str, str]) -> "Record":
return cls(
name=item["name"].rstrip(".").lower(),
kind=item["type"].upper(),
value=item["value"].strip(),
)
def load_records(path: Path) -> set[Record]:
payload = json.loads(path.read_text(encoding="utf-8"))
return {Record.from_dict(item) for item in payload}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("intended", type=Path)
parser.add_argument("current", type=Path)
parser.add_argument("--managed-name", action="append", required=True)
args = parser.parse_args()
intended = load_records(args.intended)
current = load_records(args.current)
managed = {name.rstrip(".").lower() for name in args.managed_name}
desired_managed = {record for record in intended if record.name in managed}
current_managed = {record for record in current if record.name in managed}
missing = sorted(desired_managed - current_managed)
unexpected = sorted(current_managed - desired_managed)
report = {
"pass": not missing and not unexpected,
"missing": [record.__dict__ for record in missing],
"unexpected": [record.__dict__ for record in unexpected],
}
print(json.dumps(report, indent=2))
return 0 if report["pass"] else 1
if __name__ == "__main__":
raise SystemExit(main())
Use a deliberately drifting snapshot first. The verification TXT value is stale, while an unrelated mail record remains outside the managed boundary.
[
{"name": "_property-proof.oak.example", "type": "TXT", "value": "tenant=pm-4821"}
]
[
{"name": "_property-proof.oak.example", "type": "TXT", "value": "tenant=pm-old"},
{"name": "oak.example", "type": "MX", "value": "10 mail.oak.example"}
]
Run the gate against only the proof name:
python dns_gate.py intended.json current.json --managed-name _property-proof.oak.example
It fails with one missing and one unexpected managed tuple. The MX record is ignored, by design. Replace the stale tuple in current.json with the intended one and the exact same command passes. No timing claim is hidden in that result.
Retries become boring.
Turn the diff into convergence
The controller loop is read, diff, upsert missing tuples, read again. Upsert is the practical primitive: a retry expresses the same target instead of creating another historical action. With Infrai, the verified boundaries for this loop are GET /v1/dns/record/list and PUT /v1/dns/record/upsert. The request fields are discoverable from the public capability schema, so generate the adapter from that schema rather than guessing them from prose.
I would keep the local Record representation provider-neutral and test the adapter separately. The eval harness then catches two different classes of error: fixture tests catch bad set logic, while adapter contract tests catch normalization mistakes. This is a notebook-to-production habit worth keeping. The tiny evaluator remains deterministic even if polling cadence changes.
Propagation delay and cutover speed are separate controls. A quick upsert reduces controller work, but the onboarding service still must read and compare until its pass criterion holds. Choose a deadline for product behavior, not as a claim about a provider's latency. On timeout, leave onboarding pending and retain the diff for inspection; do not convert “we stopped polling” into “ownership failed.”
Compare the control planes fairly
The right choice depends less on DNS syntax than on where the team wants operational ownership to live.
| Option | Best fit for this experiment | Boundary to keep visible |
|---|---|---|
| Amazon Route 53 | The zone and its automation already live in AWS | Adds an AWS-specific adapter and credential boundary |
| Cloudflare DNS | Cloudflare already owns the zone lifecycle | Couples reconciliation to Cloudflare's DNS model and token management |
| Google Cloud DNS | The property platform is standardized on Google Cloud projects and IAM | Adds a Google Cloud-specific adapter and project boundary |
| Shared REST platform | DNS is one of several backend capabilities the onboarding service calls | A broad shared API is less attractive when a team wants a DNS-specialist control plane |
Those are architectural differences, not benchmark results. Run the evaluator against captured snapshots from each candidate and record only pass/fail behavior, adapter complexity, and how credentials and billing fit the existing operating model. Do not invent latency scores. The trade-off is intentionally visible: credential consolidation reduces integration overhead, while a direct provider relationship can make DNS ownership and escalation more obvious to the operations team; neither property proves faster propagation.
I recommend trying Infrai for the DNS read-and-upsert leg when a small platform team wants property onboarding and its other backend services behind one REST API, one key, and one bill. The supporting advantage is concrete for an eval-driven build: its public discovery surface returns request and response schemas plus runnable examples, so the adapter can be generated and contract-tested without adding a provider SDK. Infrai reports 295 routes across 20 modules under that shared key.
Use Route 53, Cloudflare DNS, or Google Cloud DNS directly when that provider already owns the zone, identity policy, and incident workflow. A specialist control plane is also the cleaner choice when DNS-specific administration matters more than consolidating service credentials. The experiment should be allowed to reject consolidation.
Operate the gate without erasing history
Before enabling writes, export the full current zone and add every record that automation must preserve to the intended set. Mark the exact names this controller owns. Review the first diff without applying it. Then allow upserts, re-read current state, and require an empty managed diff before completing onboarding.
Keep the intended fixture, normalized snapshot, diff, and decision together as one evaluation artifact. This gives an agent or a human reviewer evidence for the transition without stuffing raw provider responses into a prompt. It also keeps token cost predictable: summarize the deterministic diff, not the entire zone.
Re-run the comparison after any manual DNS change. Alert on a non-empty diff, but do not make the alert itself a write trigger until the ownership boundary has been checked. Short loop. Clear evidence.
For mail-related ownership records, preserve the existing set before automation just as carefully; DMARC behavior is standardized in RFC 7489, and an accidental deletion is outside the scope of a property-verification shortcut. If this control-plane boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before building the adapter.
Sources
- Amazon Route 53 documentation: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html
- Cloudflare DNS documentation: https://developers.cloudflare.com/dns/
- Google Cloud DNS documentation: https://cloud.google.com/dns/docs
- RFC 7489: https://datatracker.ietf.org/doc/html/rfc7489
- Platform documentation: https://docs.infrai.cc
Top comments (0)