===== Route 53 Dynamic DNS Lambda Function ===== This Lambda function will get the public IP of an EC2 instance and update a corresponding DNS record in Route 53. It can be used for systems that need to be publicly available but might not always need to be running such as a bastion host. {{tag>AWS Lambda Linux Route53 Python}} ==== Requirements ==== The following items are needed for this Lambda function to ... function correctly. * Route 53 Hosted Zone for the desired domain name * The EC2 instance needs to have the following tags * ''Name'' used for informational purposes in Lambda logs * ''DNSName'' with the FQDN that will be updated for the instance * ''HostedZoneId'' for the domain name's Route53 Hosted Zone * Lambda role * CloudWatch Event to trigger Lambda function ==== Deployment Rundown ==== - Create Route 53 Hosted Zone for desired domain name - Create IAM policy and role for Lambda function - Deploy Lambda function and assign IAM role - Verify that EC2 instance has the needed tags and values - Create CloudWatch Event - Stop and start EC2 instance and verify that Lambda function works as expected ==== Known Issues / Limitations ==== Below is a list of known issues and limitations with this implementation. * This works for me and has only been tested in my environment * Minimal error checking is being used * Installation, deployment, and configuration are done manually * Lambda function is dependent on using an AWS default VPC * Does not account for instances with multiple ENIs * Does not account for regions where lots of instances are started (auto scaling groups for example) ==== Diagram ==== {{ :images:svg:route_53_dynamic_dns_lambda_function.svg | Route 53 Dynamic DNS Lambda Function }} https://nerdydrunk.info/_media/images:svg:route_53_dynamic_dns_lambda_function.svg - Instance starts and triggers CloudWatch Event - CloudWatch Event launches Lambda function and passes instance ID as input - Lambda function used describe instances to gather tags and public IP from Instance information - Lambda function updates DNS record based on the instance tag values ==== Lambda Function Operation ==== * Checks to see if instance ID was provided in the input * Gathers required tag values from instance * Checks that all required tag values were gathered * Exits if any of the required tags are missing * In the provided Hosted Zone it updates the DNS record for the provided FQDN with the instance's public IP ==== Lambda Function Role Permissions ==== The following items in IAM policy need to be updated to reflect your environment and deployment. * Hosted zone ids ''Z1111111111111'' and ''Z2222222222222'' * Region ''us-east-1'' * AWS account ''123456789012'' * Lambda function name ''dynamic-dns'' { "Version": "2012-10-17", "Statement": [ { "Sid": "GetInstanceInformation", "Effect": "Allow", "Action": "ec2:DescribeInstances", "Resource": "*" }, { "Sid": "UpdateDNSReocrd", "Effect": "Allow", "Action": "route53:ChangeResourceRecordSets", "Resource": [ "arn:aws:route53:::hostedzone/Z1111111111111", "arn:aws:route53:::hostedzone/Z2222222222222" ] }, { "Effect": "Allow", "Action": "logs:CreateLogGroup", "Resource": "arn:aws:logs:us-east-1:123456789012:/aws/lambda/dynamic-dns" }, { "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": [ "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/dynamic-dns:*" ] } ] } ==== CloudWatch Event ==== A CloudWatch Event is used to run the Lambda function anytime any EC2 instance starts. The CloudWatch Event will need to pass "Matches events" as input to the Lambda function. This is used to get the instance ID. Event pattern: { "source": [ "aws.ec2" ], "detail-type": [ "EC2 Instance State-change Notification" ], "detail": { "state": [ "running" ] } } ==== Lambda Function Code ==== import boto3 def lambda_handler(event, context): if 'detail' in event: if 'instance-id' in event['detail']: instance_id = event['detail']['instance-id'] else: return else: return client_ec2 = boto3.client('ec2') instance_details = client_ec2.describe_instances(InstanceIds=[instance_id]) for tag in instance_details['Reservations'][0]['Instances'][0]['Tags']: if tag['Key'] == 'Name': tag_name = tag['Value'] if tag['Key'] == 'DNSName': tag_dnsname = tag['Value'] if tag['Key'] == 'HostedZoneId': tag_hostedzoneid = tag['Value'] try: print('Found tags: {}, {}, {}'.format(tag_name, tag_dnsname, tag_hostedzoneid)) except: print('Required tags missing.') return client_r53 = boto3.client('route53') client_r53.change_resource_record_sets( HostedZoneId=tag_hostedzoneid, ChangeBatch={ 'Changes': [ { 'Action': 'UPSERT', 'ResourceRecordSet': { 'Name': tag_dnsname, 'Type': 'A', 'TTL': 360, 'ResourceRecords': [ { 'Value': instance_details['Reservations'][0]['Instances'][0]['PublicIpAddress'] } ] } } ] } ) print('Updated {} for {} to {}'.format(tag_dnsname,instance_id,instance_details['Reservations'][0]['Instances'][0]['PublicIpAddress'])) return