Skip to content

Instantly share code, notes, and snippets.

@fizz
Created February 14, 2026 19:17
Show Gist options
  • Select an option

  • Save fizz/40f49e05f2d4c6de19f85c849b602780 to your computer and use it in GitHub Desktop.

Select an option

Save fizz/40f49e05f2d4c6de19f85c849b602780 to your computer and use it in GitHub Desktop.
dns-parity.sh — compare Route53 and Cloudflare zones side by side
#!/usr/bin/env bash
# dns-parity.sh — compare Route53 and Cloudflare zones side by side
# Usage: R53_ZONE_ID=Z0XXX CFLARE_ZONE_ID=xxx CFLARE_API_TOKEN=xxx ./dns-parity.sh
set -euo pipefail
r53=$(aws route53 list-resource-record-sets \
--hosted-zone-id "$R53_ZONE_ID" --output json)
cflare=$(curl -s -H "Authorization: Bearer $CFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$CFLARE_ZONE_ID/dns_records?per_page=100")
python3 - "$r53" "$cflare" <<'PYEOF'
import json, sys
r53 = json.loads(sys.argv[1])
cflare = json.loads(sys.argv[2])
def parse_r53(data):
records = {}
for r in data["ResourceRecordSets"]:
name = r["Name"].rstrip(".")
rtype = r["Type"]
if "AliasTarget" in r:
key = f"{name} {rtype}"
records[key] = f"ALIAS -> {r['AliasTarget']['DNSName'].rstrip('.')}"
else:
ttl = r.get("TTL", "")
for rec in r.get("ResourceRecords", []):
val = rec["Value"]
# Collapse MX priority into the key
if rtype == "MX":
parts = val.split(" ", 1)
key = f"{name} {rtype} pri {parts[0]}"
val = parts[1] if len(parts) > 1 else val
else:
key = f"{name} {rtype} {val[:30]}"
records[key] = f"TTL {ttl}"
return records
def parse_cflare(data):
records = {}
for r in data["result"]:
name = r["name"]
rtype = r["type"]
ttl = r.get("ttl", "")
ttl_str = "auto" if ttl == 1 else str(ttl)
content = r["content"]
if rtype == "MX":
key = f"{name} {rtype} pri {r.get('priority','')}"
else:
key = f"{name} {rtype} {content[:30]}"
records[key] = f"TTL {ttl_str}"
return records
r53_recs = parse_r53(r53)
cflare_recs = parse_cflare(cflare)
all_keys = sorted(set(r53_recs) | set(cflare_recs))
# Detect apex A/AAAA alias (Route53) vs CNAME-flattened (Cloudflare).
# These are equivalent — only flag MISSING if one side is truly absent.
def find_apex_equivalences(r53_recs, cflare_recs):
"""Find apex names where R53 has A/AAAA aliases and CF has a CNAME
pointing to the same target. These are parallel records, not gaps."""
suppress = set()
r53_aliases = {} # name -> set of targets
for key, val in r53_recs.items():
parts = key.split(" ", 2)
if len(parts) >= 2 and parts[1] in ("A", "AAAA") and val.startswith("ALIAS"):
target = val.split("-> ", 1)[1] if "-> " in val else ""
r53_aliases.setdefault(parts[0], set()).add(target)
for key, val in cflare_recs.items():
parts = key.split(" ", 2)
if len(parts) >= 3 and parts[1] == "CNAME":
name = parts[0]
cflare_target = parts[2]
if name in r53_aliases:
# Both sides have the apex covered — suppress all three
for rkey in r53_recs:
rparts = rkey.split(" ", 2)
if rparts[0] == name and rparts[1] in ("A", "AAAA"):
suppress.add(rkey)
suppress.add(key)
return suppress
suppress = find_apex_equivalences(r53_recs, cflare_recs)
print(f"{'Record':<50s} {'Route53':<20s} {'Cloudflare':<20s}")
print(f"{'-'*50} {'-'*20} {'-'*20}")
for key in all_keys:
if key in suppress:
continue
r = r53_recs.get(key, "MISSING")
c = cflare_recs.get(key, "MISSING")
marker = ">>> " if r == "MISSING" or c == "MISSING" else " "
print(f"{marker}{key:<50s} {r:<20s} {c:<20s}")
if suppress:
print(f"\n({len(suppress)} apex alias/CNAME records suppressed — "
f"R53 A/AAAA aliases and CF CNAME flattening are equivalent)")
PYEOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment