DEV Community

Bartłomiej Danek
Bartłomiej Danek

Posted on • Originally published at bard.sh

Smart DNS: Auto-Detecting Route53 Records in Terraform

Smart DNS: Auto-Detecting Route53 Records in Terraform

Every Route53 record in plain Terraform means a zone lookup, an explicit type, and for load balancers - a separate data source just to wire the alias.

data "aws_route53_zone" "this" {
  name = "api.example.com."
}

data "aws_lb" "api" {
  name = "api-nlb"
}

resource "aws_route53_record" "api" {
  zone_id = data.aws_route53_zone.this.zone_id
  name    = "api.example.com"
  type    = "A"

  alias {
    name                   = data.aws_lb.api.dns_name
    zone_id                = data.aws_lb.api.zone_id
    evaluate_target_health = true
  }
}
Enter fullscreen mode Exit fullscreen mode

With a module that infers everything, the same record becomes:

inputs = {
  name    = "api.example.com"
  records = "api-nlb-1234567890.elb.eu-west-1.amazonaws.com"
}
Enter fullscreen mode Exit fullscreen mode

zone auto-detection

name = "api.example.com"
        │
        └─ strip first label
                │
                ▼
        "example.com"  ──► aws_route53_zone lookup
Enter fullscreen mode Exit fullscreen mode

The module splits the FQDN, drops the first label, queries Route53 for the hosted zone. No zone_id from the caller.

For apex records or non-standard cases, zone_override skips this.

type inference

records value
      │
      ├─ type_override set? ───────────────► use that type (NS/MX/TXT/etc.)
      │
      ├─ is map?
      │     ├─ has "name" key? ────────────► alias A  (lookup LB by name)
      │     └─ has "tags" key? ────────────► alias A  (lookup LB by tags)
      │
      ├─ is string or list?
      │     ├─ all values are IPv4? ───────► A
      │     ├─ ends with .amazonaws.com? ──► alias A  (lookup LB by hostname)
      │     └─ else ───────────────────────► CNAME
      │
      └─ (nothing created if no branch matches)
Enter fullscreen mode Exit fullscreen mode

Each branch maps to a separate aws_route53_record resource using for_each over a conditional set - only one fires per invocation.

lb alias lookup modes

Mode Input How
By hostname records = "my-nlb-xxx.amazonaws.com" parse LB name from hostname prefix
By name records = { nlb = { name = "my-nlb" } } aws_lb data source by name
By tags records = { alb = { tags = {...} } } aws_lb data source by tag filter

All three produce an alias block with evaluate_target_health = true.

escape hatches

Scenario Solution
NS delegation type_override = "NS"
MX at zone apex type_override = "MX" + zone_override = "example.com"
TXT verification type_override = "TXT"
Private hosted zone private_zone = true
Custom TTL ttl = 300

the idea

DNS configuration is mostly mechanical - you know the hostname or IP address(es), you know what it points to. The type, the zone, whether it needs an alias block - those are derivable. This module pushes that derivation into Terraform so callers don't repeat it.

The tradeoff is explicitness. Plain Terraform is verbose, but every field is visible and validated. The module is terse, but inference can surprise you - especially the records = "..." string that silently becomes an alias lookup against AWS instead of a literal CNAME value.

It works best when:

  • you manage many records across consistent naming conventions
  • all your LBs follow predictable hostname patterns (*.<region>.elb.amazonaws.com)
  • your zones map cleanly to second-level domains (one zone per domain, no split-horizon edge cases)

It gets awkward when:

  • you need fine-grained TTL control per record
  • you have private hosted zones with the same name as public ones (need private_zone = true consistently)
  • your LB names are not stable - tag-based lookup is more resilient there

trade-off

records is type = any. Terraform cannot validate at plan time. A typo in a map key (naam instead of name) silently falls through to CNAME instead of alias. Review plan output before apply - the wrong record type shows up as a different resource being created.

full example

# CNAME
inputs = {
  name    = "api.example.com"
  records = "old-api.example.com"
}

# A record
inputs = {
  name    = "api.example.com"
  records = ["203.0.113.10", "203.0.113.11"]
}

# Alias to NLB by hostname
inputs = {
  name    = "api.example.com"
  records = "api-nlb-1234567890.elb.eu-west-1.amazonaws.com"
}

# Alias to ALB by tags
inputs = {
  name    = "api.example.com"
  records = {
    alb = {
      tags = {
        Name = "api-alb"
        Env  = "prod"
      }
    }
  }
}

# NS delegation with zone override
inputs = {
  name          = "example.com"
  records       = ["ns1.example.net", "ns2.example.net"]
  type_override = "NS"
  zone_override = "example.com"
}
Enter fullscreen mode Exit fullscreen mode

Originally published at https://bard.sh/posts/route53-auto-detecting-terraform/

Top comments (0)