<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rev</title>
    <description>The latest articles on DEV Community by Rev (@revsystem).</description>
    <link>https://dev.to/revsystem</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F325441%2F285a88df-c8fc-4169-bc0a-78e0f3e90fc8.png</url>
      <title>DEV Community: Rev</title>
      <link>https://dev.to/revsystem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/revsystem"/>
    <language>en</language>
    <item>
      <title>Running an AWS Lambda + Route 53 DDNS Client on EdgeRouter X</title>
      <dc:creator>Rev</dc:creator>
      <pubDate>Fri, 10 Apr 2026 18:07:44 +0000</pubDate>
      <link>https://dev.to/aws-builders/running-an-aws-lambda-route-53-ddns-client-on-edgerouter-x-24f0</link>
      <guid>https://dev.to/aws-builders/running-an-aws-lambda-route-53-ddns-client-on-edgerouter-x-24f0</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;My home network is assigned a dynamic global IP address by the ISP, so DDNS is essential for inbound connections. I built a system that periodically updates DNS records from an &lt;a href="https://store.ui.com/us/en/products/er-x-sfp" rel="noopener noreferrer"&gt;EdgeRouter X&lt;/a&gt; using the serverless DDNS solution (Lambda + Route 53 + DynamoDB) published by AWS.&lt;/p&gt;

&lt;p&gt;This article covers the steps from setting up the AWS environment to configuring the client on the EdgeRouter X and verifying operation.&lt;/p&gt;

&lt;p&gt;The ultimate goal is to connect to the EdgeRouter X via remote VPN using DDNS, but first we need to build the DDNS infrastructure.&lt;/p&gt;

&lt;p&gt;For details on how the solution works, see &lt;a href="https://aws.amazon.com/startups/learn/building-a-serverless-dynamic-dns-system-with-aws" rel="noopener noreferrer"&gt;Building a Serverless Dynamic DNS System with AWS&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/startups/learn/building-a-serverless-dynamic-dns-system-with-aws" rel="noopener noreferrer"&gt;https://aws.amazon.com/startups/learn/building-a-serverless-dynamic-dns-system-with-aws&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/awslabs/route53-dynamic-dns-with-lambda" rel="noopener noreferrer"&gt;https://github.com/awslabs/route53-dynamic-dns-with-lambda&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://yabe.jp/gadgets/edgerouter-x-05-ddns/" rel="noopener noreferrer"&gt;https://yabe.jp/gadgets/edgerouter-x-05-ddns/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test Environment
&lt;/h2&gt;

&lt;p&gt;Tested on EdgeRouter X (ER-X) firmware &lt;a href="https://community.ui.com/releases/EdgeRouter-3-0-1/7fe6b39d-baea-4ce6-87a0-5dcdc9538c3a" rel="noopener noreferrer"&gt;3.0.1&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the Forked Repository
&lt;/h2&gt;

