Terraforming DNS with AWS Route53


  1. First steps with Terraform in AWS
  2. Terraforming DNS with AWS Route53 (current)
  3. Terraforming AWS VPC

AWS Route53 is a DNS service used to perform three main functions: domain registration, DNS routing, and health checking. In this post, we are going to cover how to automate the configuration of AWS Route53 as your DNS service using Terraform. If you are not familiar with Terraform, you can check my introductory post here.

If you want to go straight to the code, you can check it out at aws-terraform-examples/02-terraforming-dns-with-aws-route53.

Already back? Great! Let’s dive in.

Key concepts

Before getting into the implementation, let’s briefly mention some of the essential concepts around DNS and AWS Route53

  • Name servers: Servers in the DNS that help translate domain names (www.example.com) into IP addresses. They can be either DNS resolver or authoritative name servers.
  • Hosted zones: A container for DNS records, including information about how to route traffic for a domain (example.com) as well as its subdomains (sub.example.com).
  • DNS record: A particular entry in the hosted zone that specifies the traffic routing for the domain or subdomain.
  • Time To Live (TTL): The amount of time, in seconds, that the DNS resolver should cache the values for a record before submitting another request to the authoritative name servers.

This was just a general overview of the concepts that we are going to be leveraging for our infrastructure configuration. Let’s move now to configure our DNS in AWS using Terraform.

Implementation

To fully work with the code examples in this section, it is recommended that you use a domain that you own since you would need to configure the AWS Route53 nameservers on your domain registrar settings.

Configuring zone and nameservers

The first step to configure the DNS service for your domain is to create the public hosted zone, which you can declare in Terraform as shown below.

resource "aws_route53_zone" "example" {
  name     = "example.com"
}

As part of the creation of Route53 zones, the name server (NS) record, and the start of a zone of authority (SOA) record are automatically created by AWS. By using the allow_overwrite option below, Terraform is then capable of managing them in a single execution without the need for a subsequent terraform import.

resource "aws_route53_record" "nameservers" {
  allow_overwrite = true
  name            = "example.com"
  ttl             = 3600
  type            = "NS"
  zone_id         = aws_route53_zone.example.zone_id

  records = aws_route53_zone.example.name_servers
}

After this, if you are using a domain registrar other than Route53, you will need to add the name servers associated with your zone in Route53 to the configuration settings on your domain registrar website.

Configuring email

Let’s go through a possible email configuration as an example. We are going to be using ProtonMail as our email server of choice.

Verification

First, we need to set up email verification. This is required by the email service to confirm that you own the domain. In the following example, we create a TXT record to hold the verification value.

resource "aws_route53_record" "protonmail_txt" {
  zone_id = aws_route53_zone.example.zone_id
  name = ""
  type = "TXT"
  ttl = 300

  records = [
    "protonmail-verification=<random_number>"
  ]
}

The TXT record gets associated with the top-level domain in the zone by pointing to our previously created AWS Route53 zone using the zone_id attribute and setting the name attribute to empty. This effectively makes the TXT record refer to example.com in this scenario.

MX records

The MX record specifies the mail server responsible to receive your domain’s email.

resource "aws_route53_record" "protonmail_mx" {
  zone_id = aws_route53_zone.example.zone_id
  name = ""
  type = "MX"
  ttl = 1800

  records = [
    "10 mail.protonmail.ch.",
    "20 mailsec.protonmail.ch."
  ]
}

In TXT record configuration above, we are associating two ProtonMail servers for email deliverability. A lower number means a higher priority, therefore emails will be sent first to mail.protonmail.ch server and mailsec.protonmail.ch as a fallback in case of failure. Similar to the TXT record discussed before, the MX record is associated with the top-level example.com domain.

Sender Policy Framework (SPF)

It is an authentication mechanism to validate that an email coming from a particular domain is being sent by an authorized IP address. For SPF, we need to use a TXT record entry specifying that the ProtonMail servers are authorized to send the emails. Since we already have a record resource representing a TXT record, we will reuse it and add a new entry in its records attributes as shown below.

resource "aws_route53_record" "protonmail_txt" {
  zone_id = aws_route53_zone.example.zone_id
  name = ""
  type = "TXT"
  ttl = 1800

  records = [
    "protonmail-verification=<random_number>",
    "v=spf1 include:_spf.protonmail.ch mx ~all"
  ]
}

DomainKeys Identified Mail (DKIM)

It is another authentication technique that leverages cryptography to verify that email is sent by trusted servers. To manage the ProtonMail keys, we need to configure CNAME records to hold the public encryption keys. These keys will be used by the receiving servers to validate the emails, making sure that they haven’t been tampered. Below, we define three new CNAME records in Terraform, one for each public encryption key provided by the email service.

resource "aws_route53_record" "protonmail_dkim_1" {
  zone_id = aws_route53_zone.example.zone_id
  name = "protonmail._domainkey"
  type = "CNAME"
  ttl = 1800

  records = [
    "domain_key1"
  ]
}

resource "aws_route53_record" "protonmail_dkim_2" {
  zone_id = aws_route53_zone.example.zone_id
  name = "protonmail2._domainkey"
  type = "CNAME"
  ttl = 1800

  records = [
    "<domain_key2>"
  ]
}

resource "aws_route53_record" "protonmail_dkim_3" {
  zone_id = aws_route53_zone.example.zone_id
  name = "protonmail3._domainkey"
  type = "CNAME"
  ttl = 1800

  records = [
    "domain_key3"
  ]
}

Gotcha

While reading several online guides, it is common to see the usage of @ as a hostname to refer to the top-level domain (in this post example.com). In AWS Route53, that doesn’t work and instead, it is necessary to set the name attribute as empty to point to the root domain in the zone where the record is being added.

Resources

Below, it is a condensed list of all the resources mentioned throughout the post as well as a few others I consider may be of interest to deepen your knowledge.

DNS:

AWS Route53:

Terraform:

Email:

Conclusions

Let’s briefly recap what we discussed in this post. First, we looked at some of the basic concepts around AWS Route53 and DNS. Next, we looked at how to attach a domain to AWS Route53 by using public hosted zone records and the nameservers. Afterward, we went through the steps of configuring an email service on our domain using different DNS records such as MX, TXT and CNAME. Finally, I analyzed a common gotcha regarding the usage of @ as the hostname and listed common resources mentioned throughout the post.

Thank you so much for reading this post. Hope you enjoyed reading it as much as I did writing it. See you soon and stay tuned for more!!

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.