Introduction
DNS is often the forgotten layer of infrastructure—until it breaks, then everything stops. Route 53 DNS with Terraform: what developers should know covers the practical patterns we use when managing DNS as code in production environments. If you're deploying services across AWS regions, migrating between environments, or managing failover scenarios, this guide walks through the decisions and configurations that matter.
In this post, we'll focus on four core areas: organizing hosted zones, using alias records to point at AWS resources without the overhead of traditional CNAME records, tuning TTLs before migrations to avoid DNS resolution delays, and configuring health checks so your infrastructure can fail gracefully. By the end, you'll have a repeatable pattern for DNS infrastructure that scales alongside your applications.
Hosted Zones Per Domain
A hosted zone in Route 53 is a container for all DNS records for a single domain. The first decision you'll face is whether to use one hosted zone per domain or consolidate multiple domains into a shared structure. For most teams, one hosted zone per domain is the clearest approach—it maps directly to your domain structure, keeps billing and access controls straightforward, and avoids cross-domain record conflicts.
In Terraform, this looks like:
resource "aws_route53_zone" "primary" {
name = "example.com"
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
resource "aws_route53_zone" "staging" {
name = "staging.example.com"
tags = {
Environment = "staging"
ManagedBy = "terraform"
}
}
Each hosted zone gets its own set of nameservers. You'll register those nameservers with your domain registrar—this is the bridge between Terraform-managed DNS and the outside world. Capture the nameserver addresses as outputs so your team can quickly reference them during domain setup:
output "primary_nameservers" {
description = "Nameservers for example.com"
value = aws_route53_zone.primary.name_servers
}
output "staging_nameservers" {
description = "Nameservers for staging.example.com"
value = aws_route53_zone.staging.name_servers
}
Organizing hosted zones this way also pairs naturally with Terraform workspaces or separate state files per environment. A production team we worked with keeps prod and staging in separate Terraform projects, reducing the blast radius if someone applies a destructive change to the wrong environment.
Alias Records for AWS Resources
Traditional DNS uses CNAME or A records to point domain names at targets. In AWS, alias records are a Route 53 extension that let you point a domain directly at AWS resources like load balancers, CloudFront distributions, or API Gateways—without the latency and configuration complexity of standard CNAME records. Alias records are also free, whereas non-alias queries against Route 53 have a small per-query cost.
The key advantage: alias records automatically resolve to the current IP address of the AWS resource, even if it changes. If your load balancer scales horizontally and gets a new IP, your alias record follows it without manual updates.
Here's a common pattern: pointing a domain at an Application Load Balancer:
resource "aws_lb" "api" {
name = "api-alb"
internal = false
load_balancer_type = "application"
subnets = var.subnet_ids
tags = {
Name = "api-alb"
}
}
resource "aws_route53_record" "api" {
zone_id = aws_route53_zone.primary.zone_id
name = "api.example.com"
type = "A"
alias {
name = aws_lb.api.dns_name
zone_id = aws_lb.api.zone_id
evaluate_target_health = true
}
}
Notice evaluate_target_health = true. This tells Route 53 to check the health of the load balancer before returning its IP address in DNS queries. If the load balancer is unhealthy, Route 53 won't serve that record—critical for failover scenarios.
You can also use alias records for CloudFront distributions:
resource "aws_route53_record" "cdn" {
zone_id = aws_route53_zone.primary.zone_id
name = "cdn.example.com"
type = "A"
alias {
name = aws_cloudfront_distribution.main.domain_name
zone_id = aws_cloudfront_distribution.main.hosted_zone_id
evaluate_target_health = false
}
}
The pattern is the same: reference the AWS resource's DNS name and hosted zone ID. The resource itself provides these attributes, so your DNS configuration stays in sync with infrastructure changes automatically.
Low TTLs Before Migrations
TTL (Time To Live) controls how long DNS resolvers cache your records. High TTLs (3600 seconds or more) reduce load on Route 53 and speed up resolution for repeat queries. Low TTLs (60 seconds or lower) let you change DNS quickly when you need to cut over to new infrastructure.
The trap: if you wait until migration day to lower the TTL, cached records at ISP resolvers and client machines will still point at the old target for hours. Plan ahead by dropping the TTL a day or two before your scheduled cutover.
Here's a practical workflow:
- A few days before migration, lower TTL on records you'll be moving:
hcl
resource "aws_route53_record" "app" { zone_id = aws_route53_zone.primary.zone_id name = "app.example.com" type = "A" ttl = 60 # Lowered from 300 for migration window records = [aws_lb.app.dns_name] } - Monitor DNS propagation using tools like
digornslookupto confirm caches have cleared. - After migration is complete and validated, raise TTL back to normal:
hcl
resource "aws_route53_record" "app" { zone_id = aws_route53_zone.primary.zone_id name = "app.example.com" type = "A" ttl = 300 # Back to standard records = [aws_lb.app.dns_name] }
A team we worked with ran a database migration where they underestimated TTL caching. After switching DNS, 15% of traffic still hit the old database for another two hours because client resolvers were caching the old record. Lesson learned: plan for propagation delays, and test DNS cutover in staging first with realistic TTL settings.
Health Checks for Failover
Health checks let Route 53 verify that a target is actually responding before routing traffic to it. Combined with failover policies, this enables automatic traffic rerouting if your primary service goes down.
A typical setup: define a health check that pings an HTTP endpoint, then create two DNS records (primary and secondary) with failover routing:
resource "aws_route53_health_check" "primary_alb" {
ip_address = aws_lb.primary.dns_name
port = 443
type = "HTTPS"
resource_path = "/health"
failure_threshold = 3
request_interval = 30
tags = {
Name = "primary-alb-health"
}
}
resource "aws_route53_record" "app_primary" {
zone_id = aws_route53_zone.primary.zone_id
name = "app.example.com"
type = "A"
set_identifier = "primary"
failover_routing_policy {
type = "PRIMARY"
}
alias {
name = aws_lb.primary.dns_name
zone_id = aws_lb.primary.zone_id
evaluate_target_health = true
}
health_check_id = aws_route53_health_check.primary_alb.id
}
resource "aws_route53_record" "app_secondary" {
zone_id = aws_route53_zone.primary.zone_id
name = "app.example.com"
type = "A"
set_identifier = "secondary"
failover_routing_policy {
type = "SECONDARY"
}
alias {
name = aws_lb.secondary.dns_name
zone_id = aws_lb.secondary.zone_id
evaluate_target_health = false
}
}
The failure_threshold = 3 means Route 53 marks the primary as unhealthy after 3 consecutive failed checks. request_interval = 30 checks every 30 seconds. When the primary is down, queries automatically return the secondary. When the primary recovers, traffic gradually shifts back.
For a production e-commerce system we deployed, this setup caught a cascading failure in the primary region. Failover completed in under two minutes, with no manual intervention. Without it, we'd have lost traffic for the time it took an engineer to notice and manually update DNS.
Common Mistakes
- Forgetting to set nameservers at the registrar: You can define perfect Route 53 hosted zones in Terraform, but if the domain registrar still points to old nameservers, none of it matters. Automate this step or add it to a runbook so it doesn't slip.
- High TTLs during active development: If you're iterating on infrastructure and changing DNS frequently (like during a blue-green deployment), forget about reasonable TTLs—go low (30-60 seconds) and raise it once things stabilize.
- Skipping health check configuration: It's tempting to use alias records without health checks and assume everything works. Health checks add resilience with minimal complexity. They're worth the extra lines of Terraform.
- Not testing failover in staging: Failover routing policies often aren't tested until production hits them. Replicate your failover setup in a staging environment and practice toggling the primary down to see how it behaves.
- Mixing manual DNS edits with Terraform: If someone creates a DNS record manually in the AWS console, Terraform won't know about it. Use `terraform import` to bring manual records into state, or enforce a policy that all DNS changes go through Terraform.
Key Takeaways
- Structure hosted zones one per domain to keep DNS organized and access controls clear.
- Use alias records to point at AWS resources—they're automatic, free, and integrate with health checks naturally.
- Plan ahead by lowering TTLs a day or two before migrations to ensure fast DNS propagation during cutover.
- Combine health checks with failover routing policies to enable automatic traffic rerouting when your primary service fails.
- Test DNS changes in staging first, especially failover scenarios, before rolling to production.
- Keep DNS configuration entirely in Terraform; avoid manual edits that create drift and unpredictability.






Comments
Loading comments…