&lt;p&gt;I forked the official AWS repository &lt;a href="https://github.com/awslabs/route53-dynamic-dns-with-lambda" rel="noopener noreferrer"&gt;awslabs/route53-dynamic-dns-with-lambda&lt;/a&gt; and created &lt;a href="https://github.com/revsystem/route53-dynamic-dns-with-lambda" rel="noopener noreferrer"&gt;revsystem/route53-dynamic-dns-with-lambda&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The fork includes the following changes, organized by branch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;fix/lambda-security&lt;/strong&gt;: Fixed security vulnerabilities in the Lambda function (timing attack, input validation, exception handling, information leakage). Improved code quality (resolved function name conflicts, unified return types, moved boto3 to module level, removed Python 2 compatibility code).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fix/cdk-improvements&lt;/strong&gt;: Tightened IAM policy resource restrictions (Route 53, CloudWatch Logs). Changed DynamoDB RemovalPolicy to RETAIN and billing mode to PAY_PER_REQUEST. Set Lambda log retention period and reserved concurrency. Removed unused imports and improved CDK-Nag suppressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fix/client-scripts&lt;/strong&gt;: Fixed variable quoting, operator errors, deprecated command substitutions, and JSON construction safety in dyndns.sh. Added JSON injection prevention, improved exception handling, and secret masking in newrecord.py.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;test/add-tests&lt;/strong&gt;: Completely rewrote the unimplemented tests (all assertions were commented out). Added 7 CDK stack tests and 15 Lambda unit tests (22 total). Covers GET/SET normal cases, validation, authentication, and error handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;docs/fix-typos-and-docs&lt;/strong&gt;: Fixed typos in README.md and invocation.md. Updated response examples in invocation.md to match the Lambda implementation. Added version range specification to requirements.txt. Updated cdk.json feature flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fix/cdk-nag-log-retention&lt;/strong&gt;: Added CDK-Nag suppressions for the custom resource Lambda auto-generated by the &lt;code&gt;log_retention&lt;/code&gt; setting. Since the CDK internal resource cannot be modified, the suppression includes an &lt;code&gt;applies_to&lt;/code&gt; clause with documented rationale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;feat/manage-record-script&lt;/strong&gt;: Created managerecord.py to allow viewing configuration, immediately changing TTL, and deleting records via subcommands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CDK-Nag is a tool that automatically checks CDK applications against security and compliance best practices. Error-level violations cause &lt;code&gt;cdk synth&lt;/code&gt; to fail, blocking deployment. Warning-level violations do not block deployment. To intentionally exempt a rule, you set a suppression with a documented reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the AWS Environment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A domain hosted in Route 53&lt;/li&gt;
&lt;li&gt;Permissions to operate Lambda, DynamoDB, and Route 53&lt;/li&gt;
&lt;li&gt;An environment with the AWS CLI available&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture Overview
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Operator]
  |
  |-- newrecord.py (create/update) --&amp;gt; DynamoDB (hostname -&amp;gt; JSON)
  |
[EdgeRouter X]
  |-- dyndns.sh --&amp;gt; Lambda URL --&amp;gt; Lambda --&amp;gt; DynamoDB + Route 53
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploying the CDK Stack
&lt;/h3&gt;

&lt;p&gt;This solution is deployed using AWS CDK. The Lambda function, DynamoDB table, and IAM roles are created at once.&lt;/p&gt;

&lt;p&gt;Clone the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/revsystem/route53-dynamic-dns-with-lambda.git

&lt;span class="nb"&gt;cd &lt;/span&gt;route53-dynamic-dns-with-lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the Python dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bootstrap is required the first time you deploy to a given account and region combination (environment). If the account is the same but the region differs, run it separately for each region.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdk bootstrap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are told the CDK CLI version is too old, update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; aws-cdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the deployment completes, the Lambda function URL is output. You will use this URL in the client configuration later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the DNS Record
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;newrecord.py&lt;/code&gt; to interactively configure the Route 53 hosted zone and record set. The configuration is saved to the DynamoDB table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 newrecord.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your AWS profile or region differs from the defaults, specify the environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="nv"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1 python3 newrecord.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter the hosted zone name (e.g., &lt;code&gt;example.jp&lt;/code&gt;), hostname (e.g., &lt;code&gt;router.example.jp&lt;/code&gt;), TTL (default 60), and shared secret in order. If the hosted zone does not exist, you will be asked whether to create it.&lt;/p&gt;

&lt;p&gt;The shared secret, together with the Lambda function URL, is the key to authentication. Set it to a sufficiently complex string. Using a random string generated by &lt;code&gt;openssl rand -hex 32&lt;/code&gt; is recommended.&lt;/p&gt;

