Table of Contents

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.

Requirements

The following items are needed for this Lambda function to … function correctly.

Deployment Rundown

  1. Create Route 53 Hosted Zone for desired domain name
  2. Create IAM policy and role for Lambda function
  3. Deploy Lambda function and assign IAM role
  4. Verify that EC2 instance has the needed tags and values
  5. Create CloudWatch Event
  6. 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.

Diagram


https://nerdydrunk.info/_media/images:svg:route_53_dynamic_dns_lambda_function.svg

  1. Instance starts and triggers CloudWatch Event
  2. CloudWatch Event launches Lambda function and passes instance ID as input
  3. Lambda function used describe instances to gather tags and public IP from Instance information
  4. Lambda function updates DNS record based on the instance tag values

Lambda Function Operation

Lambda Function Role Permissions

The following items in IAM policy need to be updated to reflect your environment and deployment.

{
    "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

r53dyndns.py
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