Quick Links

If you've got servers running at home, you won't be able to route a domain to them without a static IP address. Instead of paying for a dynamic DNS service, you can build your own using AWS Route 53.

Dynamic DNS Isn't Complicated

Dynamic DNS is fairly simple in concept. A daemon runs on the client machine, and regularly checks the public-facing IP address for any changes. If the IP address changes, the daemon sends an update to the DNS provider, which changes the record. This is often offered as a paid service at many domain registrars and DNS providers.

AWS doesn't have a specific service for providing dynamic DNS, but it's fairly simple to set up yourself. The "AWS way" of doing this would be to set up a CloudWatch event that triggers a Lambda function in response to infrastructure changes. (Although you should probably just use a load balancer and auto-scaling group in most scenarios.)

However, if you want to set up dynamic DNS for a home server or another non-AWS device that will have frequent IP address changes, scripting it is pretty easy. Route 53 has simple CLI commands that you can use to update DNS records from the command line; hooking this up to a cron job that watches for a change to the public IP address and runs the AWS CLI will get the job done.

As far as pricing goes, Route 53 doesn't really cost much---a flat $0.50 per month fee for each domain name, plus a few minor charges for DNS lookups based on usage. A records are free (the most common lookup), so you probably won't see more than a few pennies per month on your bill unless you're pulling serious traffic. For comparison, DynDNS pricing starts at $55 per year.

Setting Up the AWS Side of Things

To get started, head over to the AWS Route 53 Management console. If you don't have a domain, you can register one under "Registered Domains" for fairly cheap, usually just the price of the ICANN registration fee. If your domain isn't currently in Route 53, you'll have to transfer it over, which is an easy but lengthy process.

Find or create the hosted zone for your domain, which will contain all of the records. You'll want to make note of the hosted zone ID, as you'll need it for the script.

Note the hosted zone ID.

You'll want to make a placeholder A record, so that the script has something to reference. You can set this to something obviously not correct---

        255.255.255.255
    

would work---to test the script's functionality.

You'll also need to set up the AWS CLI, which you can do with:

curl "https://d1vvhvl2y92vvt.cloudfront.net/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
    

unzip awscliv2.zip

sudo ./aws/install

And configure it with a IAM user credentials with:

aws2 configure

Writing the Script

If you just want the script, you can refer to this gist, but we'll explain how it's set up, because it's cool. If you're just copying and pasting, you'll have to change the HOSTED_ZONE_ID and NAME variables to match the record you're trying to update. Note that this script also uses the aws2 CLI, which you'll have to change if you're using version 1.

First off, we're gonna need a way to get our public-facing IP address programmatically. For this, we can use AWS's checkip.amazonaws.com API. It's entirely free, and there's no rate limit. You could also use api.ipify.org for this purpose, which is also free and unlimited. We'll load this into a variable and save it for later.

IP=$(curl https://api.ipify.org/)

Next, we'll have to validate this to make sure we got back a valid IP address and not an error code or anything malformed. Simply checking that the input is period-separated numbers is enough for this purpose, so bit of regex alongside the =~ operator (returns true if regex matches the left side input, very useful) will do the trick:

if [[ ! $IP =~ ^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$ ]]; then
    

exit 1

fi

We'll have to compare this to our old IP address to see if anything changed. We could store this on disk as a file in /tmp/, but that's messy and prone to errors. Instead, we'll query Route 53 directly with list-resource-record-sets, and filter the IP address out of the record we're trying to update with jq:

aws2 route53 list-resource-record-sets --hosted-zone-id Z1VCYR76DBUXPL | 
    

jq -r '.ResourceRecordSets[] | select (.Name == "'"$NAME"'") | select (.Type == "'"$TYPE"'") | .ResourceRecords[0].Value' > /tmp/current_route53_value

This saves it to /tmp/current_route53_value, which we can then use to check with grep, which is fairly resilient:

if grep -Fxq "$IP" /tmp/current_route53_value; then
    

echo "IP Has Not Changed, Exiting"

exit 1

fi

Finally, we prepare the payload for change-resource-record-sets. This needs to be in JSON, so we'll output to a file on disk and send it as an argument to the command.

cat > /tmp/route53_changes.json << EOF
    

{

"Comment":"Updated From DDNS Shell Script",

"Changes":[

{

"Action":"UPSERT",

"ResourceRecordSet":{

"ResourceRecords":[

{

"Value":"$IP"

}

],

"Name":"$NAME",

"Type":"$TYPE",

"TTL":$TTL

}

}

]

}

EOF

#update records

aws2 route53 change-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --change-batch file:///tmp/route53_changes.json

Everything comes together to form the following script. You'll want to change the HOSTED_ZONE_ID and NAME variables to match the record you're trying to update. Note that NAME has a period at the very end.

#!/bin/bash
    

#Variable Declaration - Change These

HOSTED_ZONE_ID="XXXXXXXXXXXX"

NAME="example.com."

TYPE="A"

TTL=60

#get current IP address

IP=$(curl http://checkip.amazonaws.com/)

#validate IP address (makes sure Route 53 doesn't get updated with a malformed payload)

if [[ ! $IP =~ ^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$ ]]; then

exit 1

fi

#get current

aws2 route53 list-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID |

jq -r '.ResourceRecordSets[] | select (.Name == "'"$NAME"'") | select (.Type == "'"$TYPE"'") | .ResourceRecords[0].Value' > /tmp/current_route53_value

cat /tmp/current_route53_value

#check if IP is different from Route 53

if grep -Fxq "$IP" /tmp/current_route53_value; then

echo "IP Has Not Changed, Exiting"

exit 1

fi

echo "IP Changed, Updating Records"

#prepare route 53 payload

cat > /tmp/route53_changes.json << EOF

{

"Comment":"Updated From DDNS Shell Script",

"Changes":[

{

"Action":"UPSERT",

"ResourceRecordSet":{

"ResourceRecords":[

{

"Value":"$IP"

}

],

"Name":"$NAME",

"Type":"$TYPE",

"TTL":$TTL

}

}

]

}

EOF

#update records

aws2 route53 change-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --change-batch file:///tmp/route53_changes.json >> /dev/null

Set Up Your Crontab

You can edit your crontab with:

crontab -e

You can set this script to run every minute, as it's fairly lightweight. Give cron a path to your script on disk. You can send the output to a log file, or to /dev/null if you don't want to deal with it.

* * * * * /home/user/update_dns.sh >/dev/null 2>&1