&lt;p&gt;Once the input is complete, a confirmation is displayed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;##############################################&lt;/span&gt;
&lt;span class="c"&gt;#                                            #&lt;/span&gt;
&lt;span class="c"&gt;# The following configuration will be saved: #&lt;/span&gt;
&lt;span class="c"&gt;#                                            #&lt;/span&gt;
  Host name:  router.example.jp
  Hosted zone &lt;span class="nb"&gt;id&lt;/span&gt;: ZXXXXXXXXXXXXXXXXXXXXXXX
  Record &lt;span class="nb"&gt;set &lt;/span&gt;TTL: 60
  Secret: &lt;span class="k"&gt;********&lt;/span&gt;
&lt;span class="c"&gt;#                                            #&lt;/span&gt;
&lt;span class="c"&gt;#      do you want to continue? (y/n)        #&lt;/span&gt;
&lt;span class="c"&gt;#                                            #&lt;/span&gt;
&lt;span class="c"&gt;##############################################&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the setup is complete, the parameters needed to run &lt;code&gt;dyndns.sh&lt;/code&gt; are displayed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./dyndns.sh &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; https://XXXXXXXXXX.lambda-url.REGION.on.aws/ &lt;span class="nt"&gt;-h&lt;/span&gt; router.example.jp &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;YOUR_SECRET&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take note of the Lambda function URL and shared secret. They will be used in the EdgeRouter X configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verification
&lt;/h3&gt;

&lt;p&gt;Test-run &lt;code&gt;dyndns.sh&lt;/code&gt; to verify the integration between the Lambda function and Route 53. On environments that use &lt;code&gt;shasum&lt;/code&gt;, you may need to install &lt;code&gt;perl-Digest-SHA&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;perl-Digest-SHA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the IP address retrieval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./dyndns.sh &lt;span class="nt"&gt;-m&lt;/span&gt; get &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"https://XXXXXXXXXX.lambda-url.REGION.on.aws/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a public IP address is returned, the Lambda function is working correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  DDNS Client Implementation Approach
&lt;/h2&gt;

&lt;p&gt;EdgeRouter X has a built-in DDNS feature that can be configured with &lt;code&gt;set service dns dynamic&lt;/code&gt;. Internally it runs ddclient and supports standard protocols such as dyndns2 and cloudflare.&lt;/p&gt;

&lt;p&gt;However, the AWS DDNS solution uses a protocol that sends an HTTP POST with JSON to a Lambda function URL and performs application-level authentication using a SHA-256 hash. The Lambda function URL itself is published with authentication type NONE (public access), and authentication is achieved by verifying the hash value included in the request within the Lambda function. Since ddclient does not have a definition for this protocol, the built-in DDNS feature cannot be used.&lt;/p&gt;

&lt;p&gt;Instead, we periodically execute the shell script &lt;code&gt;dyndns.sh&lt;/code&gt; provided in the repository using the EdgeRouter X task scheduler.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The following steps are executed on the EdgeRouter X CLI or via SSH.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Deploying dyndns.sh
&lt;/h3&gt;

&lt;p&gt;SSH into the EdgeRouter X and download the script. Files placed under &lt;code&gt;/config/scripts/&lt;/code&gt; are preserved after firmware upgrades.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /config/scripts/dyndns.sh https://raw.githubusercontent.com/revsystem/route53-dynamic-dns-with-lambda/master/dyndns.sh

&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /config/scripts/dyndns.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dyndns.sh&lt;/code&gt; internally uses &lt;code&gt;shasum -a 256&lt;/code&gt; to generate the authentication hash. Firmware 3.0.1 includes the &lt;code&gt;shasum&lt;/code&gt; command, so no script modification was needed. If &lt;code&gt;shasum&lt;/code&gt; is not found on older firmware, you can replace the relevant line with &lt;code&gt;openssl dgst -sha256&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying dyndns.sh
&lt;/h3&gt;

&lt;h4&gt;
  
  
  IP Address Retrieval Check
&lt;/h4&gt;

