← All case studies Amazon Route 53 → Cloudflare DNS

Two Route 53 zones moved to Cloudflare once tunnels were the reason to stay

Two personal domains left Route 53 for Cloudflare DNS on 13 January 2025. The bill was about 12 euros a year. The cutover took about 45 minutes, and Ansible then left stale records behind. A later post shows the tunnel and DNS-01 setup still in place.

Ben Matheja wrote this up on 13 January 2025. The zones are mittbachweg.de and benmatheja.de. He buys domains at Netcup and points the nameservers wherever the records live. For years that was Amazon Route 53.

Route 53 had been fine for him. Resolution worked, Traefik completed Let’s Encrypt DNS-01 challenges, and Terraform owned the records. The bill was about €12 a year.

The reason to leave was Cloudflare Tunnel. His ISP uses carrier-grade NAT (DS-Lite), so he has no public IPv4 and no port forward. A tunnel was the first way he could expose the homelab. He also did not want services listening on the internet even if a forward had been possible. Once that traffic already entered through Cloudflare, a second DNS host stopped making sense.

He lists what he was using on the Cloudflare free plan. Unlimited queries, unlimited records, DDoS protection, universal SSL on subdomains, tunnels, and a Terraform provider. He puts the price at $0 a year. Those are his claims for the plan at the time he wrote.

This is a homelab, two zones, not a company outage. The Route 53 → Cloudflare DNS playbook still applies to the records and the delegation.

47 records, then a placeholder in the snippet

He exported with aws route53 list-resource-record-sets and found 47 records across both domains. Types were A, AAAA, CNAME, MX, and TXT. The command he prints writes one zone file, mittbachweg-zone.json, and the zone id in it is Z1234EXAMPLE. Several later snippets use example.com, 192.0.2.10, and ns-1234.awsdns-12.org. Those are stand-ins. The 47 and the two domain names are the figures he states as his.

Terraform created the Cloudflare zones on the free plan with jump_start off, so Cloudflare did not scan the old DNS and import records on its own. He could write every row himself. The provider constraint he prints is ~> 4.43.0. The sample records are a git A record and a *.one CNAME. Treat the addresses in them as samples, the same way the zone id is a sample.

Ansible kept records he had deleted

He also taught Ansible to create a Cloudflare record when a service deployed, through community.general.cloudflare_dns. The task sets state: present and proxied: yes, and it runs only when a flag asks for a record. That felt convenient. A deploy could publish the name without a Terraform edit.

Ansible does not track the record as state. Take the task out of the playbook and the DNS row stays. Deploy twice and he got duplicates. He cleaned those by hand in the Cloudflare UI.

The fix he landed on splits the writers. Static records, which are most of them, live in Terraform, including the tunnel CNAMEs. Ansible creates a record only when a deploy truly needs one. terraform apply deletes a record that left the Terraform config. It will not delete a row Ansible created, because that row was never in the state. A leftover A record and a tunnel CNAME for the same name are two answers, and only one of them is in the plan. Let’s Encrypt DNS-01 through Traefik still works after the move. He says the migration also let him drop AWS for this setup.

The nameserver change

He checked the new zone with dig against eva.ns.cloudflare.com before he touched the registrar. That query asks Cloudflare directly, so a correct answer there means the new zone is populated. It does not mean a recursive resolver has switched. He then lowered the Route 53 TTL to 60 seconds and updated the nameservers at Netcup. The new pair he names is eva.ns.cloudflare.com and walt.ns.cloudflare.com. He watched dig NS mittbachweg.de until it settled.

His result line is zero downtime, and full propagation in about 45 minutes. He does not report a stale resolver or a negative cache. Two zones and a homelab are a smaller blast radius than a public API.

What the tunnel is doing

The path he draws is the internet, then Cloudflare’s edge, then an encrypted tunnel, then Traefik, then the service. cloudflared dials out. Nothing at home accepts an inbound connection. The tunnel config he prints sends named hosts at the edge to an internal URL, with a final rule that returns 404. The DNS record for a published name is a proxied CNAME to <tunnel-id>.cfargotunnel.com. Hostnames in the sample, such as git.example.com and ruby.internal, are stand-ins. The real zones are the two he named at the top.

On 19 December 2025, eleven months later, he describes the lab that grew on top of this. Traefik still gets Let’s Encrypt certificates with a Cloudflare DNS-01 challenge and an API token. A Cloudflare Tunnel operator publishes only what should be public, and this blog is one of those names. A separate DNS path, ExternalDNS, writes records for a zone that looks public and only resolves from inside. He does not say that internal zone replaced the January Terraform records. The Git repo that holds the Ansible and Terraform had passed 3,000 commits. That post is the later platform. The January post is the cutover.

If you copy the Ansible shortcut, plan the cleanup before the first deploy. Terraform will not see the row Ansible created. The playbook assumes one owner for the zone file. His post is what happens when a second tool writes into it.