Last active
November 27, 2025 12:15
-
-
Save cjuroz/bc55b1673f9063c6b22eceb7edcf6750 to your computer and use it in GitHub Desktop.
DDNS service running on AWS (r53, lambda, api gateway)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import requests | |
| import sys | |
| from pathlib import Path | |
| # --- Configuration --- | |
| # The API Gateway URL you copied from Step 3 | |
| API_GATEWAY_URL = "AWS_API_GATEWAY_URL" | |
| # The API Key you created and copied in Step 3 | |
| API_KEY = "AWS_API_GATEWAY_API_KEY" | |
| # --- End of Configuration --- | |
| def get_public_ip(): | |
| """Fetches the current public IP from an external service.""" | |
| try: | |
| response = requests.get("https://checkip.amazonaws.com", timeout=10) | |
| response.raise_for_status() | |
| return response.text.strip() | |
| except requests.RequestException as e: | |
| print(f"Error fetching public IP: {e}") | |
| return None | |
| def get_last_ip(domain_name): | |
| """Reads the last known IP for a specific domain from its local file.""" | |
| ip_file = Path.home() / f".ddns_current_ip_{domain_name}" | |
| try: | |
| if ip_file.exists(): | |
| return ip_file.read_text().strip() | |
| except IOError as e: | |
| print(f"Error reading IP file: {e}") | |
| return None | |
| def update_ddns(ip, domain_name): | |
| """Calls the API Gateway to update the DNS record for a specific domain.""" | |
| headers = { | |
| "x-api-key": API_KEY, | |
| "Content-Type": "application/json" | |
| } | |
| payload = { | |
| "domain_name": domain_name, | |
| "ip_address": ip # Sending the IP address in the payload | |
| } | |
| try: | |
| print(f"Attempting to update DDNS for {domain_name} with new IP: {ip}") | |
| response = requests.post(API_GATEWAY_URL, headers=headers, json=payload) | |
| if response.status_code == 200: | |
| print("Successfully updated IP via API.") | |
| # If the update was successful, save the new IP | |
| save_last_ip(ip, domain_name) | |
| else: | |
| print(f"Error updating IP. Status: {response.status_code}, Response: {response.text}") | |
| except requests.RequestException as e: | |
| print(f"Error calling API Gateway: {e}") | |
| def save_last_ip(ip, domain_name): | |
| """Saves the given IP to the local file for a specific domain.""" | |
| ip_file = Path.home() / f".ddns_current_ip_{domain_name}" | |
| try: | |
| ip_file.write_text(ip) | |
| print(f"Saved new IP {ip} to {ip_file}") | |
| except IOError as e: | |
| print(f"Error writing to IP file: {e}") | |
| if __name__ == "__main__": | |
| # --- Argument Check --- | |
| if len(sys.argv) != 2: | |
| print("Usage: python ddns_client.py <your.domain.com>") | |
| exit(1) | |
| domain_name = sys.argv[1] | |
| # --- End of Argument Check --- | |
| print(f"--- Starting DDNS Check for {domain_name} ---") | |
| current_ip = get_public_ip() | |
| if not current_ip: | |
| exit(1) | |
| last_ip = get_last_ip(domain_name) | |
| print(f"Current public IP: {current_ip}") | |
| print(f"Last known IP for {domain_name}: {last_ip}") | |
| if current_ip != last_ip: | |
| print("IP address has changed. Triggering DDNS update...") | |
| update_ddns(current_ip, domain_name) | |
| else: | |
| print("IP address has not changed. No update needed.") | |
| print("--- DDNS Check Finished ---") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import boto3 | |
| import json | |
| # Initialize the Route 53 client | |
| route53 = boto3.client('route53') | |
| # Fetch the Hosted Zone ID from environment variables | |
| # We still need this, as all domains are in the same zone. | |
| HOSTED_ZONE_ID = os.environ['HOSTED_ZONE_ID'] | |
| def lambda_handler(event, context): | |
| try: | |
| # The client now sends data in the request body. We need to parse it. | |
| body = json.loads(event['body']) | |
| # Extract the domain name and IP address from the parsed body | |
| domain_name = body['domain_name'] | |
| ip_address = body['ip_address'] | |
| print(f"Received request to update {domain_name} to {ip_address}") | |
| # Prepare the record set change using the provided data | |
| change_batch = { | |
| 'Comment': 'Automated DDNS update', | |
| 'Changes': [ | |
| { | |
| 'Action': 'UPSERT', | |
| 'ResourceRecordSet': { | |
| 'Name': domain_name, | |
| 'Type': 'A', | |
| 'TTL': 60, # A short TTL is good for DDNS | |
| 'ResourceRecords': [{'Value': ip_address}] | |
| } | |
| } | |
| ] | |
| } | |
| # Execute the change in Route 53 | |
| response = route53.change_resource_record_sets( | |
| HostedZoneId=HOSTED_ZONE_ID, | |
| ChangeBatch=change_batch | |
| ) | |
| print(f"Successfully updated DNS record. Change ID: {response['ChangeInfo']['Id']}") | |
| return { | |
| 'statusCode': 200, | |
| 'body': json.dumps(f'Successfully updated {domain_name} to {ip_address}') | |
| } | |
| except KeyError as e: | |
| # This error occurs if 'domain_name' or 'ip_address' is missing from the request | |
| error_message = f"Missing required key in request body: {e}" | |
| print(error_message) | |
| return { | |
| 'statusCode': 400, # Bad Request | |
| 'body': json.dumps(error_message) | |
| } | |
| except Exception as e: | |
| print(f"Error updating DNS record: {e}") | |
| return { | |
| 'statusCode': 500, | |
| 'body': json.dumps(f'Error updating DNS record: {str(e)}') | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment