AWS | 8 Min Read

How to Update a DNS Record on a Route 53 Hosted Zone Using a Lambda Function & AWS SDK

Learn how to automate Route 53 Hosted Zone DNS record updates with Lambda Functions and the AWS SDK. Save time and money in a few quick steps!

When working with EC2 or ECS instances on AWS, you might want to point a domain name at your service to make it easier for people to connect to it without needing to remember the IP address for it. This can easily be achieved by creating an A record on a domain that has the value of the IP address of the service you want it to connect to.

However, where you’ll run into issues is when your service restarts. This is because it’ll get a new IP address assigned to it that will likely be different from the previous one configured on the A record so the domain will no longer connect to your service.

One solution to this is using Elastic IP addresses from AWS to get a static IP that won’t change between restarts meaning the domain will always point to your service. But, the issue with Elastic IP addresses is that over time the cost can add up, especially if you need a few of them so you might want to avoid this solution if you’re aiming to keep costs down.

A second solution would be to update the A record for your domain each time a new IP address is generated to make sure the domain stays in sync with the service but this is both time-consuming and quite tedious for the person maintaining it. But, what about if we could automate this?

So, in this post, we’re going to look at doing just that and seeing how we can automatically update an A record on a Route 53 Hosted Zone using a Lambda function and the AWS SDK.

Let’s get into it!

Prerequisites

Before starting this tutorial, you’ll need to have an existing AWS CDK stack to add this code. Alternatively, you can create a new one for this tutorial by running the command `cdk init app --language typescript`.

Once you have your stack, you’ll need to make sure it’s configured to deploy to the region `us-east-1`, this is because we’ll be configuring Route 53 services which are global and are controlled from the `us-east-1` region. You can configure your stack to deploy to the `us-east-1` region by amending your stack definition file in your `bin` directory to look like the one below.

