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: BypassRdsProxy: DependsOn: - BypassRdsProxyRole Properties: Architectures: - x86_64 Code: ZipFile: "import sys\nsys.path.append('/opt')\n\nimport os\nimport json\nimport boto3\nimport multi_region_db\n\ncustom_functions = multi_region_db.Functions()\n\ndef handler(event, context):\n\ \ \n print(json.dumps(event))\n \n '''\n For each global database endpoint, we'll update it to point to the\n regional writer endpoint.\n '''\n for endpoint_type\ \ in ['READER', 'WRITER']:\n \n custom_functions.update_dns_record(\n fqdn = os.environ['GLOBAL_APP_DB_' + endpoint_type + '_ENDPOINT'],\n new_value\ \ = os.environ['REGIONAL_APP_DB_CLUSTER_' + endpoint_type + '_ENDPOINT'],\n hosted_zone_id = os.environ['PRIVATE_HOSTED_ZONE_ID'],\n )\n \n return {\n \ \ 'code': 200,\n 'records': json.dumps([])\n }" Description: Updates the global database endpoints to bypass the RDS proxy Environment: Variables: GLOBAL_APP_DB_READER_ENDPOINT: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /GlobalAppDbReaderDnsEndpoint}} GLOBAL_APP_DB_WRITER_ENDPOINT: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /GlobalAppDbWriterDnsEndpoint}} PRIVATE_HOSTED_ZONE_ID: !Ref 'PrivateHostedZoneId' REGIONAL_APP_DB_CLUSTER_READER_ENDPOINT: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDbClusterReaderEndpoint}} REGIONAL_APP_DB_CLUSTER_WRITER_ENDPOINT: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDbClusterWriterEndpoint}} Handler: index.handler Layers: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalLambdaLayerVersionArn}} MemorySize: 128 Role: !GetAtt 'BypassRdsProxyRole.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 BypassRdsProxyLogGroup: DeletionPolicy: Delete DependsOn: - BypassRdsProxy Properties: LogGroupName: !Join - '' - - /aws/lambda/ - !Ref 'BypassRdsProxy' RetentionInDays: 30 Type: AWS::Logs::LogGroup BypassRdsProxyMethod: 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 'BypassRdsProxy.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: getBypassRdsProxy ResourceId: !Ref 'BypassRdsProxyResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method BypassRdsProxyMethodInvocationPermission: Condition: '' DependsOn: - BypassRdsProxy Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt 'BypassRdsProxy.Arn' Principal: apigateway.amazonaws.com SourceArn: !Join - '' - - 'arn:aws:execute-api:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - ':' - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} - /*/ - GET - / - bypass-rds-proxy Type: AWS::Lambda::Permission BypassRdsProxyResource: Condition: '' DependsOn: [] Properties: ParentId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiRootResourceId}} PathPart: bypass-rds-proxy RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Resource BypassRdsProxyResourceOptionsMethod: 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 'BypassRdsProxyResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method BypassRdsProxyRole: 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 - PolicyDocument: Statement: - Action: - route53:ChangeResourceRecordSets Effect: Allow Resource: - !Join - '' - - arn:aws:route53:::hostedzone/ - !Ref 'PrivateHostedZoneId' Sid: UpdateRoute53Records PolicyName: update-route53-records Type: AWS::IAM::Role CalculateRecoveryTime: DependsOn: - CalculateRecoveryTimeRole 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 DATE_PART('second', max(insertedon)::time\ \ - min(insertedon)::time) AS rto\n FROM dataclient \n WHERE http_code = 500 \n GROUP BY http_code\n ''');\n \n client_events = curs.fetchall()\n \n curs.close()\n\ \ db_conn.close()\n \n records_to_return = []\n \n for client_event in client_events:\n \n c = 0\n temp2 = {}\n \n for col in curs.description:\n\ \ \n temp2.update({str(col[0]): client_event[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 the resolved RTO 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 'CalculateRecoveryTimeRole.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 CalculateRecoveryTimeLogGroup: DeletionPolicy: Delete DependsOn: - CalculateRecoveryTime Properties: LogGroupName: !Join - '' - - /aws/lambda/ - !Ref 'CalculateRecoveryTime' RetentionInDays: 30 Type: AWS::Logs::LogGroup CalculateRecoveryTimeMethod: 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 'CalculateRecoveryTime.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: getRecoveryTime ResourceId: !Ref 'CalculateRecoveryTimeResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method CalculateRecoveryTimeMethodInvocationPermission: Condition: '' DependsOn: - CalculateRecoveryTime Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt 'CalculateRecoveryTime.Arn' Principal: apigateway.amazonaws.com SourceArn: !Join - '' - - 'arn:aws:execute-api:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - ':' - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} - /*/ - GET - / - calculate-recovery-time Type: AWS::Lambda::Permission CalculateRecoveryTimeResource: Condition: '' DependsOn: [] Properties: ParentId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiRootResourceId}} PathPart: calculate-recovery-time RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Resource CalculateRecoveryTimeResourceOptionsMethod: 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 'CalculateRecoveryTimeResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method CalculateRecoveryTimeRole: 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 InitiateFailover: DependsOn: - InitiateFailoverRole 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 trigger_failover():\n \n try:\n \n boto3.client('rds').failover_db_cluster(\n \ \ DBClusterIdentifier = os.environ['REGIONAL_APP_DB_CLUSTER_IDENTIFIER']\n )\n \n except boto3_client_error as e:\n raise Exception('Failed to Initiate Cluster\ \ Failover: ' + str(e))\n \ndef log_failover_event():\n \n eastern = dateutil.tz.gettz('US/Eastern')\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 \ndef handler(event, context):\n \n print(json.dumps(event))\n \n trigger_failover()\n\n log_failover_event()\n \n return {\n \ \ 'code': 200,\n 'body': []\n }" Description: Logs failover events Environment: Variables: GLOBAL_DEMO_DB_WRITER_ENDPOINT: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /GlobalDemoDbWriterDnsEndpoint}} REGIONAL_APP_DB_CLUSTER_IDENTIFIER: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDbClusterIdentifier}} 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 'InitiateFailoverRole.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 InitiateFailoverLogGroup: DeletionPolicy: Delete DependsOn: - InitiateFailover Properties: LogGroupName: !Join - '' - - /aws/lambda/ - !Ref 'InitiateFailover' RetentionInDays: 30 Type: AWS::Logs::LogGroup InitiateFailoverMethod: 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 'InitiateFailover.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: getInitiateFailover ResourceId: !Ref 'InitiateFailoverResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method InitiateFailoverMethodInvocationPermission: Condition: '' DependsOn: - InitiateFailover Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt 'InitiateFailover.Arn' Principal: apigateway.amazonaws.com SourceArn: !Join - '' - - 'arn:aws:execute-api:' - !Ref 'AWS::Region' - ':' - !Ref 'AWS::AccountId' - ':' - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} - /*/ - GET - / - initiate-failover Type: AWS::Lambda::Permission InitiateFailoverResource: Condition: '' DependsOn: [] Properties: ParentId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiRootResourceId}} PathPart: initiate-failover RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Resource InitiateFailoverResourceOptionsMethod: 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 'InitiateFailoverResource' RestApiId: !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /ApiId}} Type: AWS::ApiGateway::Method InitiateFailoverRole: 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 - PolicyDocument: Statement: - Action: - rds:FailoverDBCluster Effect: Allow Resource: - !Join - '' - - '{{resolve:ssm:/' - !Ref 'MainStackName' - /RegionalAppDbClusterArn}} Sid: InitiateClusterFailover PolicyName: initiate-cluster-failover Type: AWS::IAM::Role