Route 53 Split-Horizon DNS gives different answers to the same DNS query depending on where the query originates, but it’s not magic, it’s just a clever use of resource record sets.
Imagine you have a web application running on AWS, accessible both publicly and internally within your VPC. You want users outside your VPC to hit a public IP address, while users inside hit a private IP address. This is a classic split-horizon DNS scenario.
Here’s how it looks in action with Route 53.
First, we need a hosted zone. Let’s say we’re managing example.com.
$ aws route53 list-hosted-zones
{
"HostedZones": [
{
"Id": "/hostedzone/Z1ABCDEFGHIJKLMN",
"Name": "example.com.",
"CallerReference": "...",
"Config": {
"Comment": "Publicly accessible hosted zone",
"PrivateZone": false
},
"ResourceRecordSetCount": 5
}
]
}
Now, let’s create a public record for www.example.com that resolves to a public Elastic IP.
aws route53 change-resource-record-sets --hosted-zone-id Z1ABCDEFGHIJKLMN --change-batch '
{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "www.example.com.",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "54.12.34.56"
}
]
}
}
]
}'
When a user on the internet queries www.example.com, Route 53 will happily return 54.12.34.56.
But what about internal users? We need a separate, private hosted zone for example.com that’s associated with our VPC.
aws route53 create-hosted-zone --name example.com --caller-reference "private-example-com-$(date +%s)" --hosted-zone-config "Comment=Private hosted zone for example.com,PrivateZone=true" --vpc "VPCId=vpc-0123456789abcdef0,Region=us-east-1"
This command creates a new hosted zone, but importantly, sets PrivateZone to true and associates it with a specific VPC. Now, inside vpc-0123456789abcdef0, DNS queries for example.com will be directed to this private zone.
Within this private zone, we’ll create another record for www.example.com, but this time pointing to an internal, private IP address.
# First, get the ID of the private hosted zone created above
PRIVATE_HOSTED_ZONE_ID=$(aws route53 list-hosted-zones-by-name --dns-name example.com --query "HostedZones[?Config.PrivateZone==`true`].Id" --output text)
aws route53 change-resource-record-sets --hosted-zone-id $PRIVATE_HOSTED_ZONE_ID --change-batch '
{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "www.example.com.",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "10.0.1.100"
}
]
}
}
]
}'
Now, when a DNS resolver within vpc-0123456789abcdef0 queries www.example.com, Route 53 will see that the query is coming from a VPC associated with the private hosted zone and will return 10.0.1.100. A query from anywhere else will hit the public zone and get 54.12.34.56.
The key here is that Route 53 doesn’t perform a "lookup" to see if the IP address is public or private. It simply checks the origin of the DNS query. If the query comes from a VPC that has a private hosted zone for a given domain, Route 53 will use that private zone. If the query comes from outside any associated VPC, it will use the public hosted zone for that domain. You can even have overlapping domain names between public and private zones, and Route 53 will correctly resolve them based on the query source.
The most surprising thing about this setup is that you don’t explicitly link the public and private zones together. Route 53’s behavior is determined by the PrivateZone flag on the hosted zone and its association with VPCs. A DNS resolver within a VPC will always prefer to query a private hosted zone for a matching domain name over a public one, even if both exist.
This mechanism is crucial for maintaining consistent application behavior while leveraging AWS’s internal networking for improved security and performance.
The next challenge is handling subdomains that should be public, even when accessed from within the VPC.