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 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: GetClusterInfo: DependsOn: - GetClusterInfoRole Properties: Architectures: - x86_64 Code: ZipFile: "import os\nimport json\nimport boto3\n\ndef handler(event, context):\n \n print(json.dumps(event))\n \n data = {}\n rds_client = boto3.client('rds')\n\n cluster_resp\ \ = rds_client.describe_db_clusters(\n DBClusterIdentifier = os.environ['REGIONAL_APP_DB_CLUSTER_IDENTIFIER']\n )\n \n for member in cluster_resp['DBClusters'][0]['DBClusterMembers']:\n\ \ \n instance_resp = rds_client.describe_db_instances(\n DBInstanceIdentifier = member['DBInstanceIdentifier']\n )\n \n data[member['DBInstanceIdentifier']]\ \ = {\n 'az': instance_resp['DBInstances'][0]['AvailabilityZone'],\n 'type': 'WRITER' if member['IsClusterWriter'] is True else 'READER'\n }\n \n return\ \ {\n 'code': 200,\n 'body': json.dumps(data)\n }" Description: Retrieves DB cluster info Environment: Variables: REGIONAL_APP_DB_CLUSTER_IDENTIFIER: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDbClusterIdentifier}} Handler: index.handler Layers: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalLambdaLayerVersionArn}} MemorySize: 128 Role: !GetAtt 'GetClusterInfoRole.Arn' Runtime: python3.9 Timeout: 60 TracingConfig: Mode: PassThrough VpcConfig: SecurityGroupIds: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /LambdaSecurityGroupId}} SubnetIds: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /PrivateSubnetZoneAId}} - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /PrivateSubnetZoneBId}} Type: AWS::Lambda::Function GetClusterInfoLogGroup: DeletionPolicy: Delete DependsOn: - GetClusterInfo Properties: LogGroupName: !Join - '' - - /aws/lambda/ - !Ref 'GetClusterInfo' RetentionInDays: 30 Type: AWS::Logs::LogGroup GetClusterInfoMethod: Condition: '' Properties: ApiKeyRequired: false AuthorizationType: NONE HttpMethod: GET Integration: IntegrationHttpMethod: POST IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.body') StatusCode: '200' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":400.* StatusCode: '400' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":401.* StatusCode: '401' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":403.* StatusCode: '403' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":404.* StatusCode: '404' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":415.* StatusCode: '415' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":429.* StatusCode: '429' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":500.* StatusCode: '500' PassthroughBehavior: WHEN_NO_TEMPLATES RequestTemplates: application/json: "#set($params = $input.params())\n{\n\"queryParams\": { #set($paramSet = $params.get('querystring')) #foreach($paramName in $paramSet.keySet()) \"$paramName\" : \"$util.escapeJavaScript($paramSet.get($paramName))\"\ \ #if($foreach.hasNext),#end #end }\n}" Type: AWS Uri: !Join - '' - - 'arn:aws:apigateway:' - !Ref 'AWS::Region' - :lambda:path/2015-03-31/functions/ - !GetAtt 'GetClusterInfo.Arn' - /invocations MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '200' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '400' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '401' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '403' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '404' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '415' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '429' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '500' OperationName: getClusterInfo ResourceId: !Ref 'GetClusterInfoResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method GetClusterInfoMethodInvocationPermission: Condition: '' DependsOn: - GetClusterInfo Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt 'GetClusterInfo.Arn' Principal: apigateway.amazonaws.com SourceArn: !Join - '' - - 'arn:aws:execute-api:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - ':' - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} - /*/ - GET - / - get-cluster-info Type: AWS::Lambda::Permission GetClusterInfoResource: Condition: '' DependsOn: [] Properties: ParentId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiRootResourceId}} PathPart: get-cluster-info RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Resource GetClusterInfoResourceOptionsMethod: Condition: '' Properties: ApiKeyRequired: false AuthorizationType: NONE HttpMethod: OPTIONS Integration: IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Headers: '''Content-Type,X-Amz-Date,Authorization,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: '{}' StatusCode: '200' PassthroughBehavior: WHEN_NO_TEMPLATES RequestTemplates: application/json: '{"statusCode": 200}' Type: MOCK MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true StatusCode: '200' ResourceId: !Ref 'GetClusterInfoResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method GetClusterInfoRole: DependsOn: [] Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - lambda.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole MaxSessionDuration: 3600 Policies: - PolicyDocument: Statement: - Action: - rds:DescribeDBClusters Effect: Allow Resource: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDbClusterArn}} Sid: DescribeRDSClusters PolicyName: get-db-clusters - PolicyDocument: Statement: - Action: - rds:DescribeDBInstances Effect: Allow Resource: - !Join - '' - - 'arn:aws:rds:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - :db* Sid: DescribeDBInstances PolicyName: get-db-instances Type: AWS::IAM::Role GetFailoverEvents: DependsOn: - GetFailoverEventsRole Properties: Architectures: - x86_64 Code: ZipFile: "import sys\nsys.path.append('/opt')\n\nimport os\nimport json\nimport psycopg2\nimport multi_region_db\n\ncustom_functions = multi_region_db.Functions()\n \ndef handler(event, context):\n\ \ \n print(json.dumps(event))\n \n demo_db_credentials = custom_functions.get_db_credentials('Demo')\n \n db_conn = psycopg2.connect(\n host = os.environ['GLOBAL_DEMO_DB_WRITER_ENDPOINT'],\n\ \ port = demo_db_credentials['port'],\n user = demo_db_credentials['username'],\n password = demo_db_credentials['password'],\n database = demo_db_credentials['database'],\n\ \ connect_timeout = 3,\n sslmode = 'require',\n )\n \n curs = db_conn.cursor()\n \n curs.execute('''\n SELECT \n event,\n to_char(insertedon,'HH24:MI:SS')\ \ AS time, \n insertedon \n FROM failoverevents\n ORDER BY insertedon\n ''');\n \n failover_events = curs.fetchall()\n \n curs.close()\n db_conn.close()\n\ \ \n records_to_return = []\n \n for x in failover_events:\n \n c = 0\n temp2 = {}\n \n for col in curs.description:\n \n temp2.update({str(col[0]):\ \ x[c]})\n c += 1\n \n records_to_return.append(temp2)\n \n return {\n 'code': 200,\n 'body': json.dumps(records_to_return, default = str)\n }" Description: Retrieves failover events from the database Environment: Variables: GLOBAL_DEMO_DB_WRITER_ENDPOINT: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /GlobalDemoDbWriterDnsEndpoint}} REGIONAL_DEMO_DB_SECRET_ARN: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalDemoDbAdminSecretArn}} Handler: index.handler Layers: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalLambdaLayerVersionArn}} MemorySize: 128 Role: !GetAtt 'GetFailoverEventsRole.Arn' Runtime: python3.9 Timeout: 60 TracingConfig: Mode: PassThrough VpcConfig: SecurityGroupIds: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /LambdaSecurityGroupId}} SubnetIds: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /PrivateSubnetZoneAId}} - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /PrivateSubnetZoneBId}} Type: AWS::Lambda::Function GetFailoverEventsLogGroup: DeletionPolicy: Delete DependsOn: - GetFailoverEvents Properties: LogGroupName: !Join - '' - - /aws/lambda/ - !Ref 'GetFailoverEvents' RetentionInDays: 30 Type: AWS::Logs::LogGroup GetFailoverEventsMethod: Condition: '' Properties: ApiKeyRequired: false AuthorizationType: NONE HttpMethod: GET Integration: IntegrationHttpMethod: POST IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.body') StatusCode: '200' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":400.* StatusCode: '400' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":401.* StatusCode: '401' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":403.* StatusCode: '403' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":404.* StatusCode: '404' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":415.* StatusCode: '415' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":429.* StatusCode: '429' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":500.* StatusCode: '500' PassthroughBehavior: WHEN_NO_TEMPLATES RequestTemplates: application/json: "#set($params = $input.params())\n{\n\"queryParams\": { #set($paramSet = $params.get('querystring')) #foreach($paramName in $paramSet.keySet()) \"$paramName\" : \"$util.escapeJavaScript($paramSet.get($paramName))\"\ \ #if($foreach.hasNext),#end #end }\n}" Type: AWS Uri: !Join - '' - - 'arn:aws:apigateway:' - !Ref 'AWS::Region' - :lambda:path/2015-03-31/functions/ - !GetAtt 'GetFailoverEvents.Arn' - /invocations MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '200' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '400' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '401' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '403' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '404' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '415' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '429' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '500' OperationName: getFailoverEvents ResourceId: !Ref 'GetFailoverEventsResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method GetFailoverEventsMethodInvocationPermission: Condition: '' DependsOn: - GetFailoverEvents Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt 'GetFailoverEvents.Arn' Principal: apigateway.amazonaws.com SourceArn: !Join - '' - - 'arn:aws:execute-api:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - ':' - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} - /*/ - GET - / - get-failover-events Type: AWS::Lambda::Permission GetFailoverEventsResource: Condition: '' DependsOn: [] Properties: ParentId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiRootResourceId}} PathPart: get-failover-events RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Resource GetFailoverEventsResourceOptionsMethod: Condition: '' Properties: ApiKeyRequired: false AuthorizationType: NONE HttpMethod: OPTIONS Integration: IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Headers: '''Content-Type,X-Amz-Date,Authorization,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: '{}' StatusCode: '200' PassthroughBehavior: WHEN_NO_TEMPLATES RequestTemplates: application/json: '{"statusCode": 200}' Type: MOCK MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true StatusCode: '200' ResourceId: !Ref 'GetFailoverEventsResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method GetFailoverEventsRole: DependsOn: [] Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - lambda.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole MaxSessionDuration: 3600 Policies: - PolicyDocument: Statement: - Action: - secretsmanager:GetSecretValue Effect: Allow Resource: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDbAdminSecretArn}} - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalDemoDbAdminSecretArn}} Sid: GetRDSAdminSecret - Action: - kms:Decrypt Effect: Allow Resource: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalKmsKeyArn}} Sid: DecryptWithKMS PolicyName: database-secret-retrieval Type: AWS::IAM::Role UpdateDatabaseNacl: Condition: IsPrimaryRegion DependsOn: - UpdateDatabaseNaclRole Properties: Architectures: - x86_64 Code: ZipFile: "import sys\nsys.path.append('/opt')\n\nimport os\nimport json\nimport boto3\nimport psycopg2\nimport datetime\nimport dateutil.tz\nimport multi_region_db\nfrom botocore.exceptions import\ \ ClientError as boto3_client_error\n\ncustom_functions = multi_region_db.Functions()\n\ndef handler(event, context):\n \n print(json.dumps(event))\n \n eastern = dateutil.tz.gettz('US/Eastern')\n\ \ \n demo_db_credentials = custom_functions.get_db_credentials('Demo')\n \n db_conn = psycopg2.connect(\n host = os.environ['GLOBAL_DEMO_DB_WRITER_ENDPOINT'],\n port\ \ = demo_db_credentials['port'],\n user = demo_db_credentials['username'],\n sslmode = 'require',\n password = demo_db_credentials['password'],\n database = demo_db_credentials['database'],\n\ \ connect_timeout = 3,\n )\n\n curs = db_conn.cursor()\n curs.execute(\"INSERT INTO failoverevents (event, insertedon) values (1,'\" + datetime.datetime.now(tz = eastern).strftime(\"\ %m/%d/%Y %H:%M:%S\") + \"' )\")\n db_conn.commit()\n \n curs.close()\n db_conn.close()\n \n try:\n \n boto3.client('ec2').replace_network_acl_entry(\n \ \ Egress = False, \n CidrBlock = '0.0.0.0/0',\n NetworkAclId = os.environ['REGIONAL_APP_DB_NACL_ID'],\n Protocol = '-1',\n RuleAction = 'deny',\n\ \ RuleNumber = 100\n )\n \n except boto3_client_error as e:\n raise Exception('Failed to Update Database NACL')\n \n return {\n 'code': 200,\n \ \ 'body': json.dumps([])\n }" Description: Updates the database NACL to deny incoming traffic Environment: Variables: GLOBAL_DEMO_DB_WRITER_ENDPOINT: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /GlobalDemoDbWriterDnsEndpoint}} REGIONAL_APP_DB_NACL_ID: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDatabaseNaclId}} REGIONAL_DEMO_DB_SECRET_ARN: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalDemoDbAdminSecretArn}} Handler: index.handler Layers: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalLambdaLayerVersionArn}} MemorySize: 128 Role: !GetAtt 'UpdateDatabaseNaclRole.Arn' Runtime: python3.9 Timeout: 60 TracingConfig: Mode: PassThrough VpcConfig: SecurityGroupIds: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /LambdaSecurityGroupId}} SubnetIds: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /PrivateSubnetZoneAId}} - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /PrivateSubnetZoneBId}} Type: AWS::Lambda::Function UpdateDatabaseNaclLogGroup: Condition: IsPrimaryRegion DeletionPolicy: Delete DependsOn: - UpdateDatabaseNacl Properties: LogGroupName: !Join - '' - - /aws/lambda/ - !Ref 'UpdateDatabaseNacl' RetentionInDays: 30 Type: AWS::Logs::LogGroup UpdateDatabaseNaclMethod: Condition: IsPrimaryRegion Properties: ApiKeyRequired: false AuthorizationType: NONE HttpMethod: GET Integration: IntegrationHttpMethod: POST IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.body') StatusCode: '200' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":400.* StatusCode: '400' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":401.* StatusCode: '401' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":403.* StatusCode: '403' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":404.* StatusCode: '404' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":415.* StatusCode: '415' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":429.* StatusCode: '429' - ResponseParameters: method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: $input.path('$.errorMessage') SelectionPattern: .*"code":500.* StatusCode: '500' PassthroughBehavior: WHEN_NO_TEMPLATES RequestTemplates: application/json: "#set($params = $input.params())\n{\n\"queryParams\": { #set($paramSet = $params.get('querystring')) #foreach($paramName in $paramSet.keySet()) \"$paramName\" : \"$util.escapeJavaScript($paramSet.get($paramName))\"\ \ #if($foreach.hasNext),#end #end }\n}" Type: AWS Uri: !Join - '' - - 'arn:aws:apigateway:' - !Ref 'AWS::Region' - :lambda:path/2015-03-31/functions/ - !GetAtt 'UpdateDatabaseNacl.Arn' - /invocations MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '200' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '400' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '401' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '403' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '404' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '415' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '429' - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true StatusCode: '500' OperationName: getFailoverEvents ResourceId: !Ref 'UpdateDatabaseNaclResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method UpdateDatabaseNaclMethodInvocationPermission: Condition: IsPrimaryRegion DependsOn: - UpdateDatabaseNacl Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt 'UpdateDatabaseNacl.Arn' Principal: apigateway.amazonaws.com SourceArn: !Join - '' - - 'arn:aws:execute-api:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - ':' - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} - /*/ - GET - / - update-database-nacl Type: AWS::Lambda::Permission UpdateDatabaseNaclResource: Condition: IsPrimaryRegion DependsOn: [] Properties: ParentId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiRootResourceId}} PathPart: update-database-nacl RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Resource UpdateDatabaseNaclResourceOptionsMethod: Condition: IsPrimaryRegion Properties: ApiKeyRequired: false AuthorizationType: NONE HttpMethod: OPTIONS Integration: IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Headers: '''Content-Type,X-Amz-Date,Authorization,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Methods: '''GET,OPTIONS''' method.response.header.Access-Control-Allow-Origin: '''*''' ResponseTemplates: application/json: '{}' StatusCode: '200' PassthroughBehavior: WHEN_NO_TEMPLATES RequestTemplates: application/json: '{"statusCode": 200}' Type: MOCK MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true StatusCode: '200' ResourceId: !Ref 'UpdateDatabaseNaclResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method UpdateDatabaseNaclRole: DependsOn: [] Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - lambda.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole MaxSessionDuration: 3600 Policies: - PolicyDocument: Statement: - Action: - secretsmanager:GetSecretValue Effect: Allow Resource: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDbAdminSecretArn}} - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalDemoDbAdminSecretArn}} Sid: GetRDSAdminSecret - Action: - kms:Decrypt Effect: Allow Resource: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalKmsKeyArn}} Sid: DecryptWithKMS PolicyName: database-secret-retrieval Type: AWS::IAM::Role UpdateDatabaseNaclRoleCustomPolicy: Condition: IsPrimaryRegion DependsOn: - UpdateDatabaseNaclRole Properties: PolicyDocument: Statement: - Action: - ec2:ReplaceNetworkAclEntry Effect: Allow Resource: - !Join - '' - - 'arn:aws:ec2:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - :network-acl/ - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDatabaseNaclId}} Sid: UpdateACLEntry PolicyName: custom-policy Roles: - !Ref 'UpdateDatabaseNaclRole' Type: AWS::IAM::Policy