Simple Dynamic DNS With Route53 and AWS Lambda

Using an AWS function to update a DNS record in Route53
2 min read

I’ve been making some enhancements to my home server recently. For the last installment of this series, see my last post about the move to Route53, using LetsEncrypt.

In this post, I’ll show how I set up a simple dynamic DNS updater service using AWS Lambda. This allows us to update a DNS record in Route53 with the latest IP address with any value we want.

Dynamic DNS With Route53

This is the source code of my lambda. All of the configuration in the environment variables. It accepts HTTP get requests, and updates the DNS record for the given domain with the given IP. It also checks that the domain is in the list of allowed domains/subdomains, and requires a secret API key to prevent abuse.

import { Route53Client, ChangeResourceRecordSetsCommand } from "@aws-sdk/client-route-53";

const HOSTED_ZONE_ID = process.env.HOSTED_ZONE_ID
const ALLOWED_RECORDS = JSON.parse(process.env.ALLOWED_RECORDS)

const client = new Route53Client({ region: 'us-east-1' });

export const handler = async (event) => {
  // only allow known API KEY
  if (event.queryStringParameters?.api_key !== process.env.API_SECRET) {
    return {
      statusCode: 403,
      body: "Forbidden"
    }
  }

  // get args from query
  const dns_record_name = event.queryStringParameters?.domain
  const new_ip = event.queryStringParameters?.ip

  // check that this record is allowed to be edited
  if (!ALLOWED_RECORDS.includes(dns_record_name)) {
    return {
      statusCode: 403,
      body: "Update of " + dns_record_name + " Forbidden"
    }
  }

  // issue change request
  const command = new ChangeResourceRecordSetsCommand({
    HostedZoneId: HOSTED_ZONE_ID,
    ChangeBatch: {
      Changes : [{
        Action: 'UPSERT',
        ResourceRecordSet: {
          Name: dns_record_name,
          Type: 'A',
          ResourceRecords: [
            {
              Value: new_ip
            }
          ],
          TTL: 30
        }
      }]
    }
  });

  try {
    const aws_response = await client.send(command);

    // send back AWS response
    return {
      statusCode: 200,
      body: JSON.stringify(aws_response),
    };
  } catch (err) {
    console.error('Error setting DNS', err)
    return {
      statusCode: 500,
      body: 'Internal Server Error'
    }
  }
};

Environment Variables

  • HOSTED_ZONE_ID - The ID of the hosted zone in Route53, find this in the AWS console
  • ALLOWED_RECORDS - A JSON array of the DNS records you want to allow to be updated.
    • Example Value: ["home.example.com."]
  • API_SECRET - A secret key to prevent unauthorized updates, randomly generate this using openssl or your password manager

Lambda URL

I updated the lambda to be called publicly via the web with no authentication, since it handles authentication via a custom API key implementation.

url configuration

Lambda Execution Role

I then attached this policy to the lambda’s execution role to allow it to modify my hosted zone. Edit to include your hosted zone’s ARN:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowModifyingRecordsInHostedZone",
            "Effect": "Allow",
            "Action": "route53:ChangeResourceRecordSets",

            // TODO: EDIT THIS TO YOUR HOSTED ZONE'S ARN:
            "Resource": "arn:aws:route53:::hostedzone/YOURHOSTEDZONEIDHERE"
        }
    ]
}

Executing the Lambda

I can now call this lambda from my home server with a simple curl command. I have a cron job that runs on my router every 5 minutes to update the DNS record with the current IP address.


https://YOUR_FUNCTION.lambda-url.us-east-1.on.aws?api_key=YOUR_KEY&domain=myhome.example.com.&ip=INSERT_IP_HERE

Mine pulls the public ip address from the wan interface of my router. You can also use a service like https://www.ipify.org/ to get your public IP address programmatically.

Since this is a simple HTTP GET request, you could call this from a multitude of other services, scripts, or tools to update your DNS record.

Subscribe to my Newsletter

Like this post? Subscribe to get notified for future posts like this.

Change Log

  • 7/22/2024 - Initial Revision

Found a typo or technical problem? file an issue!