Conditions: IsFailoverRegion: !Not - !Equals - !Ref 'PrimaryRegionName' - !Ref 'AWS::Region' IsPrimaryRegion: !Equals - !Ref 'PrimaryRegionName' - !Ref 'AWS::Region' Metadata: AWS::CloudFormation::Interface: ParameterGroups: [] ParameterLabels: {} Comments: '' CreatedBy: Carter Meyers (AWS) Description: This application deploys a Global RDS Aurora cluster. LastUpdated: February 20, 2023 Version: v1.09 Outputs: RegionalCfnExportRetrieverArn: Condition: '' Value: !GetAtt 'CfnExportRetriever.Arn' RegionalDnsRecordDeleterArn: Condition: '' Value: !If - IsPrimaryRegion - !GetAtt 'DnsRecordDeleter.Arn' - '-' RegionalLambdaLayerVersionArn: Condition: '' Value: !Ref 'LambdaLayerCreatorResource' Parameters: CodeDownloadUrl: Default: https://codeload.github.com/aws-samples/amazon-aurora-postgresql-fast-failover-demo/zip/refs/heads/main Description: The URL from which the supporting codebase can be downloaded. This codebase is used to deploy the demo dashboard. Type: String DatabaseAdminPassword: Description: The password to be used for the RDS Aurora admin account. NoEcho: true Type: String DatabaseAdminUsername: Description: The username to be used for the RDS Aurora admin account. Type: String FailoverDatabaseSubnetZoneACidr: Default: 10.10.10.0/24 Description: The CIDR range you wish to use for your primary database subnet. Type: String FailoverDatabaseSubnetZoneBCidr: Default: 10.10.13.0/24 Description: The CIDR range you wish to use for your failover database subnet. Type: String FailoverPrivateSubnetZoneACidr: Default: 10.10.9.0/24 Description: The CIDR range you wish to use for your primary private subnet. Type: String FailoverPrivateSubnetZoneBCidr: Default: 10.10.12.0/24 Description: The CIDR range you wish to use for your failover private subnet. Type: String FailoverPublicSubnetZoneACidr: Default: 10.10.8.0/24 Description: The CIDR range you wish to use for your primary public subnet. Type: String FailoverPublicSubnetZoneBCidr: Default: 10.10.11.0/24 Description: The CIDR range you wish to use for your failover public subnet. Type: String FailoverRegionName: Default: us-east-2 Description: The name of the failover region (e.g., us-east-1). You may choose any AWS Region that supports the required services. The primary and failover regions must be different. Type: String FailoverVpcCidr: Default: 10.10.8.0/21 Description: The CIDR range you wish to use for your VPC. Type: String MainStackName: Type: String PrimaryDatabaseSubnetZoneACidr: Default: 10.10.2.0/24 Description: The CIDR range you wish to use for your primary database subnet. Type: String PrimaryDatabaseSubnetZoneBCidr: Default: 10.10.5.0/24 Description: The CIDR range you wish to use for your failover database subnet. Type: String PrimaryPrivateSubnetZoneACidr: Default: 10.10.1.0/24 Description: The CIDR range you wish to use for your primary private subnet. Type: String PrimaryPrivateSubnetZoneBCidr: Default: 10.10.4.0/24 Description: The CIDR range you wish to use for your failover private subnet. Type: String PrimaryPublicSubnetZoneACidr: Default: 10.10.0.0/24 Description: The CIDR range you wish to use for your primary public subnet. Type: String PrimaryPublicSubnetZoneBCidr: Default: 10.10.3.0/24 Description: The CIDR range you wish to use for your failover public subnet. Type: String PrimaryRegionName: Default: us-east-1 Description: The name of the primary region (e.g., us-east-1). You may choose any AWS Region that supports the required services. The primary and failover regions must be different. Type: String PrimaryVpcCidr: Default: 10.10.0.0/21 Description: The CIDR range you wish to use for your VPC. Type: String PrivateHostedZoneId: Type: String PublicFqdn: Description: >- The FQDN to be used by this application (e.g., multi-region-aurora.example.com). An Amazon ACM Certificate will be issued for this FQDN and attached to an Amazon ALB. This FQDN should NOT have a DNS record currently defined in the corresponding Route 53 Hosted Zone. Type: String PublicHostedZoneId: Description: The ID of the public Route 53 Hosted Zone corresponding to the public Service FQDN. Type: String Resources: CfnExportRetriever: DependsOn: - CfnExportRetrieverRole Properties: Architectures: - x86_64 Code: ZipFile: "import sys\nimport subprocess\n\nsubprocess.call('pip install cfnresponse -t /tmp/ --no-cache-dir'.split(), stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL)\nsys.path.insert(1,\ \ '/tmp/')\n\nimport io\nimport os\nimport json\nimport boto3\nimport cfnresponse\nfrom botocore.exceptions import ClientError, ParamValidationError\n\n'''\n - Region | str\n - ExportPrefix\ \ | str\n'''\ndef handler(event, context):\n \n print(json.dumps(event))\n \n arguments = event['ResourceProperties']['Properties']\n operation = event['ResourceProperties']['Type'].replace('Custom::',\ \ '')\n \n response_data = {}\n \n boto3Session = boto3.Session(\n region_name = arguments['Region']\n )\n \n cfn_client = boto3Session.client('cloudformation')\n \ \ \n if event['RequestType'] in ['Create', 'Update']:\n \n try:\n \n response = cfn_client.list_exports()\n \n '''\n \ \ For each CloudFormation export in this region\n '''\n for export in response['Exports']:\n \t\n '''\n If this export has\ \ the proper prefix\n '''\n if export['Name'].startswith(arguments['ExportPrefix']):\n \n response_data[export['Name'].replace(arguments['ExportPrefix']\ \ + '-', '')] = export['Value']\n \n except ClientError as e:\n \n print('Failed to Retrieve CFN Exports: ' + str(e.response))\n return cfnresponse.send(event,\ \ context, cfnresponse.FAILED, response_data)\n \n return cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)" Description: '' Handler: index.handler Layers: [] MemorySize: 128 Role: !GetAtt 'CfnExportRetrieverRole.Arn' Runtime: python3.9 Timeout: 15 TracingConfig: Mode: PassThrough Type: AWS::Lambda::Function CfnExportRetrieverLogGroup: DeletionPolicy: Delete DependsOn: - CfnExportRetriever Properties: LogGroupName: !Join - '' - - /aws/lambda/ - !Ref 'CfnExportRetriever' RetentionInDays: 30 Type: AWS::Logs::LogGroup CfnExportRetrieverRole: DependsOn: [] Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - lambda.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole MaxSessionDuration: 3600 Policies: - PolicyDocument: Statement: - Action: - cloudformation:ListExports Effect: Allow Resource: - '*' Sid: GetCFNOutputs PolicyName: main-policy Type: AWS::IAM::Role DnsRecordDeleter: Condition: IsPrimaryRegion DependsOn: - DnsRecordDeleterRole Properties: Architectures: - x86_64 Code: ZipFile: "import sys\nimport subprocess\n\nsubprocess.call('pip install cfnresponse -t /tmp/ --no-cache-dir'.split(), stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL)\nsys.path.insert(1,\ \ '/tmp/')\n\nimport json\nimport boto3\nimport cfnresponse\nfrom botocore.exceptions import ClientError, ParamValidationError\n\n'''\n - Fqdns | list \n - HostedZoneId | str\n'''\ndef handler(event,\ \ context):\n \n print(json.dumps(event))\n \n arguments = event['ResourceProperties']['Properties']\n operation = event['ResourceProperties']['Type'].replace('Custom::', '')\n\ \ \n response_data = {}\n \n route53_client = boto3.client('route53')\n \n if event['RequestType'] in ['Delete']:\n \n try:\n \n record_sets_resp\ \ = route53_client.list_resource_record_sets(\n HostedZoneId = arguments['HostedZoneId'],\n )\n \n change_batch = []\n \n \ \ for record_set in record_sets_resp['ResourceRecordSets']:\n \n print(record_set)\n \n '''\n We'll be leaving NS\ \ and SOA records.\n '''\n if record_set['Type'] in ['NS', 'SOA']:\n print('Not An Eligible Record Type - Skipping')\n continue\n\ \ \n '''\n If we've been instructed to delete all FQDNs or this FQDN\n \n We're going to use for comparison\ \ the raw record name from Route53\n as well as the name minus the trailing period.\n '''\n if '*' in arguments['Fqdns'] or (record_set['Name']\ \ in arguments['Fqdns'] or record_set['Name'][0:-1] in arguments['Fqdns']):\n \n print('Deleting Record')\n \n change_batch.append({\n\ \ 'Action': 'DELETE',\n 'ResourceRecordSet': record_set,\n })\n \n if len(change_batch) > 0:\n \ \ \n route53_client.change_resource_record_sets(\n HostedZoneId = arguments['HostedZoneId'],\n ChangeBatch = {\n \ \ 'Changes': change_batch\n }\n )\n \n except ClientError as e:\n \n print('Failed to Delete DNS Records: ' + str(e.response))\n\ \ return cfnresponse.send(event, context, cfnresponse.FAILED, response_data)\n \n return cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)" Description: Deletes DNS Records on Stack Cleanup Handler: index.handler Layers: [] MemorySize: 128 Role: !GetAtt 'DnsRecordDeleterRole.Arn' Runtime: python3.9 Timeout: 15 TracingConfig: Mode: PassThrough Type: AWS::Lambda::Function DnsRecordDeleterLogGroup: Condition: IsPrimaryRegion DeletionPolicy: Delete DependsOn: - DnsRecordDeleter Properties: LogGroupName: !Join - '' - - /aws/lambda/ - !Ref 'DnsRecordDeleter' RetentionInDays: 30 Type: AWS::Logs::LogGroup DnsRecordDeleterRole: DependsOn: [] Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - lambda.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole MaxSessionDuration: 3600 Policies: - PolicyDocument: Statement: - Action: - route53:ListResourceRecordSets - route53:ChangeResourceRecordSets Effect: Allow Resource: - !Join - '' - - arn:aws:route53:::hostedzone/ - !Ref 'PublicHostedZoneId' - !Join - '' - - arn:aws:route53:::hostedzone/ - !Ref 'PrivateHostedZoneId' Sid: DeleteRoute53Records PolicyName: delete-route53-records Type: AWS::IAM::Role LambdaLayerCreator: DependsOn: - LambdaLayerCreatorRole Properties: Architectures: - x86_64 Code: ZipFile: "_A='%H:%M:%S'\nimport sys,subprocess\nsubprocess.call('pip install cfnresponse -t /tmp/ --no-cache-dir'.split(),stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)\nsys.path.insert(1,'/tmp/')\n\ import io,os,json,boto3,shutil,cfnresponse\nfrom zipfile import ZipFile\nfrom botocore.exceptions import ClientError as boto3_client_error\ndef zip_directory(path):\n\tfor (B,F,C) in os.walk(path):\n\ \t\tfor D in C:A=os.path.join(B,D);E=A[len(path)+len(os.sep):];yield(A,E)\ndef make_zip_file_bytes(path):\n\tA=io.BytesIO()\n\twith ZipFile(A,'w')as B:\n\t\tfor (C,D) in zip_directory(path=path):B.write(C,D)\n\ \treturn A.getvalue()\n'\\n - Region | str\\n - Packages | list\\n - LayerName | str\\n'\ndef handler(event,context):\n\tJ='RequestType';I='ResourceProperties';G='LayerName';C=context;A=event;print(json.dumps(A));B=A[I]['Properties'];N=A[I]['Type'].replace('Custom::','');D={};K=boto3.Session(region_name=B['Region']);E=K.client('lambda')\n\ \tif A[J]in['Create','Update']:\n\t\tsubprocess.call(('pip install '+' '.join(B['Packages'])+' -t /tmp/lambda-layer --no-cache-dir').split(),stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL);shutil.copyfile(os.path.realpath(__file__),'/tmp/lambda-layer/multi_region_db.py')\n\ \t\ttry:H=E.publish_layer_version(LayerName=B[G],Content={'ZipFile':make_zip_file_bytes('/tmp/lambda-layer')},CompatibleRuntimes=['python3.9'],CompatibleArchitectures=['x86_64','arm64']);return\ \ cfnresponse.send(A,C,cfnresponse.SUCCESS,D,H['LayerVersionArn'])\n\t\texcept boto3_client_error as F:print('Failed to Deploy Lambda Layer: '+str(F.response));return cfnresponse.send(A,C,cfnresponse.FAILED,D)\n\ \tif A[J]in['Delete']:\n\t\ttry:\n\t\t\tL=E.list_layer_versions(LayerName=B[G])\n\t\t\tfor M in L['LayerVersions']:H=E.delete_layer_version(LayerName=B[G],VersionNumber=M['Version'])\n\t\texcept\ \ boto3_client_error as F:print('Failed to Delete Layer Versions: '+str(F.response));return cfnresponse.send(A,C,cfnresponse.FAILED,D)\n\t\treturn cfnresponse.send(A,C,cfnresponse.SUCCESS,D)\n\ import dateutil.tz\nfrom datetime import datetime\nfrom datetime import timedelta\nclass Functions:\n\tdef __init__(A):''\n\tdef add_five_seconds(A,start_time):return (datetime.strptime(str(start_time),_A)+timedelta(seconds=5)).strftime(_A)\n\ \tdef subtract_five_seconds(A,start_time):return (datetime.strptime(str(start_time),_A)+timedelta(seconds=-5)).strftime(_A)\n\tdef add_time(B,label,data):\n\t\tA=label;C=dateutil.tz.gettz('US/Pacific');D=datetime.now(tz=C)\n\ \t\twhile datetime.strptime(A[len(A)-1],_A)+timedelta(seconds=9)