Mike Slinn
Mike Slinn

Handcrafted Dynamic DNS for AWS Route53

Published 2022-01-30.
Time to read: about 4 minutes.

This site is categorized under AWS, Bash.

Now that I have fiber-optic internet service in my apartment, with 500 GB/s upload and download, I thought I would save money by hosting my scalacourses.com website on an Ubuntu server that runs here, instead of AWS. My home IP address is quite stable, and only changes when the fiber modem boots up. The modem is branded as a Bell Home Hub 4000, but I believe it is actually made by Arris (formerly known as Motorola).

It makes little sense to pay the commercial cost of dedicated dynamic DNS services (typically $55 USD / year) when it is so easy to automate, and the operational cost is less than one cent per year.

Because I continue to use AWS Route53 for DNS, I wrote a little script that automatically checks my public IP address, and upserts a Route53 record for my home IP address whenever the IP address changes.

The approach shown here could be used for all DNS servers that have a command-line interface, not just AWS Route53. Alternatives include Azure DNS, Cloudflare DNS, DNSMadeEasy, DNSimple, Google Cloud DNS, and UltraDNS.

Forwarding HTTP Requests

I added some entries to the modem so incoming HTTP traffic on ports 80 and 443 would be forwarded to ports 9000 and 9443 on my home server, gojira.

Using the Script

The script saves the IP address to a file, and periodically compare the saved value to the current value. Then the script modifies the Route53 record for a specified subdomain whenever the value of my public IP address changes.

Here is the help information for the script :

Shell
$ dynamicDns
dynamicDns - Maintains a dynamic DNS record in AWS Route53
Saves data in '/home/mslinn/.dynamicDns'
Syntax: dynamicDns [OPTIONS] SUB_DOMAIN DOMAIN
OPTIONS: -v Verbose mode
Example usage: dynamicDns my_subdomain my_domain.com dynamicDns -v my_subdomain my_domain.com

Here is a sample usage:

Shell
$ dynamicDns my_subdomain my_domain.com
{
    "ChangeInfo": {
        "Id": "/change/C075751811HI18SH4L8L0",
        "Status": "PENDING",
        "SubmittedAt": "2022-01-30T21:10:09.261Z",
        "Comment": "UPSERT a record for my_subdomain.my_domain.com"
    }
} 

Invoking the Script from Crontab

A personal crontab can be modified by typing:

Shell
$ crontab -e

I pasted in the following 2 lines into crontab on my Ubuntu server, running at home. These lines invoke the script via crontab when it boots, and every 5 minutes after that.

Shell
@reboot /path/to/dynamicDns my_subdomain my_domain.com
*/5 * * * * /path/to/dynamicDns my_subdomain my_domain.com

That's it, crontab will run the script within the next 5 minutes.

Script Source Code

Here is the bash script:

#!/bin/bash

# Author: Mike Slinn mslinn@mslinn.com
# Written 2022-01-30

export SAVE_FILE_NAME="$HOME/.dynamicDns"

function help {
  echo "$( basename $0 ) - Maintains a dynamic DNS record in AWS Route53

Saves data in '$SAVE_FILE_NAME'

Syntax:
  $( basename $0) [OPTIONS] SUB_DOMAIN DOMAIN

OPTIONS:
  -v Verbose mode

Example usage:
  $( basename $0) my_subdomain mydomain.com
  $( basename $0) -v my_subdomain mydomain.com

"  
  exit 1
}

function upsert {
  export HOSTED_ZONES="$(
    aws route53 list-hosted-zones
  )"

  export HOSTED_ZONE_RECORD="$(
    jq -r ".HostedZones[] | select(.Name == \"$DOMAIN.\")" <<< "$HOSTED_ZONES"
  )"

  export HOSTED_ZONE_RECORD_ID="$(
    jq -r .Id <<< "$HOSTED_ZONE_RECORD"
  )"

  aws route53 change-resource-record-sets \
    --hosted-zone-id "$HOSTED_ZONE_RECORD_ID" \
    --change-batch "{
      \"Comment\": \"UPSERT a record for $SUBDOMAIN.$DOMAIN\",
      \"Changes\": [{
      \"Action\": \"UPSERT\",
        \"ResourceRecordSet\": {
          \"Name\": \"$SUBDOMAIN.$DOMAIN\",
          \"Type\": \"A\",
          \"TTL\": 300,
          \"ResourceRecords\": [{ \"Value\": \"$IP\"}]
        }
      }]
    }"

  echo "$IP" > "$SAVE_FILE_NAME"
}

if [ "$1" == -v ]; then
  export VERBOSE=true
  shift
fi

if [ -z "$2" ]; then help; fi

set -e 

export SUBDOMAIN="$1"
export DOMAIN="$2"
export IP="$( dig +short myip.opendns.com @resolver1.opendns.com )"

if [ ! -f "$SAVE_FILE_NAME" ]; then
  if [ "$VERBOSE" ]; then echo "Creating $SAVE_FILE_NAME"; fi
  upsert; 
elif [ $( cat "$SAVE_FILE_NAME" ) != "$IP" ]; then 
  if [ "$VERBOSE" ]; then 
    echo "Updating $SAVE_FILE_NAME"
    echo "'$IP' was not equal to '$( cat "$SAVE_FILE_NAME" )'"
  fi
  upsert; 
else
  if [ "$VERBOSE" ]; then echo "No change necessary for $SAVE_FILE_NAME"; fi
fi