Terraform’s ability to manage AWS Route 53 is a game-changer for infrastructure as code, but it’s not just about creating DNS records; it’s about orchestrating your entire domain’s presence across AWS with predictable, repeatable deployments.

Let’s see how this plays out with a concrete example. Imagine you’re setting up a new application and need a public-facing DNS record.

module "app_dns" {
  source = "./modules/route53" # Assuming a local module for reusability

  zone_name = "myapp.example.com"
  records = [
    {
      name    = "www"
      type    = "A"
      ttl     = 300
      records = ["192.0.2.10"] # Example IP address
    },
    {
      name    = "@" # Root domain
      type    = "A"
      ttl     = 300
      records = ["192.0.2.10"]
    },
    {
      name    = "api"
      type    = "CNAME"
      ttl     = 600
      records = ["api.anotherdomain.com"]
    }
  ]
}

This module block, pointing to a local route53 module, is where the magic happens. It encapsulates the logic for creating Route 53 resources. Inside that ./modules/route53 directory, you’d have files like main.tf, variables.tf, and outputs.tf.

variables.tf would define inputs like zone_name and records. main.tf would contain the actual Terraform resource definitions:

# modules/route53/main.tf

resource "aws_route53_zone" "main" {
  name = var.zone_name

  tags = {
    ManagedBy = "Terraform"
  }
}

resource "aws_route53_record" "this" {
  for_each = { for record in var.records : "${record.name}-${record.type}" => record }

  zone_id = aws_route53_zone.main.zone_id
  name    = each.value.name == "@" ? aws_route53_zone.main.name : "${each.value.name}.${aws_route53_zone.main.name}"
  type    = each.value.type
  ttl     = each.value.ttl
  records = each.value.records
}

The aws_route53_zone resource creates the hosted zone itself. The aws_route53_record resource, using for_each to iterate over the list of records provided, creates each DNS record within that zone. Notice how the name attribute dynamically constructs the full record name, handling the root domain (@) case specifically.

The true power here is the abstraction. You can reuse this route53 module across multiple projects, ensuring consistency in how your DNS is managed. Need to add a new subdomain? Just update the records list in your .tfvars file or directly in the configuration. terraform apply and it’s done.

The mental model you build is one of declarative intent. You declare the desired state of your DNS – the zones, the records, their types, TTLs, and values – and Terraform figures out how to achieve it on AWS. It handles the API calls, state management, and idempotency, meaning running terraform apply multiple times with the same configuration results in the same state without unintended side effects.

Let’s dive deeper into a specific, less obvious aspect: managing weighted or latency-based routing policies. These aren’t just simple A or CNAME records; they involve multiple aws_route53_record resources, often tied to different AWS resources (like ALBs or S3 buckets configured for static website hosting).

Consider this snippet for weighted routing:

resource "aws_route53_record" "app_weighted" {
  zone_id = module.app_dns.zone_id # Assuming zone_id is output from the module
  name    = "app.myapp.example.com"
  type    = "A"
  ttl     = 60

  weighted_routing_policy {
    weight = 100
  }

  set_identifier = "primary-app" # Unique identifier for this record set
  alias {
    name                   = aws_lb.primary.dns_name
    zone_id                = aws_lb.primary.zone_id
    evaluate_target_health = true
  }
}

resource "aws_route53_record" "app_weighted_failover" {
  zone_id = module.app_dns.zone_id
  name    = "app.myapp.example.com"
  type    = "A"
  ttl     = 60

  weighted_routing_policy {
    weight = 0 # 0 weight means it's effectively disabled until weight is increased
  }

  set_identifier = "failover-app"
  alias {
    name                   = aws_lb.failover.dns_name
    zone_id                = aws_lb.failover.zone_id
    evaluate_target_health = false # Or true, depending on your failover strategy
  }
}

Here, you’re creating two A records with the same name (app.myapp.example.com) but different set_identifier values. This is how Route 53 groups records for advanced routing policies. The weighted_routing_policy block assigns weights, and the alias block points to specific AWS resources. Terraform will create an ALIAS record set in Route 53, containing these two individual records, managed as a single logical unit. The set_identifier is crucial; without it, Terraform would try to create two independent records with the same name and type, which is invalid. The evaluate_target_health parameter on the alias block is also a powerful lever, allowing Route 53 to automatically route traffic away from unhealthy targets.

The next step in mastering Route 53 with Terraform involves integrating it with CI/CD pipelines for automated DNS updates during deployments.

Want structured learning?

Take the full Route53 course →