James Thorpe

Dynamic DNS with Bash, Cron and Route 53

Dec 30, 2019 AWS DNS

I used to have a fairly static home IP, with a DNS entry pointed at it that I manually updated on the rare occasion it actually changed. We moved in March '18, and with the move came a new internet provider - the IP now changes frequently. I've finally got around to sorting out a dynamic DNS for those times I need it.

I've mentioned before that I have an AWS account - this website is being served statically from S3 - it made sense to try to use Route 53 for this job. I have a couple of servers running in the house, and opted to run some code on a linux box I use for hosting VMs.

Initially I'd planned on writing a small .NET application to handle this, that's what I work in in my day job - I'm fairly comfortable there. Then I figured it shouldn't be hard to script it - let's have a go at a bash script!

First off, I created the new DNS entry in Route 53 - a simple A record, pointing home.example.com to 127.0.0.1. Next up, querying it to find out the current value. After reading around a bit, I settled on:

dig +short home.example.com

This gives the IP as the single output. I then needed a way to find out my current public IP. This machine is behind a NAT firewall, so it doesn't know it directly - instead I had to reach out to a service on the internet to tell me what it is. There's plenty of these around, I decided to use icanhazip.com - it seems straightfoward enough. More information about it is available on major.io. To use it in a script:

curl ipv4.icanhazip.com

Simple! Next, assign the result of the two and compare them. Here's the full script so far:

#!/bin/bash
myip="$(curl ipv4.icanhazip.com)"
dnsip="$(dig +short home.example.com)"
echo Current IP: $myip
echo DNS IP: $dnsip

if [ "$myip" == "$dnsip" ]
then
    echo IPs match, nothing to do
else
    echo IPs differ, need to update DNS
fi

The last requirement is to then push the new IP to Route 53. Installing the AWS CLI tools is easy enough:

yum install awscli

Then run:

aws configure

This will prompt you for an access key and a secret key for an IAM user for scripts to use by default. I already have an IAM user set up for this purpose - I reused it here after adding the Route53 policy in the IAM manager screens.

To update the record, you send a Change Resource Record Sets request. There are a few ways to build up the request, I opted for inline JSON:

#!/bin/bash
myip="$(curl ipv4.icanhazip.com)"
dnsip="$(dig +short home.example.com)"
echo Current IP: $myip
echo DNS IP: $dnsip

if [ "$myip" == "$dnsip" ]
then
    echo IPs match, nothing to do
else
    echo IPs differ, need to update DNS

    aws route53 change-resource-record-sets --hosted-zone-id=MYHOSTEDZONEID --change-batch='{
        "Changes": [
            {
                "Action": "UPSERT",
                "ResourceRecordSet": {
                    "Name": "home.example.com",
                    "Type": "A",
                    "TTL": 300,
                    "ResourceRecords": [
                        {
                        "Value": "'"$myip"'"
                        }
                    ]
                }
            }
        ]
    }'

fi

The main thing to note on this step is the single quotes surrounding the JSON to allow embedding of double quotes easily, but the gotcha being that the $myip variable won't expand in single quotes, so you need to end the single quotes, output the IP, then start them again. Also note you'd need to swap out your hosted zone ID.

Save the script and make it executable with chmod 775 updateip. At this point, it works. But I still need to run it manually to get things to update. Time to learn about cron!

More researching led me to the command crontab -e to edit my own user's crontab file. While writing this blogpost, I also found out about running crontab without arguments - be careful not to do that, you'll clobber your crontab file!

crontab -e started up vim. So I hit enough random keys for it to tell me to type :quit to get out again, then used export EDITOR=nano; crontab -e to get to a more familiar environment. A quick Google search led me to the incantation to run something every 5 minutes, and I then had this in my crontab file:

*/5 * * * * ~/Scripts/updateip

I went back to the Route 53 console and updated the record to 127.0.0.1, and waited a few minutes. Cron kicked in, and the IP was updated back again. Then I saw my user had new mail - and this happened every time the job ran. I don't have any mail on this box, so I didn't want this script starting to clutter things up. After a quick trip back to Google, my crontab now looks like this:

MAILTO=""
*/5 * * * * ~/Scripts/updateip

No more emails, and a nice shiny new DNS entry that's kept up to date.

Back to posts