# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Description: Route53 Hosted Zones Sync - Serverless Architecture & Private Hosted Zone Parameters: PublicHostedZoneId: Type: String ZoneName: Type: String AliasDontUpdate: Type: String VpcId: Type: String VpcRegion: Type: String Resources: # ---------- PRIVATE HOSTED ZONE ---------- PrivateHostedZone: Type: AWS::Route53::HostedZone Properties: Name: !Ref ZoneName VPCs: - VPCId: !Ref VpcId VPCRegion: !Ref VpcRegion # ---------- AMAZON EVENTBRIDGE RULE ---------- # Rule and target EventBridgeRule: Type: AWS::Events::Rule Properties: Name: "route53-publichostedzone-changes" Description: "Capture Changes in Route53 Public Hosted Zones." EventPattern: source: - aws.route53 detail-type: - "AWS API Call via CloudTrail" detail: eventSource: - route53.amazonaws.com eventName: - ChangeResourceRecordSets requestParameters: hostedZoneId: - !Ref PublicHostedZoneId Targets: - Arn: !GetAtt Route53LambdaFunction.Arn Id: "LambdaFunction" # Lambda permission (for the EventBridge rule) EventBridgeLambdaPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref Route53LambdaFunction Principal: events.amazonaws.com SourceArn: !GetAtt EventBridgeRule.Arn # ---------- LAMBDA FUNCTION ---------- Route53LambdaFunction: Type: AWS::Lambda::Function Properties: Description: "Update Private Hosted Zone from Public Hosted Zone updates." Runtime: python3.9 Timeout: 10 Role: !GetAtt LambdaFunctionRole.Arn Handler: index.lambda_handler Environment: Variables: PRIVATE_HOSTED_ZONE_ID: !Ref PrivateHostedZone DONT_UPDATE: !Ref AliasDontUpdate Code: ZipFile: |- import json import logging import os import boto3 log = logging.getLogger("handler") log.setLevel(logging.INFO) def lambda_handler(event, context): try: # We log the event received log.info("Received event: %s", json.dumps(event)) # We obtain the environment variables private_hz_id = os.environ["PRIVATE_HOSTED_ZONE_ID"] aliases = os.environ["DONT_UPDATE"] # boto3 configuration r53 = boto3.client('route53') # We obtain the information of the event changes = event["detail"]["requestParameters"]["changeBatch"]["changes"] # We initialize the phz_changes variable phz_changes = [] for c in changes: # We add the change if the alias is not part of the "DONT_UPDATE" list alias = (c["resourceRecordSet"]["name"].split("."))[0] if alias in str(aliases): log.info("Alias %s should not be updated", alias) else: # We need to capitalize all the keys in the map (to comply with the boto3 format) and add it to the Changes map c = update_change(c) phz_changes.append(c) # If we have updated the variable phz_changes, we update the Private Hosted Zone if len(phz_changes) > 0: log.info("To update: %s", phz_changes) updateHZ = r53.change_resource_record_sets( HostedZoneId=private_hz_id, ChangeBatch={ 'Comment': 'Updating Private Hosted Zone from changes in Public Hosted Zone', 'Changes': phz_changes } ) log.info("Successfully updated Private Hosted Zone %s", private_hz_id) else: log.info("No updates needed in Private Hosted Zone %s", private_hz_id) except Exception as e: log.exception("whoops") log.info(e) def update_change(c): try: # Initialize new map (Change) newc = {} # We iterate over the keys for k, v in c.items(): # ResourceRecordSet has a map, not a string if k == "resourceRecordSet": # We initialize the new ResourceRecordSet rrs = {} # We iterate over the keys for i, j in v.items(): # Special case 1: TTL if i == "tTL": rrs['TTL'] = j # Special case 2: GeoLocation, AliasTarget and CidrRoutingConfig have maps, not strings elif i == "geoLocation" or i == "aliasTarget" or i == "cidrRoutingConfig": keys = {} for key, value in j.items(): keys[key.title()] = value # Once we finish the iteration, we update resourceRecordSet rrs['GeoLocation'] = keys # Special case 3: ResourceRecords has a list of maps elif i == "resourceRecords": for rs in j: records = [] for key, value in rs.items(): records.append({key.title() : value}) # Once we finish the iteration over resourceRecords, we update resourceRecordSet rrs["ResourceRecords"] = records # For other key-value pairs, we capitalize the first letter else: rrs[i.title()] = j # We create the new ResourceRecordSet map and we add it to the new Change newc['ResourceRecordSet'] = rrs else: # We capitalize the first letter of the Action newc[k.title()] = v # We return the new Change return newc except Exception as e: log.exception("whoops") log.info(e) # Lambda Function IAM Role LambdaFunctionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: LambdaPolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - arn:aws:logs:*:*:* - Effect: Allow Action: - route53:ChangeResourceRecordSets Resource: - !Join - "/" - - arn:aws:route53:::hostedzone - !Ref PrivateHostedZone # CloudWatch Log Group LambdaFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${Route53LambdaFunction} RetentionInDays: 7