I have a desktop computer that I occasionally need to access externally from outside my home. The problem is that the IP address I get assigned by my ISP can change at any time, and when that happens I am unable to access the machine until I can find out what the new IP is. Dynamic DNS works by running an application on your computer which automatically updates a DNS record whenever a change in IP address is detected. There are many DDNS providers such as No-IP that offer this service, but we can get the same functionality for free using AWS Route53.

The plan

The idea is to create a Terraform configuration which gets the machine’s current public IP address and updates a DNS A record in Route53 with that address which can be accessed from anywhere in the world. We can then apply this configuration periodically to ensure that the IP address in Route53 is always up-to-date.

Prerequisites:

Setting up Route53

You will need to have your own domain name configured as a hosted zone inside Route53. I use AWS to host my personal website so already have a hosted zone set up.

Writing the Terraform configuration

Create an empty terraform.tf file. This will contain all of the resources we need. As the configuration is so small we won’t bother using a full Terraform project file/directory structure.

Specify the AWS provider along with a region. As we’re only using Route53 the region won’t matter because Route53 is a global AWS service, but a region is still required by Terraform/AWS CLI.

provider "aws" {
  region = "eu-west-1"
}

We’ll set two locals (Terraform local variables), one for the domain name hosted in Route53 and one for the name of the DNS record that will sit under it.

locals {
  domain_name = "mydomain.com"
  record_name = "pc"
}

To save us needing to manually get the hosted zone ID in AWS, we’ll find it by searching for the hosted zone resource by the domain name.

data "aws_route53_zone" "hosted_zone" {
  name = "${local.domain_name}"
}

The easiest way to obtain the current public IP address of the machine is to use a HTTP Data Source to make a request to icanhazip which will return out public IP address.

data "http" "ip" {
  url = "https://ipv4.icanhazip.com"
}

Finally we’ll create the actual Route53 DNS record and populate it with the IP from the HTTP Data Source.

resource "aws_route53_record" "dns_record" {
  # Use the ID of the Hosted Zone we retrieved earlier
  zone_id = "${data.aws_route53_zone.hosted_zone.zone_id}"

  # Set the name of the record, e.g. pc.mydomain.com
  name = "${local.record_name}.${local.domain_name}"

  # We're pointing to an IP address so we need to use an A record
  type = "A"

  # We'll set the TTL of the record to 30 minutes (1800 seconds)
  ttl = "1800"

  # Set the content of the record to the IP address obtained from icanhazip.com
  # The chomp function strips out any newlines from the data
  records = ["${chomp(data.http.ip.body)}"]
}

Applying the configuration

Next we’ll apply the Terraform configuration and confirm that the record has been successfully written.

$ terraform apply
data.http.ip: Refreshing state...
data.aws_route53_zone.hosted_zone: Refreshing state...

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_route53_record.dns_record
      id:                 <computed>
      allow_overwrite:    "true"
      fqdn:               <computed>
      name:               "pc.mydomain.com"
      records.#:          "1"
      records.4100942472: "81.47.22.127"
      ttl:                "1800"
      type:               "A"
      zone_id:            "KB1B480B1H4N"


Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_route53_record.dns_record: Creating...
  allow_overwrite:    "" => "true"
  fqdn:               "" => "<computed>"
  name:               "" => "pc.mydomain.com"
  records.#:          "" => "1"
  records.4100942472: "" => "81.47.22.127"
  ttl:                "" => "1800"
  type:               "" => "A"
  zone_id:            "" => "KB1B480B1H4N"
aws_route53_record.dns_record: Still creating... (10s elapsed)
aws_route53_record.dns_record: Creation complete after 14s (ID: KB1B480B1H4N_pc.mydomain.com_A)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Updating periodically

So we’ve now got an idempotent command we can call when we want to update the DNS record with our current public IP address. Next we need to make sure it is run at an interval. We can use cron to do that for us.

First we need to write a shell script that cron will invoke, we’ll call it update-ip-dns-record.sh. Terraform includes an -auto-approve flag which will avoid asking for user input before applying the configuration.

#!/bin/sh

cd /absolute/path/to/configuration/directory

/usr/local/bin/terraform apply -auto-approve

Make the script executable.

chmod +x update-ip-dns-record.sh

Open the crontab (our list of scheduled cron jobs).

crontab -e

Add the following line which will ensure cron executes our script every six hours. My public IP doesn’t change that often so six hours should be often enough, but the job could run as often as you like.

0 */6 * * * /absolute/path/to/update-ip-dns-record.sh

For testing purposes we could instead execute the job every minute and redirect stdout and stderr to a log file for us to inspect.

* * * * * /absolute/path/to/update-ip-dns-record.sh >> /tmp/ddns-debug.log 2>&1

Summary

There we have it, an automated way of keeping track of a dynamic IP. You should now be able to hit your new domain record (e.g. pc.mydomain.com) and have it resolve to your current public IP address!