managing aws route53 records with opentofu

November 15, 2025

I recently moved the DNS management for this website into version-controlled infrastructure using OpenTofuOpenTofuAn open-source fork of Terraform, managed by the Linux Foundation.. The hosted zonehosted zoneA container in Route 53 for records for a single domain. and records already existed in Route53Amazon Route 53AWS’s managed DNS service., so my goal was to manage them without causing downtime.

File structure

infra/
├── backend-bootstrap/        # One-time backend setup
│   ├── main.tf               # S3 bucket for state, DynamoDB table for locking
│   ├── providers.tf          
│   ├── variables.tf          
│   ├── versions.tf           
│   └── outputs.tf            
├── backend.tf                
├── outputs.tf                
├── providers.tf              
├── records.tf                # DNS records (A, MX, TXT, DMARC, DKIM, CAA)
├── route53.tf                
├── variables.tf              
└── versions.tf               

1. Represent the hosted zone

route53.tf defines the zone:

resource "aws_route53_zone" "primary" {
  name = var.domain_name
  comment = "fiachracurran.com public hosted zone (managed by OpenTofu)"
  lifecycle { prevent_destroy = true }
}

I used an import blockimport blockThis block is used to import existing resources into the state. only for the initial adoption (alternatively: tofu import can be used). After the first successful tofu apply, I deleted the import block file to keep plans clean as the state now tracks it after the initial import.

2. Capture existing records

Records mirrored live DNS values that were in Route53, my records.tf included the following records:

  • A
  • MX
  • TXT (including SPF, DMARC, DKIM entries)

3. Final plan check

tofu plan verifies everything is declared properly and has no state drift issues, if you see this output you’re good to go:

No changes. Your infrastructure matches the configuration.

4. Notes

  • FQDN vs relative record names can trigger replacements so make sure you match what import stores if you want a clean plan (I figured this out after being surprised at the output of my initial tofu plan).
  • Trailing dots on MX and other records prevent the zone's root domain name getting appended to the record.
  • NS/SOA records are owned by Route53 so leave them alone (unless you have a specific reason to manage them).
  • Delete one-time import files after bootstrap (for cleaner plans, not strictly necessary but I don't see the point in keeping them if the bootstrap was successful).

5. Moving from local to remote state management

After the DNS was successfully managed with state being local, I set up a remote backendremote backendA central location, S3 in this instance, where state is stored remotely. so I don't lose state if my computer gives up the ghost and to get state lockingstate lockingA way to prevent concurrent writes to state..

Bootstrap (one-time):

cd infra/backend-bootstrap
tofu init && tofu apply

Configure the backend for the main infrastructure stack:

# backend.hcl (not committed, is ignored via .gitignore)
bucket         = "<BUCKET_NAME>"
key            = "<PATH>/opentofu.tfstate"
region         = "<REGION>"
dynamodb_table = "<TABLE_NAME>"
encrypt        = true

Migrate local state to S3 (one-time):

cd infra
tofu init -backend-config=backend.hcl -migrate-state

The subsequent tofu plan shows locking and "No changes" with remote state.

Backend cost

  • S3: tens of kilobytes stored, with minimal requests/mo
  • DynamoDB: minimal reads/writes per operation

I think that the benefits easily outweigh the minimal cost, the cost should be < €0.10.

Conclusion

Moving my DNS into OpenTofu took less time than I expected, and it feels good to have everything version-controlled. If you’re sitting on manually managed DNS, this is a pretty easy improvement to make!