The goal
You've been asked to point app.contoso.com at a new server or load balancer. This post covers the words you need, how to read what's there, how to make the change and how to prove it worked.
The basic terms
| Term | Meaning |
|---|---|
| Hosted zone | The container for one domain's DNS records |
| A record | A name that points to an IPv4 address |
| AAAA record | A name that points to an IPv6 address |
| CNAME | A name that points to another name |
| Alias | A Route 53 feature: a name that points to an AWS resource such as a load balancer |
| TTL | How many seconds resolvers may cache the answer |
| NS records | The name servers that hold the zone |
Step 1: find the zone and read its records
aws route53 list-hosted-zones \
--query "HostedZones[].{Name:Name,Id:Id,Private:Config.PrivateZone}" --output table
aws route53 list-resource-record-sets \
--hosted-zone-id Z0123456789ABCDEFGHIJ \
--query "ResourceRecordSets[].{Name:Name,Type:Type,TTL:TTL,Value:ResourceRecords[0].Value,Alias:AliasTarget.DNSName}" \
--output table
Look at one name only:
aws route53 list-resource-record-sets \
--hosted-zone-id Z0123456789ABCDEFGHIJ \
--query "ResourceRecordSets[?Name=='app.contoso.com.']"
The trailing dot matters here. Route 53 stores names as app.contoso.com..
Step 2: back up before you change anything
aws route53 list-resource-record-sets \
--hosted-zone-id Z0123456789ABCDEFGHIJ > zone-backup-$(date +%F).json
That file gives you the old values if you need to put them back.
Step 3: create or update a record
Save this as change.json. UPSERT means "create it, or update it if it exists":
{
"Comment": "Point app to new server, ticket 4521",
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "app.contoso.com",
"Type": "A",
"TTL": 300,
"ResourceRecords": [{ "Value": "203.0.113.10" }]
}
}
]
}
Apply it and wait until it's in sync:
CHANGE_ID=$(aws route53 change-resource-record-sets \
--hosted-zone-id Z0123456789ABCDEFGHIJ \
--change-batch file://change.json \
--query "ChangeInfo.Id" --output text)
aws route53 wait resource-record-sets-changed --id "$CHANGE_ID"
aws route53 get-change --id "$CHANGE_ID" --query "ChangeInfo.Status"
INSYNC means every Route 53 name server has the change.
Step 4: point a name at a load balancer
Use an alias instead of an IP address, because a load balancer's addresses change. First, get its DNS name and its own zone ID:
aws elbv2 describe-load-balancers --names alb-app \
--query "LoadBalancers[0].{DNS:DNSName,Zone:CanonicalHostedZoneId}"
{
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "app.contoso.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z35SXDOTRQ7X7K",
"DNSName": "alb-app-1234567890.ap-south-1.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
}
]
}
The HostedZoneId is the load balancer's zone ID from the command above, not your own zone's. The value shown here is just an example.
Alias or CNAME?
| Question | Use |
|---|---|
Is it the root of the domain, like contoso.com? | Alias. A CNAME isn't allowed there |
| Is the target an AWS resource, like an ALB or CloudFront? | Alias |
| Is the target outside AWS, on a subdomain? | CNAME |
Step 5: test the answer
Ask Route 53 directly. This skips caches and checks your zone's data:
aws route53 test-dns-answer \
--hosted-zone-id Z0123456789ABCDEFGHIJ \
--record-name app.contoso.com --record-type A
Then ask from the outside:
dig app.contoso.com +short
dig app.contoso.com @8.8.8.8 +noall +answer
On Windows:
Resolve-DnsName app.contoso.com -Server 8.8.8.8
When the change doesn't show up
| What you see | Cause | Fix |
|---|---|---|
| Old IP for a while | Cached until the old TTL runs out | Wait for the old TTL to pass |
test-dns-answer is right, public DNS is wrong | The domain's registrar uses other name servers | Compare the NS records below |
CNAME ... is not permitted | A CNAME can't share a name with other records | Remove the conflict or use an alias |
InvalidChangeBatch | Bad value or a DELETE that doesn't match exactly | Read the message and copy exact values |
| Works outside but not inside the VPC | A private zone with the same name hides it | See private hosted zones |
Compare the name servers in the zone with those the internet is using:
aws route53 get-hosted-zone --id Z0123456789ABCDEFGHIJ --query "DelegationSet.NameServers"
dig contoso.com NS +short
These two lists must match. If they don't, your zone isn't the one that's answering, and you need to fix it at the registrar.
Warning
Lower the TTL a day ahead of a planned move, for example to 60 seconds, then raise it again afterwards. Changing the TTL at the same moment as the IP doesn't help, because resolvers still hold the old answer for the old TTL.
Caution
Don't delete the zone's own NS and SOA records. Deleting a hosted zone and recreating it gives you different name servers, and the domain stops resolving until you update the registrar.
Prevent it next time
- Export the zone to a file on a schedule, and keep it safe.
- Manage records with Terraform or CloudFormation for anything important.
- Find out who changed what with CloudTrail. If records vanish, follow the deleted records runbook.