Created
February 14, 2026 19:17
-
-
Save fizz/40f49e05f2d4c6de19f85c849b602780 to your computer and use it in GitHub Desktop.
dns-parity.sh — compare Route53 and Cloudflare zones side by side
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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