AWSTemplateFormatVersion: 2010-09-09 # File format follows https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html # Tests: # Lint: https://github.com/aws-cloudformation/cfn-lint # Nag: https://github.com/stelligent/cfn_nag # aws cloudformation validate-template: https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudformation/validate-template.html Description: >- Senzing aws-cloudformation-database-cluster: 1.3.10 For more information see https://github.com/senzing-garage/aws-cloudformation-database-cluster # ----------------------------------------------------------------------------- # Metadata # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html # ----------------------------------------------------------------------------- Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Senzing installation Parameters: - MultipleDatabases - Label: default: Security responsibility Parameters: - SecurityResponsibility ParameterLabels: MultipleDatabases: default: 'Optional: Would you like to install into a single or multiple databases?' SecurityResponsibility: default: >- Required: A default deployment of this template is for public demonstration only. Before using authentic PII, ensure the security of your deployment. The security of this deployment is your responsibility. To acknowledge your understanding and acceptance of the foregoing, type “I AGREE”. # ----------------------------------------------------------------------------- # Parameters # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html # ----------------------------------------------------------------------------- Parameters: # AWS Console: https://console.aws.amazon.com/cloudformation/home?#/stacks > {stack} > Parameters MultipleDatabases: AllowedValues: - 'Single' - 'Multiple' Default: 'Single' Description: 'Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#multipledatabases' Type: String SecurityResponsibility: AllowedPattern: '.+|^I AGREE$' ConstraintDescription: SecurityResponsibility parameter must be 'I AGREE' Default: '_' Description: 'Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#securityresponsibility' Type: String # ----------------------------------------------------------------------------- # Rules # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/rules-section-structure.html # ----------------------------------------------------------------------------- Rules: ConfirmSecurityResponsibility: Assertions: - Assert: !Equals - !Ref SecurityResponsibility - 'I AGREE' AssertDescription: 'Understanding responsibility and entering "I AGREE" is required to proceed.' # ----------------------------------------------------------------------------- # Mappings # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html # ----------------------------------------------------------------------------- Mappings: Constants: Database: Name: G2 Username: senzing Images: InitPostgresql: public.ecr.aws/senzing/init-database:0.5.2 Stack: Name: database-cluster VpcCidrs: vpc: cidr: 10.0.0.0/16 privsubnet1: cidr: 10.0.10.0/22 privsubnet2: cidr: 10.0.20.0/22 pubsubnet1: cidr: 10.0.30.0/22 pubsubnet2: cidr: 10.0.40.0/22 # ----------------------------------------------------------------------------- # Conditions # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html # ----------------------------------------------------------------------------- Conditions: IfMultipleDatabases: !Equals - !Ref MultipleDatabases - 'Multiple' IfSingleDatabase: !Not - !Equals [!Ref MultipleDatabases, 'Multiple'] # ----------------------------------------------------------------------------- # Resources # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html # ----------------------------------------------------------------------------- Resources: # -- Iam ----------------------------------------------------------------- # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html # AWS Console: https://console.aws.amazon.com/iam/home?#/roles > Search for {stack} IamRoleInitPostgres: Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com Version: '2012-10-17' Description: !Sub '${AWS::StackName}-iam-role-init-postgres' Tags: - Key: Name Value: !Sub '${AWS::StackName}-iam-role-init-postgres' Type: AWS::IAM::Role IamRoleLambda: Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com - lambda.amazonaws.com - route53.amazonaws.com - sqs.amazonaws.com Version: '2012-10-17' Description: !Sub '${AWS::StackName}-iam-role-lambda' Tags: - Key: Name Value: !Sub '${AWS::StackName}-iam-role-lambda' Type: AWS::IAM::Role # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-policy.html # AWS Console: https://console.aws.amazon.com/iam/home?#/roles > Search for {stack} > {role} > inline policy IamPolicyLoggingCreateStream: Properties: PolicyName: !Sub '${AWS::StackName}-iam-policy-logging-create-stream' PolicyDocument: Statement: - Action: - logs:CreateLogStream - logs:PutLogEvents Effect: Allow Resource: - '*' Version: '2012-10-17' Roles: - !Ref IamRoleInitPostgres - !Ref IamRoleLambda Type: AWS::IAM::Policy IamPolicyPassRole: Properties: PolicyName: !Sub '${AWS::StackName}-iam-policy-pass-role' PolicyDocument: Statement: - Action: - iam:PassRole Effect: Allow Resource: - '*' Version: '2012-10-17' Roles: - !Ref IamRoleLambda Type: AWS::IAM::Policy IamPolicyRds: Properties: PolicyName: !Sub '${AWS::StackName}-iam-policy-rds' PolicyDocument: Statement: - Action: - rds:ModifyDBCluster Effect: Allow Resource: - '*' Version: '2012-10-17' Roles: - !Ref IamRoleLambda Type: AWS::IAM::Policy IamPolicyTaskRunner: Properties: PolicyName: !Sub '${AWS::StackName}-iam-policy-task-runner' PolicyDocument: Statement: - Action: - ecs:DescribeTasks - ecs:RunTask - ecs:TagResource Effect: Allow Resource: - '*' Version: '2012-10-17' Roles: - !Ref IamRoleLambda Type: AWS::IAM::Policy # -- Logging ------------------------------------------------------------------ # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html # AWS Console: https://console.aws.amazon.com/cloudwatch/home?#logsV2:log-groups > Search for {stack} LogsLogGroupDbCluster: Condition: IfSingleDatabase Properties: LogGroupName: !Sub - '/aws/rds/cluster/${StackNameAsLower}-aurora-senzing-cluster/postgresql' - StackNameAsLower: !GetAtt LambdaRunnerStackNameAsLower.OutputString Type: AWS::Logs::LogGroup LogsLogGroupDbClusterCore: Condition: IfMultipleDatabases Properties: LogGroupName: !Sub - '/aws/rds/cluster/${StackNameAsLower}-aurora-senzing-core-cluster/postgresql' - StackNameAsLower: !GetAtt LambdaRunnerStackNameAsLower.OutputString Type: AWS::Logs::LogGroup LogsLogGroupDbClusterLibfeat: Condition: IfMultipleDatabases Properties: LogGroupName: !Sub - '/aws/rds/cluster/${StackNameAsLower}-aurora-senzing-libfeat-cluster/postgresql' - StackNameAsLower: !GetAtt LambdaRunnerStackNameAsLower.OutputString Type: AWS::Logs::LogGroup LogsLogGroupDbClusterRes: Condition: IfMultipleDatabases Properties: LogGroupName: !Sub - '/aws/rds/cluster/${StackNameAsLower}-aurora-senzing-res-cluster/postgresql' - StackNameAsLower: !GetAtt LambdaRunnerStackNameAsLower.OutputString Type: AWS::Logs::LogGroup LogsLogGroupLambdaRandomPassword: Properties: LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-lambda-random-password' Type: AWS::Logs::LogGroup LogsLogGroupLambdaRandomString: Properties: LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-lambda-random-string' Type: AWS::Logs::LogGroup LogsLogGroupLambdaRunTask: Properties: LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-lambda-run-task' Type: AWS::Logs::LogGroup LogsLogGroupLambdaRunTaskAndWait: Properties: LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-lambda-run-task-and-wait' Type: AWS::Logs::LogGroup LogsLogGroupLambdaSetRdbTimeoutAction: Properties: LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-lambda-set-rdb-timeout-action' Type: AWS::Logs::LogGroup LogsLogGroupLambdaStringToLower: Properties: LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-lambda-string-to-lower' Type: AWS::Logs::LogGroup LogsLogGroupMain: Properties: LogGroupName: !Sub - '/senzing/${StackName}/${AWS::StackName}' - StackName: !FindInMap [Constants, Stack, Name] Type: AWS::Logs::LogGroup # -- Cloud, subnets, routing -------------------------------------------------- # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html # AWS Console: https://console.aws.amazon.com/vpc/home?#vpcs > Search for {stack} Ec2Vpc: Properties: CidrBlock: !FindInMap - VpcCidrs - vpc - cidr EnableDnsHostnames: true EnableDnsSupport: true Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-vpc' Type: AWS::EC2::VPC # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html # AWS Console: https://console.aws.amazon.com/vpc/home?#subnets > Search for {stack} Ec2SubnetPrivate1: Properties: AvailabilityZone: !Select - '0' - !GetAZs Ref: AWS::Region CidrBlock: !FindInMap - VpcCidrs - privsubnet1 - cidr Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-subnet-private-1' VpcId: !Ref Ec2Vpc Type: AWS::EC2::Subnet Ec2SubnetPrivate2: Properties: AvailabilityZone: !Select - '1' - !GetAZs Ref: AWS::Region CidrBlock: !FindInMap - VpcCidrs - privsubnet2 - cidr Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-subnet-private-2' VpcId: !Ref Ec2Vpc Type: AWS::EC2::Subnet Ec2SubnetPublic1: Properties: AvailabilityZone: !Select - '0' - !GetAZs Ref: AWS::Region CidrBlock: !FindInMap - VpcCidrs - pubsubnet1 - cidr Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-subnet-public-1' VpcId: !Ref Ec2Vpc Type: AWS::EC2::Subnet Ec2SubnetPublic2: Properties: AvailabilityZone: !Select - '1' - !GetAZs Ref: AWS::Region CidrBlock: !FindInMap - VpcCidrs - pubsubnet2 - cidr Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-subnet-public-2' VpcId: !Ref Ec2Vpc Type: AWS::EC2::Subnet # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-eip.html # AWS Console: https://console.aws.amazon.com/vpc/home?#Addresses: > Search for {stack} Ec2Eip: Properties: Domain: vpc Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-eip' Type: AWS::EC2::EIP # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-natgateway.html # AWS Console: https://console.aws.amazon.com/vpc/home?#NatGateways: > Search for {stack} Ec2NatGateway: Properties: AllocationId: !GetAtt Ec2Eip.AllocationId SubnetId: !Ref Ec2SubnetPublic1 Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-nat-gateway' Type: AWS::EC2::NatGateway # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html # AWS Console: https://console.aws.amazon.com/vpc/home?#SecurityGroups > Search for {stack} Ec2SecurityGroupInternal: Properties: GroupDescription: !Sub '${AWS::StackName} - ECS internal open ports.' SecurityGroupEgress: - CidrIp: 0.0.0.0/0 Description: All IpProtocol: '-1' SecurityGroupIngress: - CidrIp: !FindInMap - VpcCidrs - vpc - cidr Description: PostgreSQL FromPort: 5432 IpProtocol: tcp ToPort: 5432 Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-security-group-internal' VpcId: !Ref Ec2Vpc Type: AWS::EC2::SecurityGroup Ec2SecurityGroupLambdaRunner: Properties: GroupDescription: !Sub '${AWS::StackName} - Lambda open ports.' SecurityGroupEgress: - CidrIp: 0.0.0.0/0 Description: All IpProtocol: '-1' Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-security-group-lambda-runner' VpcId: !Ref Ec2Vpc Type: AWS::EC2::SecurityGroup # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-internetgateway.html # AWS Console: https://console.aws.amazon.com/vpc/home?#igws > Search for {stack} Ec2InternetGateway: Properties: Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-internet-gateway' Type: AWS::EC2::InternetGateway # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc-gateway-attachment.html # AWS Console: https://console.aws.amazon.com/vpc/home?#igws > Search for {stack} > State & VPI ID Ec2VpcGatewayAttachment: Properties: InternetGatewayId: !Ref Ec2InternetGateway VpcId: !Ref Ec2Vpc Type: AWS::EC2::VPCGatewayAttachment # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route-table.html # AWS Console: https://console.aws.amazon.com/vpc/home?#RouteTables > Search for {stack} Ec2RouteTablePrivate: Properties: Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-route-table-private' VpcId: !Ref Ec2Vpc Type: AWS::EC2::RouteTable Ec2RouteTablePublic: Properties: Tags: - Key: Name Value: !Sub '${AWS::StackName}-ec2-route-table-public' VpcId: !Ref Ec2Vpc Type: AWS::EC2::RouteTable # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route.html # AWS Console: https://console.aws.amazon.com/vpc/home?#RouteTables > {name} > "Routes" tab Ec2RoutePrivate: Properties: DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref Ec2NatGateway RouteTableId: !Ref Ec2RouteTablePrivate Type: AWS::EC2::Route Ec2RoutePublic: DependsOn: - Ec2VpcGatewayAttachment Properties: DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref Ec2InternetGateway RouteTableId: !Ref Ec2RouteTablePublic Type: AWS::EC2::Route # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet-route-table-assoc.html # AWS Console: https://console.aws.amazon.com/vpc/home?#RouteTables > {name} > "Subnet Associations" tab Ec2SubnetRouteTableAssociationPrivate1: Properties: RouteTableId: !Ref Ec2RouteTablePrivate SubnetId: !Ref Ec2SubnetPrivate1 Type: AWS::EC2::SubnetRouteTableAssociation Ec2SubnetRouteTableAssociationPrivate2: Properties: RouteTableId: !Ref Ec2RouteTablePrivate SubnetId: !Ref Ec2SubnetPrivate2 Type: AWS::EC2::SubnetRouteTableAssociation Ec2SubnetRouteTableAssociationPublic1: Properties: RouteTableId: !Ref Ec2RouteTablePublic SubnetId: !Ref Ec2SubnetPublic1 Type: AWS::EC2::SubnetRouteTableAssociation Ec2SubnetRouteTableAssociationPublic2: Properties: RouteTableId: !Ref Ec2RouteTablePublic SubnetId: !Ref Ec2SubnetPublic2 Type: AWS::EC2::SubnetRouteTableAssociation # -- Database ----------------------------------------------------------------- # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsubnet-group.html # AWS Console: https://console.aws.amazon.com/rds/home#db-subnet-groups-list: > Search for {stack} RdsDbSubnetGroup: Properties: DBSubnetGroupDescription: !Sub '${AWS::StackName}-db-subnet-description' DBSubnetGroupName: !Sub '${AWS::StackName}-db-subnet' SubnetIds: - !Ref Ec2SubnetPrivate1 - !Ref Ec2SubnetPrivate2 Tags: - Key: Name Value: !Sub '${AWS::StackName}-rds-db-subnet-group' Type: AWS::RDS::DBSubnetGroup # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbclusterparametergroup.html # AWS Console: https://console.aws.amazon.com/rds/home?#parameter-groups: > Search for {stack} RdsDbClusterParameterGroup: Properties: Description: !Sub '${AWS::StackName}-rds-db-cluster-parameter-group-description' Family: aurora-postgresql13 Parameters: autovacuum_max_workers: 5 enable_seqscan: 0 pglogical.synchronous_commit: 0 synchronous_commit: 'off' Tags: - Key: Name Value: !Sub '${AWS::StackName}-rds-db-cluster-parameter-group' Type: AWS::RDS::DBClusterParameterGroup # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html # AWS Console: https://console.aws.amazon.com/rds/home?#databases: > Search for {stack} RdsDbCluster: Condition: IfSingleDatabase DependsOn: - LogsLogGroupDbCluster - RdsDbSubnetGroup Properties: AvailabilityZones: - !GetAtt Ec2SubnetPrivate1.AvailabilityZone - !GetAtt Ec2SubnetPrivate2.AvailabilityZone DatabaseName: G2 DBClusterIdentifier: !Sub '${AWS::StackName}-aurora-senzing-cluster' DBClusterParameterGroupName: Ref: RdsDbClusterParameterGroup # FIXME: Tricky code. See https://console.aws.amazon.com/support/home#/case/?displayId=7725760511 DBSubnetGroupName: !Sub - '${StackNameAsLower}-db-subnet' - StackNameAsLower: !GetAtt LambdaRunnerStackNameAsLower.OutputString DeletionProtection: false EnableHttpEndpoint: true Engine: aurora-postgresql EngineMode: serverless EngineVersion: '13.12' MasterUsername: !FindInMap [Constants, Database, Username] MasterUserPassword: !GetAtt LambdaRunnerDbPassword.RandomString ScalingConfiguration: AutoPause: true MaxCapacity: 192 MinCapacity: 2 SecondsUntilAutoPause: 3600 # FIXME: Uncomment once 'TimeoutAction' is supported. # https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/298 # https://console.aws.amazon.com/support/home#/case/?displayId=7705681311&language=en # TimeoutAction: ForceApplyCapacityChange StorageEncrypted: true Tags: - Key: Name Value: !Sub '${AWS::StackName}-rds-db-cluster' VpcSecurityGroupIds: - !Ref Ec2SecurityGroupInternal Type: AWS::RDS::DBCluster RdsDbClusterCore: Condition: IfMultipleDatabases DependsOn: - LogsLogGroupDbClusterCore - RdsDbSubnetGroup Properties: AvailabilityZones: - !GetAtt Ec2SubnetPrivate1.AvailabilityZone - !GetAtt Ec2SubnetPrivate2.AvailabilityZone DatabaseName: G2 DBClusterIdentifier: !Sub '${AWS::StackName}-aurora-senzing-core-cluster' DBClusterParameterGroupName: Ref: RdsDbClusterParameterGroup # FIXME: Tricky code. See https://console.aws.amazon.com/support/home#/case/?displayId=7725760511 DBSubnetGroupName: !Sub - '${StackNameAsLower}-db-subnet' - StackNameAsLower: !GetAtt LambdaRunnerStackNameAsLower.OutputString DeletionProtection: false EnableHttpEndpoint: true Engine: aurora-postgresql EngineMode: serverless EngineVersion: '13.12' MasterUsername: !FindInMap [Constants, Database, Username] MasterUserPassword: !GetAtt LambdaRunnerDbPassword.RandomString ScalingConfiguration: AutoPause: true MaxCapacity: 192 MinCapacity: 2 SecondsUntilAutoPause: 3600 # FIXME: Uncomment once 'TimeoutAction' is supported. # https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/298 # https://console.aws.amazon.com/support/home#/case/?displayId=7705681311&language=en # TimeoutAction: ForceApplyCapacityChange StorageEncrypted: true Tags: - Key: Name Value: !Sub '${AWS::StackName}-rds-db-core-cluster' VpcSecurityGroupIds: - !Ref Ec2SecurityGroupInternal Type: AWS::RDS::DBCluster RdsDbClusterLibfeat: Condition: IfMultipleDatabases DependsOn: - LogsLogGroupDbClusterLibfeat - RdsDbSubnetGroup Properties: AvailabilityZones: - !GetAtt Ec2SubnetPrivate1.AvailabilityZone - !GetAtt Ec2SubnetPrivate2.AvailabilityZone DatabaseName: G2 DBClusterIdentifier: !Sub '${AWS::StackName}-aurora-senzing-libfeat-cluster' DBClusterParameterGroupName: Ref: RdsDbClusterParameterGroup # FIXME: Tricky code. See https://console.aws.amazon.com/support/home#/case/?displayId=7725760511 DBSubnetGroupName: !Sub - '${StackNameAsLower}-db-subnet' - StackNameAsLower: !GetAtt LambdaRunnerStackNameAsLower.OutputString DeletionProtection: false EnableHttpEndpoint: true Engine: aurora-postgresql EngineMode: serverless EngineVersion: '13.12' MasterUsername: !FindInMap [Constants, Database, Username] MasterUserPassword: !GetAtt LambdaRunnerDbPassword.RandomString ScalingConfiguration: AutoPause: true MaxCapacity: 192 MinCapacity: 2 SecondsUntilAutoPause: 3600 # FIXME: Uncomment once 'TimeoutAction' is supported. # https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/298 # https://console.aws.amazon.com/support/home#/case/?displayId=7705681311&language=en # TimeoutAction: ForceApplyCapacityChange StorageEncrypted: true Tags: - Key: Name Value: !Sub '${AWS::StackName}-rds-db-libfeat-cluster' VpcSecurityGroupIds: - !Ref Ec2SecurityGroupInternal Type: AWS::RDS::DBCluster RdsDbClusterRes: Condition: IfMultipleDatabases DependsOn: - LogsLogGroupDbClusterRes - RdsDbSubnetGroup Properties: AvailabilityZones: - !GetAtt Ec2SubnetPrivate1.AvailabilityZone - !GetAtt Ec2SubnetPrivate2.AvailabilityZone DatabaseName: G2 DBClusterIdentifier: !Sub '${AWS::StackName}-aurora-senzing-res-cluster' DBClusterParameterGroupName: Ref: RdsDbClusterParameterGroup # FIXME: Tricky code. See https://console.aws.amazon.com/support/home#/case/?displayId=7725760511 DBSubnetGroupName: !Sub - '${StackNameAsLower}-db-subnet' - StackNameAsLower: !GetAtt LambdaRunnerStackNameAsLower.OutputString DeletionProtection: false EnableHttpEndpoint: true Engine: aurora-postgresql EngineMode: serverless EngineVersion: '13.12' MasterUsername: !FindInMap [Constants, Database, Username] MasterUserPassword: !GetAtt LambdaRunnerDbPassword.RandomString ScalingConfiguration: AutoPause: true MaxCapacity: 384 MinCapacity: 2 SecondsUntilAutoPause: 3600 # FIXME: Uncomment once 'TimeoutAction' is supported. # https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/298 # https://console.aws.amazon.com/support/home#/case/?displayId=7705681311&language=en # TimeoutAction: ForceApplyCapacityChange StorageEncrypted: true Tags: - Key: Name Value: !Sub '${AWS::StackName}-rds-db-res-cluster' VpcSecurityGroupIds: - !Ref Ec2SecurityGroupInternal Type: AWS::RDS::DBCluster # -- ECS Cluster -------------------------------------------------------------- # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html # AWS Console: https://console.aws.amazon.com/ecs/home?#/clusters > Search for {stack} EcsCluster: Properties: ClusterName: !Sub '${AWS::StackName}-cluster' Tags: - Key: Name Value: !Sub '${AWS::StackName}-ecs-cluster' Type: AWS::ECS::Cluster # -- Wait conditions ---------------------------------------------------------- DbClusterCoreReady: Metadata: DBClusterCoreReady: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterCore.Endpoint.Address Type: AWS::CloudFormation::WaitConditionHandle DbClusterLibfeatReady: Metadata: DBClusterCoreReady: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterLibfeat.Endpoint.Address Type: AWS::CloudFormation::WaitConditionHandle DbClusterResReady: Metadata: DBClusterCoreReady: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterRes.Endpoint.Address Type: AWS::CloudFormation::WaitConditionHandle # -- LambdaFunction ----------------------------------------------------------- # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html # AWS Console: https://console.aws.amazon.com/lambda/home?#/functions > Search for {stack} LambdaFunctionRandomString: DependsOn: - Ec2SubnetRouteTableAssociationPrivate1 - Ec2SubnetRouteTableAssociationPrivate2 Properties: Code: ZipFile: | #!/usr/bin/env python3 import cfnresponse import json import logging import random import string import traceback logger = logging.getLogger() logger.setLevel(logging.INFO) def handler(event, context): result = cfnresponse.SUCCESS response_data = {} try: logger.info("Event: {0}".format(json.dumps(event))) if event['RequestType'] in ['Create', 'Update']: properties = event.get('ResourceProperties', {}) length = int(properties.get('Length', 0)) response_data["RandomString"] = ''.join(random.choices(string.ascii_letters + string.digits, k=length)) except Exception as e: logger.error(e) traceback.print_exc() result = cfnresponse.FAILED finally: cfnresponse.send(event, context, result, response_data) Description: Generate string of random characters. FunctionName: !Sub '${AWS::StackName}-lambda-random-string' Handler: index.handler Role: !GetAtt IamRoleLambda.Arn Runtime: python3.8 Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-random-string' Timeout: 600 Type: AWS::Lambda::Function LambdaFunctionRunTaskAndWait: DependsOn: - Ec2SubnetRouteTableAssociationPrivate1 - Ec2SubnetRouteTableAssociationPrivate2 Properties: Code: ZipFile: | #!/usr/bin/env python3 import boto3 import cfnresponse import datetime import json import logging import traceback from json import JSONEncoder logger = logging.getLogger() logger.setLevel(logging.INFO) class DateTimeEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, (datetime.date, datetime.datetime)): return obj.isoformat() def handler(event, context): result = cfnresponse.SUCCESS response = {} try: logger.info("Event: {0}".format(json.dumps(event))) if event['RequestType'] in ['Create', 'Update']: properties = event.get('ResourceProperties', {}) run_task_parameters = properties.get('RunTaskParameters', {}) # Change strings to integers. numbers = [ "count", ] for number in numbers: if number in run_task_parameters: run_task_parameters[number] = int(run_task_parameters[number]) # Make AWS ECS request. # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html#ECS.Client.run_task ecs = boto3.client('ecs') response = ecs.run_task(**run_task_parameters) # Wait for completion. # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html#waiters task_list = response.get('tasks', []) if len(task_list) > 0: taskArn = task_list[0].get('taskArn', None) cluster = properties.get('ClusterId', None) if not [x for x in (taskArn, cluster) if x is None]: waiter = ecs.get_waiter('tasks_stopped') waiter.wait( cluster=cluster, tasks=[taskArn], ) response['describe_task'] = ecs.describe_tasks( cluster=cluster, tasks=[taskArn], ) logger.info("describe_task response: {0}".format(json.dumps(response.get('describe_task', {}), cls=DateTimeEncoder))) # test for failures and log any fail_list = response.get('failures', []) if len(fail_list) > 0: for item in fail_list: logger.info(f"Task failed to run. ARN: {item.get('arn', 'unknown')}") logger.info(f" Reason: {item.get('reason', 'unknown')}") logger.info(f" Details: {item.get('detail','none')}") exit_code = response.get('describe_task', {}).get('tasks', [{}])[0].get('containers', [{}])[0].get('exitCode', 99) if exit_code != 0: result = cfnresponse.FAILED logger.info("Response: {0}".format(json.dumps(response, cls=DateTimeEncoder))) except Exception as e: logger.error(e) traceback.print_exc() result = cfnresponse.FAILED finally: cfnresponse.send(event, context, result, {}) Description: Runs an ECS task and waits until completion. FunctionName: !Sub '${AWS::StackName}-lambda-run-task-and-wait' Handler: index.handler Role: !GetAtt IamRoleLambda.Arn Runtime: python3.8 Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-run-task-and-wait' Timeout: 600 Type: AWS::Lambda::Function LambdaFunctionSenzingEngineConfigurationJson: Properties: Code: ZipFile: | #!/usr/bin/env python3 import cfnresponse import json import logging import traceback logger = logging.getLogger() logger.setLevel(logging.INFO) def handler(event, context): result = cfnresponse.SUCCESS response_data = {} try: logger.info("Event: {0}".format(json.dumps(event))) if event['RequestType'] in ['Create', 'Update']: properties = event.get('ResourceProperties', {}) database_name = properties.get('DatabaseName', '') database_user_name = properties.get('DatabaseUsername', '') database_password = properties.get('DatabasePassword', '') database_host_core = properties.get('DatabaseHostCore', '') database_port_core = properties.get('DatabasePortCore', '') database_host_res = properties.get('DatabaseHostRes', '') database_port_res = properties.get('DatabasePortRes', '') database_host_libfeat = properties.get('DatabaseHostLibfeat', '') database_port_libfeat = properties.get('DatabasePortLibfeat', '') db_config = 'Multiple' if database_host_core == database_host_res == database_host_libfeat: db_config = 'Single' logger.info(f"db_config={db_config}") license_string = properties.get('SenzingLicenseAsBase64', '') if db_config == 'Single': response_data["ConfigJSON"] = ( '{' ' "PIPELINE": {' ' "CONFIGPATH": "/etc/opt/senzing",' f' "LICENSESTRINGBASE64": "{license_string}",' ' "RESOURCEPATH": "/opt/senzing/g2/resources",' ' "SUPPORTPATH": "/opt/senzing/data"' ' },' ' "SQL": {' ' "BACKEND": "SQL",' f' "CONNECTION":"postgresql://{database_user_name}:{database_password}@{database_host_core}:{database_port_core}:{database_name}"' ' }' '}') else: response_data["ConfigJSON"] = ( '{' ' "PIPELINE": {' ' "CONFIGPATH": "/etc/opt/senzing",' f' "LICENSESTRINGBASE64": "{license_string}",' ' "RESOURCEPATH": "/opt/senzing/g2/resources",' ' "SUPPORTPATH": "/opt/senzing/data"' ' },' ' "SQL": {' ' "BACKEND": "HYBRID",' f' "CONNECTION":"postgresql://{database_user_name}:{database_password}@{database_host_core}:{database_port_core}:{database_name}"' ' },' ' "C1": {' ' "CLUSTER_SIZE": "1",' f' "DB_1": "postgresql://{database_user_name}:{database_password}@{database_host_res}:{database_port_res}:{database_name}"' ' },' ' "C2": {' ' "CLUSTER_SIZE": "1",' f' "DB_1": "postgresql://{database_user_name}:{database_password}@{database_host_libfeat}:{database_port_libfeat}:{database_name}"' ' },' ' "HYBRID": {' ' "LIB_FEAT": "C2",' ' "LIB_FEAT_HKEY": "C2",' ' "RES_FEAT": "C1",' ' "RES_FEAT_EKEY": "C1",' ' "RES_FEAT_LKEY": "C1",' ' "RES_FEAT_STAT": "C1"' ' }' '}') except Exception as e: logger.error(e) traceback.print_exc() result = cfnresponse.FAILED finally: cfnresponse.send(event, context, result, response_data) Description: Constructs the Senzing Engine configuration JSON. FunctionName: !Sub '${AWS::StackName}-lambda-senzing-engine-configuration-json' Handler: index.handler Role: !GetAtt IamRoleLambda.Arn Runtime: python3.8 Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-senzing-engine-configuration-json' Type: AWS::Lambda::Function LambdaFunctionSetRdbTimeoutAction: Properties: Code: ZipFile: | #!/usr/bin/env python3 import boto3 import cfnresponse import json import logging import traceback logger = logging.getLogger() logger.setLevel(logging.INFO) def handler(event, context): result = cfnresponse.SUCCESS response_data = {} try: logger.info("Event: {0}".format(json.dumps(event))) if event['RequestType'] in ['Create', 'Update']: properties = event.get('ResourceProperties', {}) region = properties.get('Region') cluster_id = properties.get('DBClusterIdentifier') if cluster_id and region: client = boto3.client('rds', region_name=region) response_data = client.modify_db_cluster( DBClusterIdentifier = cluster_id, ScalingConfiguration = { 'TimeoutAction': 'ForceApplyCapacityChange', 'SecondsBeforeTimeout': 60 }, ) else: logger.error("Properties not provided in ResourceProperties") except Exception as e: logger.error(e) traceback.print_exc() result = cfnresponse.FAILED finally: cfnresponse.send(event, context, result, {}) logger.info(response_data) Description: Sets the RDB TimeoutAction to ForceApplyCapacityChange. FunctionName: !Sub '${AWS::StackName}-lambda-set-rdb-timeout-action' Handler: index.handler Role: !GetAtt IamRoleLambda.Arn Runtime: python3.8 Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-set-rdb-timeout-action' Timeout: 30 Type: AWS::Lambda::Function LambdaFunctionStringToLower: Properties: Code: ZipFile: | #!/usr/bin/env python3 import cfnresponse import json import logging import traceback logger = logging.getLogger() logger.setLevel(logging.INFO) def handler(event, context): result = cfnresponse.SUCCESS response_data = {} try: logger.info("Event: {0}".format(json.dumps(event))) if event['RequestType'] in ['Create', 'Update']: properties = event.get('ResourceProperties', {}) input_string = properties.get('InputString', '') response_data["OutputString"] = input_string.lower() except Exception as e: logger.error(e) traceback.print_exc() result = cfnresponse.FAILED finally: cfnresponse.send(event, context, result, response_data) Description: Performs string.lower() FunctionName: !Sub '${AWS::StackName}-lambda-string-to-lower' Handler: index.handler Role: !GetAtt IamRoleLambda.Arn Runtime: python3.8 Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-string-to-lower' Type: AWS::Lambda::Function # -- Run Lambda jobs ---------------------------------------------------------- # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html # AWS Console: FIXME: none? LambdaRunnerDbPassword: Properties: Length: 16 ServiceToken: !GetAtt LambdaFunctionRandomString.Arn Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-runner-db-password' Type: Custom::LambdaRunnerDbPassword LambdaRunnerInitPostgresConfiguration: DependsOn: - DbClusterCoreReady - DbClusterLibfeatReady - DbClusterResReady Properties: ClusterId: !Ref EcsCluster RunTaskParameters: # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html#ECS.Client.run_task cluster: !Ref EcsCluster count: 1 launchType: FARGATE networkConfiguration: awsvpcConfiguration: assignPublicIp: DISABLED securityGroups: - !Ref Ec2SecurityGroupLambdaRunner subnets: - !Ref Ec2SubnetPrivate1 - !Ref Ec2SubnetPrivate2 platformVersion: 1.4.0 tags: - key: Name value: !Sub '${AWS::StackName}-lambda-runner-init-postgres-configuration-run-task-parameters' taskDefinition: !Ref EcsTaskDefinitionInitPostgresConfiguration ServiceToken: !GetAtt LambdaFunctionRunTaskAndWait.Arn Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-runner-init-postgres-configuration' Type: Custom::LambdaRunnerInitPostgresConfiguration LambdaRunnerSenzingEngineConfigurationJson: Properties: DatabaseHostCore: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterCore.Endpoint.Address DatabaseHostLibfeat: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterLibfeat.Endpoint.Address DatabaseHostRes: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterRes.Endpoint.Address DatabaseName: !FindInMap [Constants, Database, Name] DatabasePassword: !GetAtt LambdaRunnerDbPassword.RandomString DatabasePortCore: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Port - !GetAtt RdsDbClusterCore.Endpoint.Port DatabasePortLibfeat: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Port - !GetAtt RdsDbClusterLibfeat.Endpoint.Port DatabasePortRes: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Port - !GetAtt RdsDbClusterRes.Endpoint.Port DatabaseUsername: !FindInMap [Constants, Database, Username] SenzingLicenseAsBase64: '' ServiceToken: !GetAtt LambdaFunctionSenzingEngineConfigurationJson.Arn Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-runner-senzing-engine-configuration-json' Type: Custom::LambdaRunnerSenzingEngineConfigurationJson LambdaRunnerSetPostgresTimeoutAction: Condition: IfSingleDatabase Properties: DBClusterIdentifier: !Ref RdsDbCluster Region: !Ref AWS::Region ServiceToken: !GetAtt LambdaFunctionSetRdbTimeoutAction.Arn Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-runner-set-postgres-timeout-action' Type: Custom::LambdaRunnerSetPostgresTimeoutAction LambdaRunnerSetPostgresCoreTimeoutAction: Condition: IfMultipleDatabases Properties: DBClusterIdentifier: !Ref RdsDbClusterCore Region: !Ref AWS::Region ServiceToken: !GetAtt LambdaFunctionSetRdbTimeoutAction.Arn Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-runner-set-postgres-core-timeout-action' Type: Custom::LambdaRunnerSetPostgresCoreTimeoutAction LambdaRunnerSetPostgresLibfeatTimeoutAction: Condition: IfMultipleDatabases Properties: DBClusterIdentifier: !Ref RdsDbClusterLibfeat Region: !Ref AWS::Region ServiceToken: !GetAtt LambdaFunctionSetRdbTimeoutAction.Arn Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-runner-set-postgres-libfeat-timeout-action' Type: Custom::LambdaRunnerSetPostgresLibfeatTimeoutAction LambdaRunnerSetPostgresResTimeoutAction: Condition: IfMultipleDatabases Properties: DBClusterIdentifier: !Ref RdsDbClusterRes Region: !Ref AWS::Region ServiceToken: !GetAtt LambdaFunctionSetRdbTimeoutAction.Arn Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-runner-set-postgres-res-timeout-action' Type: Custom::LambdaRunnerSetPostgresResTimeoutAction LambdaRunnerStackNameAsLower: Properties: InputString: !Sub '${AWS::StackName}' ServiceToken: !GetAtt LambdaFunctionStringToLower.Arn Tags: - Key: Name Value: !Sub '${AWS::StackName}-lambda-runner-stack-name-as-lower' Type: Custom::LambdaRunnerStackNameAsLower # -- EcsTaskDefinition -------------------------------------------------------- # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html # AWS Console: https://console.aws.amazon.com/ecs/home?#/taskDefinitions > Search for {stack} EcsTaskDefinitionInitPostgresConfiguration: Properties: ContainerDefinitions: - Environment: - Name: SENZING_TOOLS_ENGINE_CONFIGURATION_JSON Value: !GetAtt LambdaRunnerSenzingEngineConfigurationJson.ConfigJSON - Name: LC_CTYPE Value: en_US.utf8 - Name: SENZING_SUBCOMMAND Value: mandatory - Name: SENZING_DEBUG Value: False Essential: true Image: !FindInMap [Constants, Images, InitPostgresql] LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref LogsLogGroupMain awslogs-region: !Ref AWS::Region awslogs-stream-prefix: job Name: postgresinit Privileged: false ReadonlyRootFilesystem: false Cpu: '512' ExecutionRoleArn: !GetAtt IamRoleInitPostgres.Arn Family: !Sub '${AWS::StackName}-task-definition-init-postgres-configuration' Memory: '1024' NetworkMode: awsvpc RequiresCompatibilities: - FARGATE Tags: - Key: Name Value: !Sub '${AWS::StackName}-ecs-task-definition-init-postgres-configuration' Type: AWS::ECS::TaskDefinition # ----------------------------------------------------------------------------- # Outputs # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html # ----------------------------------------------------------------------------- Outputs: # AWS Console: https://console.aws.amazon.com/cloudformation/home?#/stacks > {stack} > Outputs AccountID: Description: 'The AWS AccountID. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#accountid' Export: Name: !Sub '${AWS::StackName}-account-id' Value: !Sub '${AWS::AccountId}' DatabaseHostCore: Description: 'Hostname of the Core database. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databasehostcore' Export: Name: !Sub '${AWS::StackName}-database-host-core' Value: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterCore.Endpoint.Address DatabaseHostLibfeat: Description: 'Hostname of the Libfeat database. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databasehostlibfeat' Export: Name: !Sub '${AWS::StackName}-database-host-libfeat' Value: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterLibfeat.Endpoint.Address DatabaseHostRes: Description: 'Hostname of the Res database. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databasehostres' Export: Name: !Sub '${AWS::StackName}-database-host-res' Value: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Address - !GetAtt RdsDbClusterRes.Endpoint.Address DatabaseName: Description: 'The name of the database. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databasename' Export: Name: !Sub '${AWS::StackName}-database-name' Value: !FindInMap [Constants, Database, Name] DatabasePassword: Description: 'The randomly generated password for the administrative user (DatabaseUsername). Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databasepassword' Export: Name: !Sub '${AWS::StackName}-database-password' Value: !GetAtt LambdaRunnerDbPassword.RandomString DatabasePortCore: Description: 'The port number that will accept connections on the Core database. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databaseportcore' Export: Name: !Sub '${AWS::StackName}-database-port-core' Value: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Port - !GetAtt RdsDbClusterCore.Endpoint.Port DatabasePortLibfeat: Description: 'The port number that will accept connections on the Libfeat database. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databaseportlibfeat' Export: Name: !Sub '${AWS::StackName}-database-port-libfeat' Value: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Port - !GetAtt RdsDbClusterLibfeat.Endpoint.Port DatabasePortRes: Description: 'The port number that will accept connections on the Res database. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databaseportres' Export: Name: !Sub '${AWS::StackName}-database-port-res' Value: !If - IfSingleDatabase - !GetAtt RdsDbCluster.Endpoint.Port - !GetAtt RdsDbClusterRes.Endpoint.Port DatabaseUsername: Description: 'The administrative user name. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#databaseusername' Export: Name: !Sub '${AWS::StackName}-database-username' Value: !FindInMap [Constants, Database, Username] Ec2InternetGateway: Description: 'Internet Gateway. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#ec2internetgateway' Export: Name: !Sub '${AWS::StackName}-ec2-internet-gateway' Value: !Ref Ec2InternetGateway Ec2SecurityGroupInternal: Description: 'The security group used internally. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#ec2securitygroupinternal' Export: Name: !Sub '${AWS::StackName}-ec2-security-group-internal' Value: !Ref Ec2SecurityGroupInternal Ec2Vpc: Description: 'The ID of the VPC. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#ec2vpc' Export: Name: !Sub '${AWS::StackName}-ec2-VpcId' Value: !Ref Ec2Vpc Ec2VpcCidrBlock: Description: 'The CidrBloc of the VPC. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#ec2vpccidrblock' Export: Name: !Sub '${AWS::StackName}-ec2-VpcId-cidrblock' Value: !GetAtt Ec2Vpc.CidrBlock ImageVersions: Description: 'List of Docker images used in this stack.' Export: Name: !Sub '${AWS::StackName}-image-versions' Value: !Join - '' - - 'InitPostgresql:' - !FindInMap [Constants, Images, InitPostgresql] SubnetPrivate1: Description: 'The ID of private subnet 1. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#subnetprivate1' Export: Name: !Sub '${AWS::StackName}-subnet-private-1' Value: !Ref Ec2SubnetPrivate1 SubnetPrivate2: Description: 'The ID of private subnet 2. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#subnetprivate2' Export: Name: !Sub '${AWS::StackName}-subnet-private-2' Value: !Ref Ec2SubnetPrivate2 SubnetPublic1: Description: 'The ID of public subnet 1. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#subnetpublic1' Export: Name: !Sub '${AWS::StackName}-subnet-public-1' Value: !Ref Ec2SubnetPublic1 SubnetPublic2: Description: 'The ID of public subnet 2. Help: https://hub.senzing.com/aws-cloudformation-database-cluster/#subnetpublic2' Export: Name: !Sub '${AWS::StackName}-subnet-public-2' Value: !Ref Ec2SubnetPublic2