&lt;p&gt;First, use the &lt;code&gt;-m get&lt;/code&gt; mode to verify that the connection to the Lambda function URL and IP address retrieval succeed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/config/scripts/dyndns.sh &lt;span class="nt"&gt;-m&lt;/span&gt; get &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"https://XXXXXXXXXX.lambda-url.REGION.on.aws/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a global IP address is returned, the network connection and Lambda function are working correctly. If nothing is returned, check the Lambda function URL, DNS resolution, or firewall rules.&lt;/p&gt;

&lt;h4&gt;
  
  
  DNS Record Update Check
&lt;/h4&gt;

&lt;p&gt;Next, use the &lt;code&gt;-m set&lt;/code&gt; mode to actually update the Route 53 record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/config/scripts/dyndns.sh &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"https://XXXXXXXXXX.lambda-url.REGION.on.aws/"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="s2"&gt;"router.example.jp"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;YOUR_SECRET&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, a response like the following is returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"return_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"return_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"router.example.jp has been updated to XXX.XXX.XXX.XXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"201"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the IP address has not changed, the following response is returned. This is also normal behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"return_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"return_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your IP address matches the current Route53 DNS record."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"200"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating a Wrapper Script
&lt;/h3&gt;

&lt;p&gt;Since &lt;code&gt;dyndns.sh&lt;/code&gt; operates via command-line arguments, prepare a wrapper script for the task scheduler to call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; /config/scripts/run-ddns.sh
#!/bin/bash
/config/scripts/dyndns.sh &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -m set &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -u "https://XXXXXXXXXX.lambda-url.REGION.on.aws/" &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -h "router.example.jp" &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -s "&amp;lt;YOUR_SECRET&amp;gt;" &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  &amp;gt;&amp;gt; /var/log/aws-ddns.log 2&amp;gt;&amp;amp;1
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /config/scripts/run-ddns.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Registering with the Task Scheduler
&lt;/h3&gt;

&lt;p&gt;Configure the task scheduler in EdgeOS CLI configuration mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;configure
&lt;span class="nb"&gt;set &lt;/span&gt;system task-scheduler task aws-ddns interval 300m
&lt;span class="nb"&gt;set &lt;/span&gt;system task-scheduler task aws-ddns executable path /config/scripts/run-ddns.sh
commit
save
&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs &lt;code&gt;run-ddns.sh&lt;/code&gt; every 300 minutes (5 hours). Adjust the &lt;code&gt;interval&lt;/code&gt; value based on how frequently your ISP changes your IP address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the Configuration
&lt;/h3&gt;

&lt;p&gt;Check the task scheduler registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;configure
show system task-scheduler
&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verifying the DNS Response
&lt;/h3&gt;

&lt;p&gt;Verify that the record has been correctly reflected in Route 53. There is one caveat here: the EdgeRouter X CLI is Vyatta-based and interprets &lt;code&gt;?&lt;/code&gt; as a help invocation key. This behavior persists even in operational mode (the &lt;code&gt;$&lt;/code&gt; prompt). Therefore, you cannot paste a URL containing &lt;code&gt;?&lt;/code&gt; directly into the CLI.&lt;/p&gt;

&lt;p&gt;To work around this, create and execute a script file with &lt;code&gt;vi&lt;/code&gt;. Using &lt;code&gt;echo&lt;/code&gt; does not help because the CLI still interprets the &lt;code&gt;?&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vi /tmp/dnscheck.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write the following content and save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://dns.google/resolve?name=router.example.jp&amp;amp;type=A"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execute the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh /tmp/dnscheck.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is working, JSON like the following is returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TC"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"RD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"RA"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"router.example.jp."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"router.example.jp."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"TTL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XXX.XXX.XXX.XXX"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Response from 205.251.196.248."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the IP address in the &lt;code&gt;data&lt;/code&gt; field under &lt;code&gt;Answer&lt;/code&gt; matches the WAN interface IP address, the DDNS setup is working correctly. You can check the WAN IP address with &lt;code&gt;show interfaces&lt;/code&gt;. The &lt;code&gt;205.251.196.248&lt;/code&gt; in the &lt;code&gt;Comment&lt;/code&gt; is a Route 53 nameserver IP address, indicating that the record is being served from the AWS side.&lt;/p&gt;

