How to Set Up a Domain in Amazon Route 53 with Terraform
Edit: Oct 12, 2021
Please note the comment by Chris Harrison, below, regarding a breaking change with lists vs sets.
Original Start:
I’ve lately used Terraform to set up several domains in Route 53. My searches on the topic found posts more ambitious than this one explaining how to create an entire website using Terraform: domain, certificate, S3 bucket, and CloudFront distribution. Whether because of the articles’ larger focus or my own density, I couldn’t really understand them. I’m loathe to copy-paste code without understanding what it does. I wanted to understand the Terraform resources, how they interacted, and where the dependencies and boundaries lay. I also wanted to understand what was happening with Route 53 and Certificate Manager. Here’s what I learned.
Scope
This post covers creating a domain and adding a certificate for it. It does not cover creating a website to serve over HTTPS from that domain. There’s no CloudFront Distribution, API Gateway, S3 Bucket, or Elastic Beanstalk. Creating stuff to serve from your domain is a topic for another blog post. I may even write one someday. This material, though, establishes the foundation on which you can create an actual website.
Terraform Resources
To create a hosted zone, with certificate, you use four Terraform resources:
- aws_route53_zone — creates the Route 53 hosted zone.
- aws_acm_certificate — requests the certificate from Certificate Manager.
- aws_route53_record — creates the CNAME record Certificate Manager uses to validate you own the domain.
- aws_acm_certificate_validation — waits for the certificate to be issued.
These four resources perform a dance to:
- Create the hosted zone.
- Request a certificate.
- Insert the CNAME record that Certificate Manager specifies.
- Wait for Certificate Manager to validate the CNAME record and issue certificate.
The sequence diagram below illustrates the process (credit: Mermaid):
Route 53 Hosted Zone
Creating a hosted zone in Route 53 using Terraform requires only one resource, aws_route53_zone, with one argument, name. The code for that looks like this:
resource "aws_route53_zone" "my_hosted_zone" {
name = var.domain_name
}
After creation, the zone contains two DNS records:
- Start of Authority (SOA) — the domain’s entrypoint
- Name Servers (NS) — the name servers for this zone
Note: If you registered your domain through Route 53, Amazon already created your hosted zone. You can still manage the hosted zone through Terraform, even though you didn’t create it. You just import the hosted zone into Terraform’s state before running plan or apply. Go to Route 53 and copy your hosted zone’s ID, and then import:
$ terraform import aws_route53_zone.my_hosted_zone <HOSTED_ZONE_ID>
The name servers listed in the NS record in your hosted zone must match the NS record in your domain registrar. If you registered through Route 53, they already match. If you used some other registrar, copy the four servers from the NS record. Then, update the NS record in your registrar’s record for your domain.
Certificate Request
You request a certificate using the aws_acm_certificate resource, specifying the domain you want the certificate for and the method you want to use to validate that you, indeed, own this domain. You can also request any subject alternatives names (SANs) this certificate covers (e.g., a wildcard certificate for any subdomains in your domain).
You can also specify a Name tag for friendly display in the Certificate Manager console. In addition, you should tell Certificate Manager that, when the time comes to renew your certificate, it should create the new certificate before deleting the old one. The Terraform code looks like this:
resource "aws_acm_certificate" "my_certificate_request" {
domain_name = var.domain_name
subject_alternative_names = ["*.{var.domain_name}"]
validation_method = "DNS"
tags = {
Name : var.domain_name
}
lifecycle {
create_before_destroy = true
}
}
The Certificate Request replies with an outrageous-looking CNAME record. More on this in the next section.
DNS Validation
To issue a certificate, Certificate Manager must know that you control the domain that you’re requesting a certificate for. How else could it vouch for you when people hit your site? There’s an old-fashioned email flow for domain validation, but DNS validation is faster and simpler.
When you request the certificate, Certificate Manager returns a CNAME record for you to insert into your hosted zone. Then, it pings that domain and verifies that the value it returns matches what it expects. The CNAME record is actually returned as an array of domain_validation_options, each of which has four fields:
- domain — the domain this record is for (useful for SANs, not covered here)
- resource_record_name — the name for the DNS record
- resource_record_type — the type of the DNS record: “CNAME”
- resource_record_value — the value this CNAME record points to
Use an aws_route53_record resource to insert the CNAME record, using your hosted zone’s ID, the CNAME information returned from the certificate request, and a time-to-live for your CNAME record.
resource "aws_route53_record" "my_validation_record" {
zone_id = aws_route53_zone.my_hosted_zone.zone_id
name = aws_acm_certificate.my_certificate_request.domain_validation_options.0.resource_record_name
type = aws_acm_certificate.my_certificate_request.domain_validation_options.0.resource_record_type
records = [aws_acm_certificate.my_certificate_request.domain_validation_options.0.resource_record_value]
ttl = var.ttl
}
You might think that, once your request has been validated, you can delete this CNAME record. It’s kind of ugly, after all. Don’t do it. When your certificate is about to expire, Certificate Manager will automatically renew your certificate, as long as it can still validate, using this same CNAME record, that you control this domain.
Waiting
You don’t actually have to wait for the validation to process, unless your Terraform plan is going to do something like add a CloudFront distribution that uses your new certificate and expects it to be valid. To wait for the certificate to be successfully issued, use the aws_acm_certificate_validation resource. You specify the certificate’s ARN and your CNAME record’s fully-qualified domain name (FQDN), like this:
resource "aws_acm_certificate_validation" "my_certificate_validation" {
certificate_arn = aws_acm_certificate.my_certificate_request.arn
validation_record_fqdns = [aws_route53_record.my_validation_record.fqdn]
}
Troubleshooting
You might see an error like this in Certificate Manager:
One or more domain names have failed validation due to a certificate authority authentication (caa) error
I saw this when I was creating a hosted zone and certificate for a subdomain for a domain that was managed in some other company’s DNS. To fix this error, I had to create a Certification Authority Authorization (CAA) record in my hosted zone to authorize Certificate Manager to issue me a certificate for my subdomain. That code uses the aws_route53_record resource and looks like this:
resource "aws_route53_record" "my_caa_record" {
zone_id = aws_route53_zone.my_hosted_zone.zone_id
name = var.domain_name
type = "CAA"
records = [
"0 issue \"amazon.com\"",
"0 issuewild \"amazon.com\""
]
ttl = var.ttl
}
The first record, issue, says that amazon.com can issue a certificate, and the second record, issuewild, says that amazon.com can issue a wildcard certificate.
Summary
Creating a domain and certificate using Terraform isn’t complex, once you understand the interactions between the four Terraform resources and what Route 53 and Certificate Manager are doing. I hope this post helps you understand the process.
Thank for the really useful article. From version 3.0.0 of the AWS provider, the domain_validation_options (among other things) have changed from a list to a set. As a result they cannot be referenced via list indices, and must be called using for_each. See this github link for details:
https://github.com/hashicorp/terraform-provider-aws/issues/10098#issuecomment-663562342
Thanks for this! At some point, I’ll get around to updating the article. For now, I’ve edited to call attention to your comment.
Highly recommend adding the update from what he sent. Otherwise, this was very helpful. Thank you!