DNS zone delegation with multiple accounts, Route53 and Terraform

I spent the evening untangling a friends domain setup, and thought it might be useful to document it a bit. It also highlights a bit of DNS which I don't think most people understand. This is fairly AWS specific, tho the concepts should work with any DNS or cloud provider.

The situation

Lets say you have a single domain for your project - site.nz - and you want to also have a UAT and TEST setup for this site.

Like a normal person, you have these split into 4 AWS accounts:

  • management / identity/ billing - this one has no compute resources, but controls users, payments etc.
  • production - all the prod stuff lives here, usually access is tightly controlled
  • uat - all the uat stuff lives here
  • test / dev - all the test stuff lives here.

So how do you go about doing the DNS for this? The easiest way is with Zone Delegation.

Whats a zone?

A zone is a collection of DNS records, which is a single unit, and will be served by a given set of name servers. It normally consists of a SOA (Start of Authority) record, a set NS records, and any number of other records such as host (A), text (TXT), mail exchange (MX) etc.

For a zone to be complete, it must have an SOA and one or more NS.

dig fastchicken.co.nz SOA

;; ANSWER SECTION:
fastchicken.co.nz.	300	IN	SOA	ns-1470.awsdns-55.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

➜  ~ dig fastchicken.co.nz NS

;; ANSWER SECTION:
fastchicken.co.nz.	21600	IN	NS	ns-1470.awsdns-55.org.
fastchicken.co.nz.	21600	IN	NS	ns-1881.awsdns-43.co.uk.
fastchicken.co.nz.	21600	IN	NS	ns-70.awsdns-08.com.
fastchicken.co.nz.	21600	IN	NS	ns-926.awsdns-51.net.

A zone is usually for one "top level" domain. In our case, it'll be for site.nz. You can then include records for subdomains (www.site.nz), but you can also set it up to say "all of subdomain.site.nz is over there", using Zone Delegtion to another zone. That "other zone" has a top level of subdomain.site.nz and can then also contain other records - same as the original top level one.

If a lot of this doesn't make sense, have a read of Julia Evans' 'zine on DNS - How DNS works.

At the end of the day, you are setting up a "tree" of answers to "I want site.nz, who do I ask" and within that, "I want subdomain.site.nz, who do I ask?".

Of course, DNS is a full tree, so it can also answer "I have nothing, and I want .nz, who do I ask?" (your DNS settings on your computer) and "I have site.nz, who do I ask?" (your registrar usually controls this, by pushing it into the .nz space)

How do I model this?

First, you need to find where you domain is registered or hosted. This is normally where you registered it - eg hover.com - or AWS if you used them.

This will allow you to set the nameserver (NS) records for where anyone can find the details of your domain. Work out where this is, and we'll put values in here shortly.

The main zone

Next, we need to setup the main zone in the production account. This is the one which handles site.nz. As your production site will use site.nz, www.site.nz and api.site.nz, this should live in your production account.

You can set the zone up in Route53 (this all works with any DNS provider, but I use AWS, so... Route53 it is!) for site.nz, and add in A records for the root (site.nz), www and api, as needed. These could be A records to an IP, or ALIAS records to another AWS service like Cloudfront or API Gateway.


// most likely just a `aws_route53_zone` record
module "dns_zone" {
  source = "../modules/dns_zone"
  environment = "prod"

  domain = "site.nz"
}

// this will be a bunch of `aws_route53_record` records
module "dns_records" {
  source = "../modules/dns_records

  environment = "prod"
  root_domain = module.dns_zone.domain
  zone_id     = module.dns_zone.zone_id

  web_alias = "d-prod.cloudfront.net"
  api_alias = "o-prod.apigateway.aws"
}

You can also add other DNS records like MX (mail) or TXT as needed.

The output of the dns_zone module would be the nameservers for this zone - NS records. This is provided by AWS automatically. It'll look something like

ns-1470.awsdns-55.org.
ns-1881.awsdns-43.co.uk.
ns-70.awsdns-08.com.
ns-926.awsdns-51.net.

You can plug these into your domain registrar (hover, godaddy, AWS, whoever) so the world knows where to look for site.nz. Note this change and take 2-24 hours to propogate.

Delegated Zones

Next up, you need a zone each for UAT and TEST. These should live in their respective accounts, so assume a role into there and make an zone in each one for uat.site.nz and test.site.nz respectively. You can also add A records as needed for the root, www and api hosts.

module "dns_zone" {
  source = "../modules/dns_zone"
  environment = "test"

  domain = "test.site.nz"
}

Once those are setup, look at the NS records that were created when you made the zone - you'll need these. They will look like the NS records above.

Now go back to the main domain (site.nz) in production, and add a new NS record for each of uat.site.nz and test.site.nz, using the NS records you got from each zone.


# in the production config

resource "aws_route53_record" "uat_ns" {
  name    = "uat.site.nz"
  zone_id = module.dns_zone.zone_id
  type    = "NS"
  ttl     = "300"
  records = [
    "ns-123.awsdns-46.com",
    "ns-456.awsdns-30.net",
    "ns-1234.awsdns-21.co.uk",
    "ns-5678.awsdns-13.org",
  ]
}

resource "aws_route53_record" "test_ns" {
  name    = "test.site.nz"
  zone_id = module.dns_zone.zone_id
  type    = "NS"
  ttl     = "300"
  records = [
    "ns-4444.awsdns-46.com",
    "ns-5555.awsdns-30.net",
    "ns-666.awsdns-21.co.uk",
    "ns-7777.awsdns-13.org",
  ]
}

And you're done.

You can now use a consistent, templated configuation for each domain - a single module which makes root, www and api in the provided zone, your names are consistent (no more api-test.site.nz), and the responsibiliy for each one sits with the appropriate account.

module "dns_records" {
  source = "../modules/dns_records

  environment = "test"
  root_domain = module.dns_zone.domain
  zone_id     = module.dns_zone.zone_id

  web_alias = "d12345.cloudfront.net"
  api_alias = "o-12345.apigateway.aws"
}

Summary

DNS isn't THAT hard, tho it can be difficult to grok initially. Getting an undertstanding of zones and zone delegation can make breaking up and managing your domains a lot easier, esp if you are using reusable terraform modules.

Nic Wise

Nic Wise

Auckland, NZ