&lt;p&gt;If you want to use &lt;code&gt;dig&lt;/code&gt; or &lt;code&gt;nslookup&lt;/code&gt;, you can install the &lt;code&gt;dnsutils&lt;/code&gt; package, but the EdgeRouter X has limited storage (256MB NAND). For simple DNS verification, the method above is sufficient, and it is best to avoid installing unnecessary packages.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ping&lt;/code&gt; can also be used as a verification method that does not involve &lt;code&gt;?&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ping router.example.jp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line shows &lt;code&gt;PING router.example.jp (XXX.XXX.XXX.XXX)&lt;/code&gt;, revealing the resolved IP address. When run from the LAN behind the router, the ping targets the router's own WAN IP address, so no reply is returned. Stop with Ctrl+C. Use this only to verify whether name resolution succeeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking the Logs
&lt;/h3&gt;

&lt;p&gt;The execution results of &lt;code&gt;run-ddns.sh&lt;/code&gt; are appended to &lt;code&gt;/var/log/aws-ddns.log&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /var/log/aws-ddns.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Task scheduler execution may not be recorded in the system log. Verify scheduler operation by checking the log output in &lt;code&gt;/var/log/aws-ddns.log&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing DNS Records
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;managerecord.py&lt;/code&gt; to view, modify, or delete configurations set by &lt;code&gt;newrecord.py&lt;/code&gt;. The DynamoDB table name is automatically resolved from CloudFormation, so you do not need to look it up manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reconfiguring / Changing the Shared Secret
&lt;/h3&gt;

&lt;p&gt;Re-running &lt;code&gt;newrecord.py&lt;/code&gt; overwrites the configuration for the same hostname. Use this method to change the TTL or shared secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  Viewing the Current Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 managerecord.py show router.example.jp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Displays the configuration stored in DynamoDB along with the current IP and TTL from Route 53.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hostname        : router.example.jp
Hosted zone ID  : ZXXXXXXXXXXXXXXXXXXXXXXX
TTL             : 60
Secret          : ********
Current IP      : XXX.XXX.XXX.XXX
Route 53 TTL    : 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Immediately Applying a TTL Change
&lt;/h3&gt;

&lt;p&gt;The Lambda function only checks for IP address changes and does not compare TTL values. Therefore, the Route 53 TTL is not updated unless the IP address changes. To apply a TTL change immediately, use the &lt;code&gt;update-ttl&lt;/code&gt; subcommand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 managerecord.py update-ttl router.example.jp 300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This updates both the DynamoDB TTL and the Route 53 record simultaneously. From the next IP address change onward, the updated TTL is automatically applied.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deleting a Record
&lt;/h3&gt;

&lt;p&gt;To delete only the DynamoDB record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 managerecord.py delete router.example.jp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To also delete the Route 53 DNS record, add &lt;code&gt;--also-route53&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 managerecord.py delete &lt;span class="nt"&gt;--also-route53&lt;/span&gt; router.example.jp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In both cases, a confirmation prompt is displayed before execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I built a DDNS client that updates Route 53 DNS records from an EdgeRouter X using AWS Lambda + Route 53 + DynamoDB. This allows DDNS operation entirely within the AWS ecosystem, without depending on external DDNS services.&lt;/p&gt;

&lt;p&gt;Since this solution retrieves the IP address and calls the Lambda function URL using curl in a shell script, it can be adapted to other network devices or servers that support curl and a scheduler such as cron.&lt;/p&gt;

&lt;p&gt;The AWS repository does not provide a way to modify or delete configurations, so I created &lt;code&gt;managerecord.py&lt;/code&gt; to fill that gap. It supports viewing configurations, immediately changing TTL, and deleting records via subcommands.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>tutorial</category>
      <category>route53</category>
    </item>
  </channel>
</rss>
