Manage DNS with Amazon Route 53 and Terraform Cloud
12/27/2021
Update: May 21, 2023: Removed inaccurate statement that GitHub is not free for private repositories.
If you're new to Terraform - it manages infrastructure as code. In this particular instance, rather than going to the AWS console, navigating to Route 53, and manually making changes to DNS entries, we can do it with code. Behind the scenes, Terraform is really just making API calls - essentially the same ones that would be made from actions taken in the AWS console. This article describes how I set up a hosted zone in Amazon Route 53 for my domain.
Terraform Cloud
Terraform remembers the state of the infrastructure it manages. It does this, by default, by storing the state on your local machine. This, however, has disadvantages. You could lose your files and thus lose the known state of your infrastructure. You could decide to work on your code from a different machine, which doesn't have the state, and not be able to do anything.
There are many places you can store remote Terraform state. One of them is Terraform Cloud. Since it's free for up to five users, this is a good option for a developer managing infrastructure for a personal site.
VCS Provider
I'm going to use the "version control" workflow, because my source code for managing my site's DNS entries is going to be stored in a git repository. For the version control provider, I'm going to use GitLab. There's no reason for this repo to be public, so it will be private.
The documentation provided by HashiCorp was more than adequate. However, they don't address the option to let the auth token expire. I tried that to start with and it seems Terraform Cloud doesn't renew the token, so everything just breaks after a while. With the VCS provider (GitLab) set up, I'm now ready to move on and set up my workspace.
Workspace
After clicking on the button to create a new workspace in Terraform Cloud, I'm now shown the VCS provider I just set up. When I click on it, I'm presented with a list of repos in my GitLab.com account. However, I don't yet have a repo with my code in it for managing my domain's DNS entries using Route 53. So, I created a group in GitLab and a blank project inside it. Refreshing the page in Terraform Cloud, I now see the project. After selecting it, I'm prompted for a workspace name, and I choose personal-site-dns
.
Variables
To make authenticated API calls to my AWS account, Terraform Cloud needs credentials. Terraform Cloud stores variables in HashiCorp Vault. So, I need to set up an IAM user with only the permissions necessary for managing Route53 entries.
AWS IAM User and Permissions
Wait a minute. We're using Terraform Cloud to treat infrastructure as code, but now we're going to log in to the AWS console and manually create an IAM user and set its permissions? Yes. Technically, we could create a separate Terraform project to create this IAM user - but at some point, we have to get some existing credentials from somewhere. And credentials would be dangerous to have anywhere, as if you can create/modify IAM resources, then you can do anything.
When you first set up an AWS account, all you have is the root user, and you can generate IAM credentials for it and use those to bootstrap your AWS account. You could, if you wanted to, even go back in and wipe out the credentials in your Terraform Cloud workspace, and only use temporary credentials each time you update your IAM infrastructure. It's up to you.
IAM Resources
Policy
This is likely still more than is necessary, but it's much reduced from the wide permissions granted by the AWS-managed permissions for Route 53. I used Route53TerraformManageDNS
for the name of the policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LimitedRoute53Permissions",
"Effect": "Allow",
"Action": [
"route53:ListReusableDelegationSets",
"route53:ListTrafficPolicyInstances",
"route53:GetChange",
"route53:TestDNSAnswer",
"route53:GetHostedZone",
"route53:ListHostedZones",
"route53:GetHealthCheck",
"route53:ChangeResourceRecordSets",
"route53:ListHostedZonesByName",
"route53:ListTagsForResource",
"route53:ListTagsForResources",
"route53:GetAccountLimit",
"route53:GetCheckerIpRanges",
"route53:ListResourceRecordSets",
"route53:ChangeTagsForResource",
"route53:ListGeoLocations",
"route53:GetHostedZoneLimit",
"route53:GetHostedZoneCount",
"route53:UpdateHostedZoneComment",
"route53:GetDNSSEC"
],
"Resource": "*"
}
]
}
User
For the user name, I put terraform-cloud-personal-site-dns
. It needs to use an access key, and should not have console access. We need to attach the Route53TerraformManageDNS
policy directly to it. I saved the IAM credentials in my password manager.
Add Variables to Teraform Cloud
Create two workspace variables, using the IAM credentials for the new user. Note that the variable category should be set to "Environment variable" and not "Terraform variable."
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
Importing Existing Terraform Resources
Importing the Hosted Zone
When I registered my domain name, AWS created a hosted zone for me in Route 53. I need Terraform to know the state of that hosted zone - otherwise, it will try and create a new one, which I don't want.
This document outlines how to import a resource into a remote backend.
Here is an adjusted version of the contents of my main.tf file:
terraform {
backend "remote" {
organization = "my_organization"
workspaces {
name = "personal-site-dns"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_route53_zone" "zone" {
name = "my-domain.tld"
}
Next, log in to Terraform Cloud and import the resource:
terraform cloud
terraform init
terraform import aws_route53_zone.zone ZONE_ID_HERE
Importing DNS Records
There are some DNS records already in the hosted zone that were created automatically by Amazon when I registered the domain name:
- NS record
- SOA record
So let's get those added to main.tf:
resource "aws_route53_record" "ns" {
zone_id = aws_route53_zone.zone.zone_id
name = "my-domain.tld"
type = "NS"
ttl = "172800"
records = ["ns1_change_me.",
"ns2_change_me.",
"ns3_change_me.",
"ns4_change_me."]
}
resource "aws_route53_record" "soa" {
zone_id = aws_route53_zone.zone.zone_id
name = "my-domain.tld"
type = "SOA"
ttl = "900"
records = ["ns1_change_me. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"]
}
Now, import them into remote state:
terraform import aws_route53_record.ns ZONE_ID_HERE_my-domain.tld_NS
terraform import aws_route53_record.soa ZONE_ID_HERE_my-domain.tld_SOA
And now, check to see how our Terraform config compares to the actual state of these resources:
terraform plan
Exporting Data
I may make one or more Terraform projects in the future that will manage DNS entries in this same hosted zone. To do that, they will need to know the resource ID of the hosted zone. So it needs to be an output. In outputs.tf
, we will add this:
output zone_id {
type = string
description = "Resource ID of the hosted zone"
value = aws_route53_zone.zone.id
}
Start Using Version Control
None of this code is checked in to version control, at this point. But, it needs to be. So let's take care of that right now. We need a .gitignore
file, which HashiCorp provides.
git init
git remote add origin https://gitlab.com/YOUR_PROJECT/dns.git
git checkout -b first_branch
git add .
git commit -m "Initial commit"
git push origin first_branch
Then from GitLab.com, create and merge the merge request. Over in Terraform Cloud, you'll now see that this has started a run. If all is well, the run will succeed.