I recently moved the DNS management for this website into version-controlled infrastructure using OpenTofuOpenTofu—An open-source fork of Terraform, managed by the Linux Foundation.. The hosted zonehosted zone—A container in Route 53 for records for a single domain. and records already existed in Route53Amazon Route 53—AWS’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 block—This 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 backend—A 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 locking—A 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!