TypeScript logo./bin/*-stack.ts
1const app = new cdk.App();
2new Route53UpdateDNSRecordStack(app, "Route53UpdateDNSRecordStack", {
3 env: {
4 region: "us-east-1",
5 },
6});
ts

NOTE: While it is possible to deploy your Route 53 infrastructure to `us-east-1` and your other infrastructure such as Lambda functions to another region, this is out of the scope of this tutorial. So, for simplicity, we’re deploying everything to `us-east-1`.

Creating our Hosted Zone and A Record

Once your stack is configured to deploy to `us-east-1`, let’s jump into actually configuring our services for this tutorial. The first thing we’re going to define is our Route 53 Hosted Zone. For the sake of this tutorial, I’m going to use an example domain but you can use a domain you own if you would like.

To define your Route 53 Hosted Zone, go into your services definition file in your `lib` directory and add the below code to the constructor inside it.

TypeScript logo./lib/*-stack.ts
1const domain = "yourdomain.com";
2
3// Define the Route53 hosted zone for our domain
4const domainHostedZone = new HostedZone(this, "DomainHostedZone", {
5 zoneName: domain,
6});
ts

After we’ve added this code to create our hosted zone, let’s add the code to create the base A record on it so we can update it later on in our lambda function.

TypeScript logo./lib/*-stack.ts
1// ...hosted zone code
2
3// Define a new A record for our domain to be updated by our lambda function
4const aRecord = new ARecord(this, "ARecord", {
5 target: {
6 values: ["192.168.1.1"],
7 },
8 recordName: domain,
9 zone: domainHostedZone,
10});
11
12// Add a dependency on the A Record to ensure that is removed first when the stack is deleted.
13aRecord.node.addDependency(domainHostedZone);
ts

Here we define a new A record with an example IP Address of `192.168.1.1` and add a dependency on the A record to the hosted zone so when we destroy the stack, we make sure the A record is removed first before the hosted zone.

Defining our Lambda function

With our hosted zone and A record configured, let’s now configure the lambda function that will handle the updating of our A record when triggered. To do this, we first need to configure a custom IAM Policy Statement that will allow our lambda function to execute the necessary SDK commands required. To create this policy statement add the below code under the A record code we just added.

TypeScript logo./lib/*-stack.ts
1// ...A record code
2
3// Define our IAM permissions for our lambda function
4const route53Permission = new PolicyStatement({
5 actions: [
6 "route53:ListResourceRecordSetsCommand",
7 "route53:ChangeResourceRecordSets",
8 "route53:ListResourceRecordSets",
9 ],
10 resources: [`arn:aws:route53:::hostedzone/${domainHostedZone.hostedZoneId}`],
11});
ts

This IAM policy statement allows us to list records and update them on the hosted zone we defined earlier. After this, we need to define our lambda function and add this new policy to it which we can do with the code below.

TypeScript logo./lib/*-stack.ts
1// ...IAM policy code
2
3// Define our lambda to update the IP of our A Record
4const updateRecordLambda = new NodejsFunction(this, "UpdateRecordLambda", {
5 memorySize: 1024,
6 runtime: Runtime.NODEJS_16_X,
7 handler: "handler",
8 entry: `./resources/update-record.ts`,
9 environment: {
10 zoneId: domainHostedZone.hostedZoneId,
11 zoneName: domainHostedZone.zoneName,
12 },
13});
14
15// Add our route53 permissions to our lambda function
16updateRecordLambda.addToRolePolicy(route53Permission);
ts

The important thing to note here is the environment variables we pass to the function, `zoneId` and `zoneNam` as we’ll need these to update the A record in the lambda function. With that code added, we’ve defined our lambda function so we just need to write it. To do that create a new file at `./resources/update-record.ts` and add the below code.

TypeScript logo./resources/update-record.ts
1import {
2 ChangeResourceRecordSetsCommand,
3 ListResourceRecordSetsCommand,
4 Route53Client,
5} from "@aws-sdk/client-route-53";
6
7const route53 = new Route53Client({});
8
9async function getRecords(zoneId: string) {
10 // Get all of the current records for the hosted zone
11 const { ResourceRecordSets } = await route53.send(
12 new ListResourceRecordSetsCommand({ HostedZoneId: zoneId })
13 );
14
15 if (!ResourceRecordSets) return undefined;
16
17 // Filter the records to only include A records
18 const aRecords = JSON.stringify(
19 ResourceRecordSets?.filter((record) => record.Type === "A")
20 );
21
22 console.log(aRecords);
23}
24
25export const handler = async () => {
26 const { zoneId = "", zoneName = "" } = process.env;
27
28 // Print out all A records for the hosted zone
29 await getRecords(zoneId);
30
31 const params = {
32 HostedZoneId: zoneId,
33 ChangeBatch: {
34 Changes: [
35 {
36 Action: "UPSERT",
37 ResourceRecordSet: {
38 Name: zoneName,
39 ResourceRecords: [
40 {
41 Value: "192.168.1.2",
42 },
43 ],
44 TTL: 1800,
45 Type: "A",
46 },
47 },
48 ],
49 },
50 };
51
52 // Update the A record for the hosted zone
53 await route53.send(new ChangeResourceRecordSetsCommand(params));
54
55 // Print out all A records for the hosted zone
56 await getRecords(zoneId);
57};
ts

In this function, we first get the `zoneId` and `zoneName` from the environment variables passed to the function. We then get all of the current A records on the hosted zone and print them out to the console so we can see the starting values, we then send the `ChangeResourceRecordSetsCommand` with the new IP address to update the A record on our hosted zone. Before, finally printing out the updated A records to confirm the changes were successful.

With this lambda function’s code written, we’ve fully configured our CDK stack and all we need to do is deploy and test it so let’s do that now!

Deployment and Testing

To deploy our CDK stack to your AWS account, run the command `cdk deploy` in your terminal and accept any prompts presented. Once your stack has finished deploying, log into your AWS dashboard and check the IP address of the A Record on your Route 53 Hosted Zone by going to the Route 53 dashboard.

Once you’ve confirmed the A record has an IP address of `192.168.1.1`, invoke your lambda function via the AWS CLI using the command `aws lambda invoke --function-name YOUR_LAMBDA_NAME --invocation-type Event - --region us-east-1`, make sure to replace `YOUR_LAMBDA_NAME` with the name of your deployed lambda function which you can get from your AWS dashboard.

After invoking your lambda function, head back to your Route 53 dashboard and check the A record again, its IP address should now be `192.168.1.2`, if so your lambda function and stack worked as expected. Another way you can check everything worked is by going to CloudWatch and inspecting the logs for the lambda function we invoked, it should show the IP address previous to the update and after with their respective values.

Tidying Up

Once you’ve finished testing the stack and no longer require it, you can destroy it by running `cdk destroy` in your terminal.

If you get an error during the destroy process stating “The specified hosted zone contains non-required resource record sets and so cannot be deleted.” This will be related to the A record that was changed by the SDK and doesn’t match the value that was deployed by the CDK. To resolve this, manually go to your Route 53 hosted zone and remove the A record, then run `cdk destroy` again and everything should be removed successfully.

Closing Thoughts

During this post, we’ve looked at how we can update a DNS record on a Route 53 Hosted Zone by using a Lambda function and the AWS SDK. If you’d like to see the full example code for this project as well as all my other CDK tutorials, you can view the complete GitHub repository here.

I hope you found this post helpful and until next time.

Thank you for reading.

Coner



Contact

Join My Newsletter

Subscribe to my weekly newsletter by filling in the form.

Get my latest content every week and 0 spam!