diff --git a/README.md b/README.md index b4ccf30..e0d894c 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,16 @@ In this step, you will use the AWS Command Line Interface (AWS CLI) to create th _ddns-policy.json_ -The policy includes **ec2:Describe permission**, required for the function to obtain the EC2 instance’s attributes, including the private IP address, public IP address, and DNS hostname. The policy also includes DynamoDB and Route 53 full access which the function uses to create the DynamoDB table and update the Route 53 DNS records. The policy also allows the function to create log groups and log events. +The policy includes **ec2:Describe permission** as well as **elasticloadbalancing:Describe permission**, required for the function to obtain the EC2 instance or LoadBalancer’s attributes, including the private IP address for EC2, public IP address, and DNS hostname. The policy also includes DynamoDB and Route 53 full access which the function uses to create the DynamoDB table and update the Route 53 DNS records. The policy also allows the function to create log groups and log events. ```JSON { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", - "Action": "ec2:Describe*", + "Action": [ + "ec2:Describe*", + "elasticloadbalancing:Describe*" + ], "Resource": "*" }, { "Effect": "Allow", @@ -124,15 +127,15 @@ The code performs the following: - Checks to see whether the “DDNS” table exists in DynamoDB and creates the table if it does not. This table is used to keep a record of instances that have been created along with their attributes. It’s necessary to persist the instance attributes in a table because once an EC2 instance is terminated, its attributes are no longer available to be queried via the EC2 API. Instead, they must be fetched from the table. -- Queries the event data to determine the instance's state. If the state is “running”, the function queries the EC2 API for the data it will need to update DNS. If the state is anything else, e.g. "stopped" or "terminated", it will retrieve the necessary information from the “DDNS” DynamoDB table. +- Queries the event data to determine the instance's state. If the state is “running”, the function queries the EC2 API for the data it will need to update DNS. If the state is anything else, e.g. "stopped" or "terminated", it will retrieve the necessary information from the “DDNS” DynamoDB table. For LoadBalancers event is somewhat different. It is single event that comes from "elasticloadbalancing.amazonaws.com", but detail of it provide "CreateLoadBalancer" or "DeleteLoadBalancer" specifics. - Verifies that “DNS resolution” and “DNS hostnames” are enabled for the VPC, as these are required in order to use Route 53 for private name resolution. The function then checks whether a reverse lookup zone for the instance already exists. If it does, it checks to see whether the reverse lookup zone is associated with the instance's VPC. If it isn't, it creates the association. This association is necessary in order for the VPC to use Route 53 zone for private name resolution. -- Checks the EC2 instance’s tags for the CNAME and ZONE tags. If the ZONE tag is found, the function creates A and PTR records in the specified zone. If the CNAME tag is found, the function creates a CNAME record in the specified zone. +- Checks the EC2 instance or LoadBalancer’s tags for the CNAME and ZONE tags. For EC2, if the ZONE tag is found, the function creates A and PTR records in the specified zone. If the CNAME tag is found, the function creates a CNAME record in the specified zone. - Verifies whether there's a DHCP option set assigned to the VPC. If there is, it uses the value of the domain name to create resource records in the appropriate Route 53 private hosted zone. The function also checks to see whether there's an association between the instance's VPC and the private hosted zone. If there isn't, it creates it. -- Deletes the required DNS resource records if the state of the EC2 instance changes to “shutting down” or “stopped”. +- Deletes the required DNS resource records if the state of the EC2 instance changes to “shutting down” or “stopped” or LoadBalancer sends "DeleteLoadBalancer" event. Use the AWS CLI to create the Lambda function: @@ -152,19 +155,22 @@ aws lambda create-function --function-name ddns_lambda --runtime python2.7 --rol ##### Step 3 – Create the CloudWatch Events Rule -In this step, you create the CloudWatch Events rule that triggers the Lambda function whenever CloudWatch detects a change to the state of an EC2 instance. You configure the rule to fire when any EC2 instance state changes to “running”, “shutting down”, or “stopped”. Use the **aws events put-rule** command to create the rule and set the Lambda function as the execution target: +In this step, you create the CloudWatch Events rules. One that triggers the Lambda function whenever CloudWatch detects a change to the state of an EC2 instance, and second for LoadBalancer. You configure the rule to fire when any EC2 instance or LoadBalancer state changes. Use the **aws events put-rule** command to create the rule and set the Lambda function as the execution target: ``` aws events put-rule --event-pattern "{\"source\":[\"aws.ec2\"],\"detail-type\":[\"EC2 Instance State-change Notification\"],\"detail\":{\"state\":[\"running\",\"shutting-down\",\"stopped\"]}}" --state ENABLED --name ec2_lambda_ddns_rule +aws events put-rule --event-pattern "{\"account\": [\"674511019039\"], \"detail\": {\"eventName\": [\"CreateLoadBalancer\", \"DeleteLoadBalancer\"], \"eventSource\": [\"elasticloadbalancing.amazonaws.com\"]}, \"detail-type\": [\"AWS API Call via CloudTrail\"]}" --state ENABLED --name lb_lambda_ddns_rule ``` -The output of the command returns the ARN to the newly created CloudWatch Events rule, named **ec2\_lambda\_ddns\_rule**. Save the ARN, as you will need it to associate the rule with the Lambda function and to set the appropriate Lambda permissions. +The output of the commands returns the ARNs to the newly created CloudWatch Events rule, named **ec2\_lambda\_ddns\_rule** and **lb\_lambda\_ddns\_rule**. Save the ARNs, as you will need it to associate the rule with the Lambda function and to set the appropriate Lambda permissions. -Next, set the target of the rule to the Lambda function. Note that the **--targets** input parameter requires that you include a unique identifier for the **Id** target. You also need to update the command to use the ARN of the Lambda function that you created previously. +Next, set the target of the rules to the Lambda function. Note that the **--targets** input parameter requires that you include a unique identifier for the **Id** target. You also need to update the command to use the ARN of the Lambda function that you created previously. ``` aws events put-targets --rule ec2_lambda_ddns_rule --targets Id=id123456789012,Arn= +aws events put-targets --rule lb_lambda_ddns_rule --targets Id=id123456789012,Arn= ``` -Next, you add the permissions required for the CloudWatch Events rule to execute the Lambda function. Note that you need to provide a unique value for the **--statement-id** input parameter. You also need to provide the ARN of the CloudWatch Events rule you created earlier. +Next, you add the permissions required for the CloudWatch Events rules to execute the Lambda function. Note that you need to provide a unique value for the **--statement-id** input parameter for each permission. You also need to provide respective ARNs of the CloudWatch Events rules you created earlier. ``` -aws lambda add-permission --function-name ddns_lambda --statement-id 45 --action lambda:InvokeFunction --principal events.amazonaws.com --source-arn +aws lambda add-permission --function-name ddns_lambda --statement-id 45 --action lambda:InvokeFunction --principal events.amazonaws.com --source-arn +aws lambda add-permission --function-name ddns_lambda --statement-id 46 --action lambda:InvokeFunction --principal events.amazonaws.com --source-arn ``` ##### Step 4 – Create the private hosted zone in Route 53 diff --git a/ddns-pol.json b/ddns-pol.json index ac1fbf5..fb3c534 100644 --- a/ddns-pol.json +++ b/ddns-pol.json @@ -2,7 +2,10 @@ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", - "Action": "ec2:Describe*", + "Action": [ + "ec2:Describe*", + "elasticloadbalancing:Describe*" + ], "Resource": "*" }, { "Effect": "Allow", @@ -27,4 +30,4 @@ "*" ] }] -} \ No newline at end of file +} diff --git a/ddns.template b/ddns.template index 3243092..2250c0c 100644 --- a/ddns.template +++ b/ddns.template @@ -34,7 +34,10 @@ "Statement": [ { "Effect": "Allow", - "Action": "ec2:Describe*", + "Action": [ + "ec2:Describe*", + "elasticloadbalancing:Describe*" + ], "Resource": "*" }, { @@ -90,7 +93,7 @@ "Timeout": "90" } }, - "DdnsRule": { + "DdnsIRule": { "Type": "AWS::Events::Rule", "Properties": { "Description": "trigger whenever CloudWatch detects a change to the state of an EC2 instance", @@ -124,6 +127,60 @@ ] } }, + + "DdnsLRule": { + "Type": "AWS::Events::Rule", + "Properties": { + "Description": "trigger whenever CloudWatch detects a change to the state of an LB instance", + "Name": "lb_lambda_ddns_rule", + "EventPattern": { + "account": [ + "AWS::AccountId" + ], + "detail-type": [ + "AWS API Call via CloudTrail" + ], + "detail": { + "eventName": [ + "CreateLoadBalancer", + "DeleteLoadBalancer" + ], + "eventSource": [ + "elasticloadbalancing.amazonaws.com" + ] + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "ddnslambda", + "Arn" + ] + }, + "Id": "TargetFunctionV1" + } + ] + } + }, + + "PermissionForEventsToInvokeLambda": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Ref": "ddnslambda" + }, + "Action": "lambda:InvokeFunction", + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "DdnsIRule", + "Arn" + ] + } + } + }, "PermissionForEventsToInvokeLambda": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -134,7 +191,7 @@ "Principal": "events.amazonaws.com", "SourceArn": { "Fn::GetAtt": [ - "DdnsRule", + "DdnsLRule", "Arn" ] } diff --git a/union.py b/union.py index 7a4622f..a82170b 100644 --- a/union.py +++ b/union.py @@ -4,19 +4,28 @@ import uuid import time import random +import sys from datetime import datetime print('Loading function ' + datetime.now().time().isoformat()) route53 = boto3.client('route53') ec2 = boto3.resource('ec2') compute = boto3.client('ec2') +elb = boto3.client('elb') +elbv2 = boto3.client('elbv2') dynamodb_client = boto3.client('dynamodb') dynamodb_resource = boto3.resource('dynamodb') def lambda_handler(event, context): """ Check to see whether a DynamoDB table already exists. If not, create it. This table is used to keep a record of - instances that have been created along with their attributes. This is necessary because when you terminate an instance + assets that have been created along with their attributes. This is necessary because when you terminate it its attributes are no longer available, so they have to be fetched from the table.""" + global table, asset_id, asset, event_state, region + asset_id = '' + event_state = '' + asset = {} + region = '' + tables = dynamodb_client.list_tables() if 'DDNS' in tables['TableNames']: print 'DynamoDB table already exists' @@ -24,75 +33,61 @@ def lambda_handler(event, context): create_table('DDNS') # Set variables - # Get the state from the Event stream - state = event['detail']['state'] - - # Get the instance id, region, and tag collection - instance_id = event['detail']['instance-id'] - region = event['region'] table = dynamodb_resource.Table('DDNS') - if state == 'running': - time.sleep(60) - instance = compute.describe_instances(InstanceIds=[instance_id]) - # Remove response metadata from the response - instance.pop('ResponseMetadata') - # Remove null values from the response. You cannot save a dict/JSON document in DynamoDB if it contains null - # values - instance = remove_empty_from_dict(instance) - instance_dump = json.dumps(instance,default=json_serial) - instance_attributes = json.loads(instance_dump) - table.put_item( - Item={ - 'InstanceId': instance_id, - 'InstanceAttributes': instance_attributes - } - ) + # Check actual event type + # And get the asset id, region, and tag collection + if event['source'] == 'aws.ec2': + set_instance_vars(event) + elif event['source'] == 'aws.elasticloadbalancing': + try: + set_lbv1_vars(event) + except: + set_lbv2_vars(event) else: - # Fetch item from DynamoDB - instance = table.get_item( - Key={ - 'InstanceId': instance_id - }, - AttributesToGet=[ - 'InstanceAttributes' - ] - ) - instance = instance['Item']['InstanceAttributes'] + print 'Unexpected event source %s' % event['source'] + return - try: - tags = instance['Reservations'][0]['Instances'][0]['Tags'] - except: - tags = [] - # Get instance attributes - private_ip = instance['Reservations'][0]['Instances'][0]['PrivateIpAddress'] - private_dns_name = instance['Reservations'][0]['Instances'][0]['PrivateDnsName'] - private_host_name = private_dns_name.split('.')[0] - try: - public_ip = instance['Reservations'][0]['Instances'][0]['PublicIpAddress'] - public_dns_name = instance['Reservations'][0]['Instances'][0]['PublicDnsName'] - public_host_name = public_dns_name.split('.')[0] - except BaseException as e: - print 'Instance has no public IP or host name', e + if asset['extras']['type'] == 'instance': + # Asset is instance, thus has private IP. Get instance attributes + private_ip = asset['extras']['private_ip'] + try: + public_ip = asset['extras']['public_ip'] + except BaseException as e: + print 'Instance has no public IP', e - # Get the subnet mask of the instance - subnet_id = instance['Reservations'][0]['Instances'][0]['SubnetId'] - subnet = ec2.Subnet(subnet_id) - cidr_block = subnet.cidr_block - subnet_mask = int(cidr_block.split('/')[-1]) + # Get the subnet mask of the instance + subnet = ec2.Subnet(asset['extras']['subnet_id']) + cidr_block = subnet.cidr_block + subnet_mask = int(cidr_block.split('/')[-1]) - reversed_ip_address = reverse_list(private_ip) - reversed_domain_prefix = get_reversed_domain_prefix(subnet_mask, private_ip) - reversed_domain_prefix = reverse_list(reversed_domain_prefix) + reversed_ip_address = reverse_list(private_ip) + reversed_domain_prefix = get_reversed_domain_prefix(subnet_mask, private_ip) + reversed_domain_prefix = reverse_list(reversed_domain_prefix) - # Set the reverse lookup zone - reversed_lookup_zone = reversed_domain_prefix + 'in-addr.arpa.' - print 'The reverse lookup zone for this instance is:', reversed_lookup_zone + # Set the reverse lookup zone for instances only + reversed_lookup_zone = reversed_domain_prefix + 'in-addr.arpa.' + print 'The reverse lookup zone for this instance is:', reversed_lookup_zone + else: + reversed_lookup_zone = '' # Get VPC id - vpc_id = instance['Reservations'][0]['Instances'][0]['VpcId'] + vpc_id = asset['extras']['vpc_id'] vpc = ec2.Vpc(vpc_id) - + # Get private and public DNS names + private_host_name = '' + public_host_name = '' + try: + private_dns_name = asset['extras']['private_dns_name'] + private_host_name = private_dns_name.split('.')[0] + except BaseException as e: + print 'Asset '+str(asset['extras']['type'])+' has no private DNS host name', e + try: + public_dns_name = asset['extras']['public_dns_name'] + public_host_name = public_dns_name.split('.')[0] + except BaseException as e: + print 'Asset '+str(asset['extras']['type'])+' has no public DNS host name', e + # Are DNS Hostnames and DNS Support enabled? if is_dns_hostnames_enabled(vpc): print 'DNS hostnames enabled for %s' % vpc_id @@ -104,11 +99,12 @@ def lambda_handler(event, context): print 'DNS support disabled for %s. You have to enabled DNS support to use Route 53 private hosted zones.' % vpc_id # Create the public and private hosted zone collections. These are collections of zones in Route 53. + region = asset['extras']['region'] hosted_zones = route53.list_hosted_zones() private_hosted_zones = filter(lambda x: x['Config']['PrivateZone'] is True, hosted_zones['HostedZones']) - private_hosted_zone_collection = map(lambda x: x['Name'], private_hosted_zones) + private_hosted_zones_collection = map(lambda x: {'Name': x['Name'], 'Id': str.split(str(x['Id']),'/')[2]}, private_hosted_zones) public_hosted_zones = filter(lambda x: x['Config']['PrivateZone'] is False, hosted_zones['HostedZones']) - public_hosted_zones_collection = map(lambda x: x['Name'], public_hosted_zones) + public_hosted_zones_collection = map(lambda x: {'Name': x['Name'], 'Id': str.split(str(x['Id']),'/')[2]}, public_hosted_zones) # Check to see whether a reverse lookup zone for the instance already exists. If it does, check to see whether # the reverse lookup zone is associated with the instance's VPC. If it isn't create the association. You don't # need to do this when you create the reverse lookup zone because the association is done automatically. @@ -126,58 +122,58 @@ def lambda_handler(event, context): print e else: print 'No matching reverse lookup zone' - # create private hosted zone for reverse lookups - if state == 'running': - create_reverse_lookup_zone(instance, reversed_domain_prefix, region) + # create private hosted zone for reverse lookups if it is needed + if event_state == 'create' and reversed_lookup_zone != '': + create_reverse_lookup_zone(vpc_id, reversed_domain_prefix, region) reverse_lookup_zone_id = get_zone_id(reversed_lookup_zone) # Wait a random amount of time. This is a poor-mans back-off if a lot of instances are launched all at once. time.sleep(random.random()) # Loop through the instance's tags, looking for the zone and cname tags. If either of these tags exist, check # to make sure that the name is valid. If it is and if there's a matching zone in DNS, create A and PTR records. - for tag in tags: + for tag in asset['tags']: if 'ZONE' in tag.get('Key',{}).lstrip().upper(): if is_valid_hostname(tag.get('Value')): - if tag.get('Value').lstrip().lower() in private_hosted_zone_collection: + private_zone_record = next(( zone for zone in private_hosted_zones_collection if zone['Name'].lstrip().lower() == tag.get('Value').lstrip().lower()), False) + public_zone_record = next(( zone for zone in public_hosted_zones_collection if zone['Name'].lstrip().lower() == tag.get('Value').lstrip().lower()), False) + if private_zone_record and private_host_name != '': print 'Private zone found:', tag.get('Value') - private_hosted_zone_name = tag.get('Value').lstrip().lower() - private_hosted_zone_id = get_zone_id(private_hosted_zone_name) - private_hosted_zone_properties = get_hosted_zone_properties(private_hosted_zone_id) - if state == 'running': + private_hosted_zone_properties = get_hosted_zone_properties(private_zone_record['Id']) + if event_state == 'create': if vpc_id in map(lambda x: x['VPCId'], private_hosted_zone_properties['VPCs']): - print 'Private hosted zone %s is associated with VPC %s' % (private_hosted_zone_id, vpc_id) + print 'Private hosted zone %s is associated with VPC %s' % (private_zone_record['Id'], vpc_id) else: - print 'Associating zone %s with VPC %s' % (private_hosted_zone_id, vpc_id) + print 'Associating zone %s with VPC %s' % (private_zone_record['Id'], vpc_id) try: - associate_zone(private_hosted_zone_id, region, vpc_id) + associate_zone(private_zone_record['Id'], region, vpc_id) except BaseException as e: print 'You cannot create an association with a VPC with an overlapping subdomain.\n', e - exit() + sys.exit() try: - create_resource_record(private_hosted_zone_id, private_host_name, private_hosted_zone_name, 'A', private_ip) + create_resource_record(private_zone_record['Id'], private_host_name, private_zone_record['Name'], 'A', private_ip) create_resource_record(reverse_lookup_zone_id, reversed_ip_address, 'in-addr.arpa', 'PTR', private_dns_name) except BaseException as e: print e else: try: - delete_resource_record(private_hosted_zone_id, private_host_name, private_hosted_zone_name, 'A', private_ip) + delete_resource_record(private_zone_record['Id'], private_host_name, private_zone_record['Name'], 'A', private_ip) delete_resource_record(reverse_lookup_zone_id, reversed_ip_address, 'in-addr.arpa', 'PTR', private_dns_name) except BaseException as e: print e # create PTR record - elif tag.get('Value').lstrip().lower() in public_hosted_zones_collection: + elif public_zone_record and public_host_name != '': print 'Public zone found', tag.get('Value') public_hosted_zone_name = tag.get('Value').lstrip().lower() public_hosted_zone_id = get_zone_id(public_hosted_zone_name) # create A record in public zone - if state =='running': + if event_state =='create': try: - create_resource_record(public_hosted_zone_id, public_host_name, public_hosted_zone_name, 'A', public_ip) + create_resource_record(public_zone_record['Id'], public_host_name, public_zone_record['Name'], 'A', public_ip) except BaseException as e: print e else: try: - delete_resource_record(public_hosted_zone_id, public_host_name, public_hosted_zone_name, 'A', public_ip) + delete_resource_record(public_zone_record['Id'], public_host_name, public_zone_record['Name'], 'A', public_ip) except BaseException as e: print e else: @@ -192,34 +188,35 @@ def lambda_handler(event, context): cname = tag.get('Value').lstrip().lower() cname_host_name = cname.split('.')[0] cname_domain_suffix = cname[cname.find('.')+1:] - cname_domain_suffix_id = get_zone_id(cname_domain_suffix) - for cname_private_hosted_zone in private_hosted_zone_collection: - cname_private_hosted_zone_id = get_zone_id(cname_private_hosted_zone) - if cname_domain_suffix_id == cname_private_hosted_zone_id: - if cname.endswith(cname_private_hosted_zone): - #create CNAME record in private zone - if state == 'running': - try: - create_resource_record(cname_private_hosted_zone_id, cname_host_name, cname_private_hosted_zone, 'CNAME', private_dns_name) - except BaseException as e: - print e - else: - try: - delete_resource_record(cname_private_hosted_zone_id, cname_host_name, cname_private_hosted_zone, 'CNAME', private_dns_name) - except BaseException as e: - print e + if cname_domain_suffix[-1] != '.': + cname_domain_suffix = cname_domain_suffix + '.' + cname_private_zone_record = next(( zone for zone in private_hosted_zones_collection if zone['Name'].lstrip().lower() == cname_domain_suffix), False) + cname_public_zone_record = next(( zone for zone in public_hosted_zones_collection if cname.endswith(zone['Name'])), False) + if cname_private_zone_record: + #create CNAME record in private zone + if event_state == 'create': + try: + create_resource_record(cname_private_zone_record['Id'], cname_host_name, cname_private_zone_record['Name'], 'CNAME', private_dns_name) + except BaseException as e: + print e + else: + try: + delete_resource_record(cname_private_zone_record['Id'], cname_host_name, cname_private_zone_record['Name'], 'CNAME', private_dns_name) + except BaseException as e: + print e +# Next 3 lines could be dropped in favour of cname_public_zone_record for cname_public_hosted_zone in public_hosted_zones_collection: - if cname.endswith(cname_public_hosted_zone): - cname_public_hosted_zone_id = get_zone_id(cname_public_hosted_zone) + if cname.endswith(cname_public_hosted_zone['Name']): + cname_public_hosted_zone_id = cname_public_hosted_zone['Id'] #create CNAME record in public zone - if state == 'running': + if event_state == 'create': try: - create_resource_record(cname_public_hosted_zone_id, cname_host_name, cname_public_hosted_zone, 'CNAME', public_dns_name) + create_resource_record(cname_public_hosted_zone_id, cname_host_name, cname_public_hosted_zone['Name'], 'CNAME', public_dns_name) except BaseException as e: print e else: try: - delete_resource_record(cname_public_hosted_zone_id, cname_host_name, cname_public_hosted_zone, 'CNAME', public_dns_name) + delete_resource_record(cname_public_hosted_zone_id, cname_host_name, cname_public_hosted_zone['Name'], 'CNAME', public_dns_name) except BaseException as e: print e # Is there a DHCP option set? @@ -229,54 +226,61 @@ def lambda_handler(event, context): dhcp_configurations = get_dhcp_configurations(dhcp_options_id) except BaseException as e: print 'No DHCP option set assigned to this VPC\n', e - exit() + sys.exit() # Look to see whether there's a DHCP option set assigned to the VPC. If there is, use the value of the domain name # to create resource records in the appropriate Route 53 private hosted zone. This will also check to see whether # there's an association between the instance's VPC and the private hosted zone. If there isn't, it will create it. for configuration in dhcp_configurations: - if configuration[0] in private_hosted_zone_collection: - private_hosted_zone_name = configuration[0] - print 'Private zone found %s' % private_hosted_zone_name + private_zone_record = next(( zone for zone in private_hosted_zones_collection if zone['Name'].lstrip().lower() == configuration[0].lstrip().lower()), False) + if private_zone_record: + print 'Private zone found %s' % private_zone_record['Name'] # TODO need a way to prevent overlapping subdomains - private_hosted_zone_id = get_zone_id(private_hosted_zone_name) - private_hosted_zone_properties = get_hosted_zone_properties(private_hosted_zone_id) + private_hosted_zone_properties = get_hosted_zone_properties(private_zone_record['Id']) # create A records and PTR records - if state == 'running': + if event_state == 'create': if vpc_id in map(lambda x: x['VPCId'], private_hosted_zone_properties['VPCs']): - print 'Private hosted zone %s is associated with VPC %s' % (private_hosted_zone_id, vpc_id) + print 'Private hosted zone %s is associated with VPC %s' % (private_zone_record['Id'], vpc_id) else: - print 'Associating zone %s with VPC %s' % (private_hosted_zone_id, vpc_id) + print 'Associating zone %s with VPC %s' % (private_zone_record['Id'], vpc_id) try: - associate_zone(private_hosted_zone_id, region,vpc_id) + associate_zone(private_zone_record['Id'], region,vpc_id) except BaseException as e: print 'You cannot create an association with a VPC with an overlapping subdomain.\n', e - exit() + sys.exit() try: - create_resource_record(private_hosted_zone_id, private_host_name, private_hosted_zone_name, 'A', private_ip) + create_resource_record(private_zone_record['Id'], private_host_name, private_zone_record['Name'], 'A', private_ip) create_resource_record(reverse_lookup_zone_id, reversed_ip_address, 'in-addr.arpa', 'PTR', private_dns_name) except BaseException as e: print e else: try: - delete_resource_record(private_hosted_zone_id, private_host_name, private_hosted_zone_name, 'A', private_ip) + delete_resource_record(private_zone_record['Id'], private_host_name, private_zone_record['Name'], 'A', private_ip) delete_resource_record(reverse_lookup_zone_id, reversed_ip_address, 'in-addr.arpa', 'PTR', private_dns_name) except BaseException as e: print e else: print 'No matching zone for %s' % configuration[0] - + + # Clean up DynamoDB after deleting records + if event_state != 'create': + table.delete_item( + Key={ + 'AssetId': asset_id + } + ) + def create_table(table_name): dynamodb_client.create_table( TableName=table_name, AttributeDefinitions=[ { - 'AttributeName': 'InstanceId', + 'AttributeName': 'AssetId', 'AttributeType': 'S' }, ], KeySchema=[ { - 'AttributeName': 'InstanceId', + 'AttributeName': 'AssetId', 'KeyType': 'HASH' }, ], @@ -288,6 +292,141 @@ def create_table(table_name): table = dynamodb_resource.Table(table_name) table.wait_until_exists() +def set_instance_vars(event): + global asset_id, asset, event_state + + asset_id = event['detail']['instance-id'] + + if event['detail']['state'] == 'running': + time.sleep(60) + event_state = 'create' + asset = compute.describe_instances(InstanceIds=[asset_id]) + # Remove response metadata from the response + asset.pop('ResponseMetadata') + try: + tags = asset['Reservations'][0]['Instances'][0]['Tags'] + except: + tags = [] + asset['tags'] = tags + asset['extras'] = {} + asset['extras']['type'] = 'instance' + asset['extras']['region'] = event['region'] + asset['extras']['private_ip'] = asset['Reservations'][0]['Instances'][0]['PrivateIpAddress'] + asset['extras']['private_dns_name'] = asset['Reservations'][0]['Instances'][0]['PrivateDnsName'] + try: + asset['extras']['public_ip'] = asset['Reservations'][0]['Instances'][0]['PublicIpAddress'] + asset['extras']['public_dns_name'] = asset['Reservations'][0]['Instances'][0]['PublicDnsName'] + except BaseException as e: + print 'Instance has no public IP or host name', e + asset['extras']['subnet_id'] = asset['Reservations'][0]['Instances'][0]['SubnetId'] + asset['extras']['vpc_id'] = asset['Reservations'][0]['Instances'][0]['VpcId'] + db_put_asset(asset_id, asset, table) + else: + event_state = 'destroy' + # Fetch item from DynamoDB + asset = db_fetch_asset(asset_id, table) + +def set_lbv1_vars(event): + global asset_id, asset, event_state + + asset_id = event['detail']['requestParameters']['loadBalancerName'] + + if event['detail']['eventName'] == 'CreateLoadBalancer': + time.sleep(60) + event_state = 'create' + asset = elb.describe_load_balancers(LoadBalancerNames=[asset_id]) + # Remove response metadata from the response + asset.pop('ResponseMetadata') + try: + tags = elb.describe_tags(LoadBalancerNames=[asset_id])['TagDescriptions'][0]['Tags'] + except: + tags = [] + asset['tags'] = tags + asset['extras'] = {} + asset['extras']['type'] = 'elb' + asset['extras']['version'] = 'v1' + # Perhaps 'region' could/should be derived from availability zone + asset['extras']['region'] = event['detail']['awsRegion'] + asset['extras']['lb_scheme'] = asset['LoadBalancerDescriptions'][0]['Scheme'] + if asset['extras']['lb_scheme'] == 'internal': + asset['extras']['private_dns_name'] = asset['LoadBalancerDescriptions'][0]['DNSName'] + else: + asset['extras']['public_dns_name'] = asset['LoadBalancerDescriptions'][0]['DNSName'] + asset['extras']['vpc_id'] = asset['LoadBalancerDescriptions'][0]['VPCId'] + db_put_asset(asset_id, asset, table) + else: + event_state = 'destroy' + asset = db_fetch_asset(asset_id, table) + +def set_lbv2_vars(event): + global asset_id, asset, event_state + + if event['detail']['eventName'] == 'CreateLoadBalancer': + time.sleep(60) + event_state='create' +# lbv2_name = event['detail']['requestParameters']['name'] +# asset_id = elbv2.describe_load_balancers(Names=[lbv2_name])['LoadBalancers'][0]['LoadBalancerArn'] + asset_id = event['detail']['responseElements']['loadBalancers'][0]['loadBalancerArn'] + asset = elbv2.describe_load_balancers(LoadBalancerArns=[asset_id]) + asset.pop('ResponseMetadata') + try: + tags = elbv2.describe_tags(ResourceArns=[asset_id])['TagDescriptions'][0]['Tags'] + except: + tags = [] + asset['tags'] = tags + asset['extras'] = {} + asset['extras']['type'] = 'elb' + asset['extras']['version'] = 'v2' + # Perhaps 'region' could/should be derived from availability zone + asset['extras']['region'] = event['detail']['awsRegion'] + asset['extras']['lb_scheme'] = asset['LoadBalancers'][0]['Scheme'] + if asset['extras']['lb_scheme'] == 'internal': + asset['extras']['private_dns_name'] = asset['LoadBalancers'][0]['DNSName'] + else: + asset['extras']['public_dns_name'] = asset['LoadBalancers'][0]['DNSName'] + asset['extras']['vpc_id'] = asset['LoadBalancers'][0]['VpcId'] + db_put_asset(asset_id, asset, table) + else: + event_state='destroy' + asset_id = event['detail']['requestParameters']['loadBalancerArn'] + asset = db_fetch_asset(asset_id, table) + +def db_put_asset(asset_id, asset, table): + # Remove null values from the response. You cannot save a dict/JSON document in DynamoDB if it contains null + # values + asset = remove_empty_from_dict(asset) + asset_dump = json.dumps(asset,default=json_serial) + asset_attributes = json.loads(asset_dump) + + table.put_item( + Item={ + 'AssetId': asset_id, + 'AssetAttributes': asset_attributes + } + ) + region = asset['extras']['region'] + +def db_fetch_asset(asset_id, table): + # Fetch item from DynamoDB + asset = table.get_item( + Key={ + 'AssetId': asset_id + }, + AttributesToGet=[ + 'AssetAttributes' + ] + ) + asset = asset['Item']['AssetAttributes'] + # Make sure that empty elements are initialized + try: + tags = asset['tags'] + except: + tags = [] + asset['tags'] = tags + region = asset['extras']['region'] + + return asset + def create_resource_record(zone_id, host_name, hosted_zone_name, type, value): """This function creates resource records in the hosted zone passed by the calling function.""" print 'Updating %s record %s in zone %s ' % (type, host_name, hosted_zone_name) @@ -373,7 +512,7 @@ def get_dhcp_configurations(dhcp_options_id): return zone_names def reverse_list(list): - """Reverses the order of the instance's IP address and helps construct the reverse lookup zone name.""" + """Reverses the order of the asset's IP address and helps construct the reverse lookup zone name.""" if (re.search('\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}',list)) or (re.search('\d{1,3}.\d{1,3}.\d{1,3}\.',list)) or (re.search('\d{1,3}.\d{1,3}\.',list)) or (re.search('\d{1,3}\.',list)): list = str.split(str(list),'.') list = filter(None, list) @@ -384,7 +523,7 @@ def reverse_list(list): return reversed_list else: print 'Not a valid ip' - exit() + sys.exit() def get_reversed_domain_prefix(subnet_mask, private_ip): """Uses the mask to get the zone prefix for the reverse lookup zone""" @@ -398,14 +537,14 @@ def get_reversed_domain_prefix(subnet_mask, private_ip): first_octet = re.search('\d{1,3}.', private_ip) return first_octet.group(0) -def create_reverse_lookup_zone(instance, reversed_domain_prefix, region): +def create_reverse_lookup_zone(vpc_id, reversed_domain_prefix, region): """Creates the reverse lookup zone.""" print 'Creating reverse lookup zone %s' % reversed_domain_prefix + 'in.addr.arpa.' route53.create_hosted_zone( Name = reversed_domain_prefix + 'in-addr.arpa.', VPC = { 'VPCRegion':region, - 'VPCId': instance['Reservations'][0]['Instances'][0]['VpcId'] + 'VPCId': vpc_id }, CallerReference=str(uuid.uuid1()), HostedZoneConfig={ @@ -457,4 +596,4 @@ def is_dns_support_enabled(vpc): def get_hosted_zone_properties(zone_id): hosted_zone_properties = route53.get_hosted_zone(Id=zone_id) hosted_zone_properties.pop('ResponseMetadata') - return hosted_zone_properties \ No newline at end of file + return hosted_zone_properties