AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::SecretsManager-2020-07-23 Description: 'CloudFormation Template to create Aurora Postgresql Serverless v2 DB Instance' ############################################################################### # Parameters ############################################################################### Parameters: ParentVPCStack: Description: 'Provide Stack name of parent VPC stack based on VPC-3AZs yaml template. Refer Cloudformation dashboard in AWS Console to get this.' Type: String MinLength: '1' MaxLength: '128' AllowedPattern: '^[a-zA-Z]+[0-9a-zA-Z\-]*$' ParentSSHBastionStack: Description: 'Provide Stack name of parent Amazon Linux bastion host stack based on VPC-SSH-Bastion yaml template. Refer Cloudformation dashboard in AWS Console to get this.' Type: String MinLength: '1' MaxLength: '128' AllowedPattern: '^[a-zA-Z]+[0-9a-zA-Z\-]*$' ParentDMSStack: Description: 'Optional. Provide DMS stack used to create replication instance. Refer Cloudformation dashboard in AWS Console to get this.' Type: String Default: "" DBName: Description: 'Database Name - must start with a letter. Only numbers, letters, and _ accepted. max length 64 characters.' Type: String MinLength: '1' MaxLength: '64' AllowedPattern: "^[a-zA-Z]+[0-9a-zA-Z_]*$" ConstraintDescription: 'Must start with a letter. Only numbers, letters, and _ accepted. Max length 64 characters.' DBPort: Description: 'TCP/IP Port for the Database Instance' Type: Number Default: 5432 ConstraintDescription: 'Must be in the range [1115-65535]' MinValue: 1115 MaxValue: 65535 DBUsername: Description: 'Database admin username' Type: String Default: 'dbadmin' MinLength: '1' MaxLength: '16' AllowedPattern: "^[a-zA-Z]+[0-9a-zA-Z_]*$" ConstraintDescription: 'Must start with a letter. Only numbers, letters, and _ accepted. Max length 16 characters.' DBEngineVersion: Description: 'Select Database Engine Version' Type: String Default: 14.4 AllowedValues: ['13.7', '14.4'] MinCapacity: Description: 'Enter minimum Aurora Capacity Units (ACUs). 1 ACU provides 2 GiB of memory and corresponding compute and networking. Valid ranges are from 0.5 to 128 in increments of 0.5' Type: String AllowedPattern: ([0-9]?(\.(0|5))?|[1-8][0-9](\.(0|5))?|9[0-9]|1[01][0-9](\.(0|5))?|12[0-7](\.(0|5))?|128) ConstraintDescription: 'Only values from 0.5 to 128, in increments of 0.5' MaxCapacity: Description: 'Enter maximum Aurora Capacity Units (ACUs). 1 ACU provides 2 GiB of memory and corresponding compute and networking. Valid ranges are from 1 to 128 in increments of 0.5' Type: String AllowedPattern: ([1-9]?(\.(0|5))?|[1-8][0-9](\.(0|5))?|9[0-9]|1[01][0-9](\.(0|5))?|12[0-7](\.(0|5))?|128) ConstraintDescription: 'Only values from 1 to 128, in increments of 0.5' DBSnapshotName: Description: 'Optional. DB Snapshot ID to restore database. Leave this blank if you are not restoring from a snapshot.' Type: String Default: "" EnvironmentStage: Type: String Description: 'The environment tag is used to designate the Environment Stage of the associated AWS resource.' AllowedValues: - dev - test - pre-prod - prod Default: dev NotificationList: Type: String Description: 'The Email notification list is used to configure a SNS topic for sending cloudwatch alarm and RDS Event notifications.' AllowedPattern: '^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$' ConstraintDescription: 'Provide a valid email address.' ########################################################################### # Optional tags that will be added to all resources that support tags ########################################################################### Application: Type: String Default: '' Description: 'Optional. The Application tag is used to designate the application of the associated AWS resource. In this capacity application does not refer to an installed software component, but rather the overall business application that the resource supports.' #AllowedPattern: "^[a-zA-Z]+[a-zA-Z ]+[a-zA-Z]+$" #ConstraintDescription: provide a valid application name containing only letters and spaces ApplicationVersion: Type: String Description: 'Optional. The ApplicationVersion tag is used to designate the specific version of the application. Format should be in the Pattern - "#.#.#"' Default: '' #AllowedPattern: '^[a-zA-Z0-9\.\-]+$' #ConstraintDescription: provide a valid application version ProjectCostCenter: Type: String Default: '' Description: 'Optional. The ProjectCostCenter tag is used to designate the cost center associated with the project of the given AWS resource.' #AllowedPattern: "^[a-zA-Z0-9]+$" #ConstraintDescription: provide a valid cost center ServiceOwnersEmailContact: Type: String Default: '' Description: 'Optional. The ServiceOwnersEmailContact tag is used to designate business owner(s) email address associated with the given AWS resource for sending outage or maintenance notifications.' #AllowedPattern: '^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$' !or '' #ConstraintDescription: provide a valid email address. Confidentiality: Type: String Default: '' Description: 'Optional. The Confidentiality tag is used to designate the confidentiality classification of the data that is associated with the resource.' #AllowedValues: # - public # - private # - confidential # - pii/phi Compliance: Type: String Default: '' Description: 'Optional. The Compliance tag is used to specify the Compliance level for the AWS resource.' #AllowedValues: # - hipaa # - sox # - fips # - other # - none ############################################################################### # Parameter groups ############################################################################### Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: 'Environment' Parameters: - EnvironmentStage - Label: default: 'DB Parameters' Parameters: - DBName - DBPort - DBUsername - MinCapacity - MaxCapacity - DBEngineVersion - DBSnapshotName - NotificationList - Label: default: 'Networking' Parameters: - ParentVPCStack - ParentSSHBastionStack - ParentDMSStack - Label: default: 'Optional Tags' Parameters: - Application - ApplicationVersion - ProjectCostCenter - ServiceOwnersEmailContact - Confidentiality - Compliance ############################################################################### # Mappings ############################################################################### Mappings: DBFamilyMap: "13.7": "family": "aurora-postgresql13" "14.4": "family": "aurora-postgresql14" ############################################################################### # Conditions ############################################################################### Conditions: IsUseDBSnapshot: !Not [!Equals [!Ref DBSnapshotName, ""]] IsNotUseDBSnapshot: !Not [Condition: IsUseDBSnapshot] IsProd: !Equals [!Ref EnvironmentStage, 'prod'] IsReplica: !Or [!Equals [!Ref EnvironmentStage, 'pre-prod'], Condition: IsProd] IsDMS: !Not [!Equals [!Ref ParentDMSStack, '']] IsApplication: !Not [!Equals [!Ref Application, '']] IsApplicationVersion: !Not [!Equals [!Ref ApplicationVersion, '']] IsProjectCostCenter: !Not [!Equals [!Ref ProjectCostCenter, '']] IsServiceOwnersEmailContact: !Not [!Equals [!Ref ServiceOwnersEmailContact, '']] IsConfidentiality: !Not [!Equals [!Ref Confidentiality, '']] IsCompliance: !Not [!Equals [!Ref Compliance, '']] ############################################################################### # Resources ############################################################################### Resources: MonitoringIAMRole: Type: AWS::IAM::Role Condition: IsProd Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "monitoring.rds.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole DBSNSTopic: Type: AWS::SNS::Topic Properties: KmsMasterKeyId: 'alias/aws/sns' Subscription: - Endpoint: !Ref NotificationList Protocol: email DBSubnetGroup: Type: 'AWS::RDS::DBSubnetGroup' Properties: DBSubnetGroupDescription: !Ref 'AWS::StackName' SubnetIds: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPrivate'}] ClusterSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: !Ref 'AWS::StackName' SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref DBPort ToPort: !Ref DBPort SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentSSHBastionStack}-BastionSecurityGroupID'} Description: 'Access to Bastion Host Security Group' - IpProtocol: tcp FromPort: !Ref DBPort ToPort: !Ref DBPort SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-SecretRotationLambdaSecurityGroup'} Description: 'Access to Lambda Security Group' VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'} Tags: - Key: Name Value: !Sub '${AWS::StackName}-AuroraClusterSecurityGroup' ClusterSecurityGroupIngress: Type: 'AWS::EC2::SecurityGroupIngress' Properties: GroupId: !GetAtt 'ClusterSecurityGroup.GroupId' IpProtocol: -1 SourceSecurityGroupId: !Ref ClusterSecurityGroup Description: 'Self Reference' ClusterSecurityGroupIngressDMS: Condition: IsDMS Type: 'AWS::EC2::SecurityGroupIngress' Properties: GroupId: !GetAtt 'ClusterSecurityGroup.GroupId' IpProtocol: tcp FromPort: !Ref DBPort ToPort: !Ref DBPort SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentDMSStack}-DMSSecurityGroupID'} Description: 'Access to DMS Security Group' RDSDBClusterParameterGroup: Type: AWS::RDS::DBClusterParameterGroup Properties: Description: !Join [ "- ", [ "Aurora PG Cluster Parameter Group for Cloudformation Stack ", !Ref DBName ] ] Family: !FindInMap [DBFamilyMap, !Ref DBEngineVersion, "family"] Parameters: rds.force_ssl: 1 DBParamGroup: Type: AWS::RDS::DBParameterGroup Properties: Description: !Join [ "- ", [ "Aurora PG Database Instance Parameter Group for Cloudformation Stack ", !Ref DBName ] ] Family: !FindInMap [DBFamilyMap, !Ref DBEngineVersion, "family"] Parameters: shared_preload_libraries: auto_explain,pg_stat_statements,pg_hint_plan,pgaudit log_statement: "ddl" log_connections: 1 log_disconnections: 1 log_lock_waits: 1 log_min_duration_statement: 5000 auto_explain.log_min_duration: 5000 auto_explain.log_verbose: 1 log_rotation_age: 1440 log_rotation_size: 102400 rds.log_retention_period: 10080 random_page_cost: 1 track_activity_query_size: 16384 idle_in_transaction_session_timeout: 7200000 statement_timeout: 7200000 search_path: '"$user",public' AuroraMasterSecret: Condition: IsNotUseDBSnapshot Type: AWS::SecretsManager::Secret Properties: Name: !Join ['/', [!Ref EnvironmentStage, 'aurora-pg', !Ref 'AWS::StackName']] Description: !Join ['', ['Aurora PostgreSQL Master User Secret ', 'for CloudFormation Stack ', !Ref 'AWS::StackName']] Tags: - Key: EnvironmentStage Value: !Ref EnvironmentStage - Key: DatabaseEngine Value: 'Aurora PostgreSQL' - Key: StackID Value: !Ref 'AWS::StackId' GenerateSecretString: SecretStringTemplate: !Join ['', ['{"username": "', !Ref DBUsername, '"}']] GenerateStringKey: "password" ExcludeCharacters: '"@/\;+%' PasswordLength: 32 SecretAuroraClusterAttachment: Condition: IsNotUseDBSnapshot Type: AWS::SecretsManager::SecretTargetAttachment Properties: SecretId: !Ref AuroraMasterSecret TargetId: !Ref AuroraDBCluster TargetType: AWS::RDS::DBCluster AuroraSecretResourcePolicy: Condition: IsNotUseDBSnapshot Type: AWS::SecretsManager::ResourcePolicy Properties: SecretId: !Ref AuroraMasterSecret ResourcePolicy: Version: "2012-10-17" Statement: - Effect: "Deny" Principal: AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" Action: "secretsmanager:DeleteSecret" Resource: "*" SecretRotationLambda: Condition: IsNotUseDBSnapshot Type: AWS::SecretsManager::RotationSchedule DependsOn: - SecretAuroraClusterAttachment - AuroraDBFirstInstance Properties: SecretId: !Ref AuroraMasterSecret HostedRotationLambda: RotationLambdaName: !Sub 'SecretsManager-SecretRotationFn-${AWS::StackName}' RotationType: 'PostgreSQLSingleUser' RotationRules: AutomaticallyAfterDays: 30 AuroraDBCluster: Type: AWS::RDS::DBCluster DeletionPolicy: Snapshot UpdateReplacePolicy: Snapshot Properties: Engine: aurora-postgresql EngineVersion: !Ref DBEngineVersion DatabaseName: !If [IsUseDBSnapshot, !Ref "AWS::NoValue", !Ref DBName] ServerlessV2ScalingConfiguration: MinCapacity: !Ref MinCapacity MaxCapacity: !Ref MaxCapacity Port: !Ref DBPort MasterUsername: !If [IsUseDBSnapshot, !Ref "AWS::NoValue", !Join ['', ['{{resolve:secretsmanager:', !Ref AuroraMasterSecret, ':SecretString:username}}' ]]] MasterUserPassword: !If [IsUseDBSnapshot, !Ref "AWS::NoValue", !Join ['', ['{{resolve:secretsmanager:', !Ref AuroraMasterSecret, ':SecretString:password}}' ]]] DBSubnetGroupName: !Ref DBSubnetGroup VpcSecurityGroupIds: - !Ref ClusterSecurityGroup BackupRetentionPeriod: !If [IsProd, 35, 7] DBClusterParameterGroupName: !Ref RDSDBClusterParameterGroup SnapshotIdentifier: !If [IsUseDBSnapshot, !Ref DBSnapshotName, !Ref "AWS::NoValue"] StorageEncrypted: !If [IsUseDBSnapshot, !Ref "AWS::NoValue", true] #KmsKeyId: !If [IsNotUseDBSnapshot, '/alias/aws/rds/', !Ref 'AWS::NoValue'] EnableIAMDatabaseAuthentication: true Tags: - Key: EnvironmentStage Value: !Ref EnvironmentStage - Key: Application Value: !If [IsApplication, !Ref Application, !Ref "AWS::NoValue"] - Key: ApplicationVersion Value: !If [IsApplicationVersion, !Ref ApplicationVersion, !Ref "AWS::NoValue"] - Key: ProjectCostCenter Value: !If [IsProjectCostCenter, !Ref ProjectCostCenter, !Ref "AWS::NoValue"] - Key: ServiceOwnersEmailContact Value: !If [IsServiceOwnersEmailContact, !Ref ServiceOwnersEmailContact, !Ref "AWS::NoValue"] - Key: Confidentiality Value: !If [IsConfidentiality, !Ref Confidentiality, !Ref "AWS::NoValue"] - Key: Compliance Value: !If [IsCompliance, !Ref Compliance, !Ref "AWS::NoValue"] AuroraDBFirstInstance: Type: AWS::RDS::DBInstance Properties: CopyTagsToSnapshot: true DBClusterIdentifier: !Ref AuroraDBCluster DBInstanceClass: db.serverless Engine: aurora-postgresql #EngineVersion: !Ref DBEngineVersion DBParameterGroupName: Ref: DBParamGroup MonitoringInterval: !If [IsProd, 1, 0] MonitoringRoleArn: !If [IsProd, !GetAtt MonitoringIAMRole.Arn, !Ref "AWS::NoValue"] AutoMinorVersionUpgrade: !If [IsProd, 'false', 'true'] DBSubnetGroupName: !Ref DBSubnetGroup PubliclyAccessible: false EnablePerformanceInsights: true #PerformanceInsightsKMSKeyId: '/alias/aws/rds' PerformanceInsightsRetentionPeriod: !If [IsProd, 731, 7] Tags: - Key: EnvironmentStage Value: !Ref EnvironmentStage - Key: Application Value: !If [IsApplication, !Ref Application, !Ref "AWS::NoValue"] - Key: ApplicationVersion Value: !If [IsApplicationVersion, !Ref ApplicationVersion, !Ref "AWS::NoValue"] - Key: ProjectCostCenter Value: !If [IsProjectCostCenter, !Ref ProjectCostCenter, !Ref "AWS::NoValue"] - Key: ServiceOwnersEmailContact Value: !If [IsServiceOwnersEmailContact, !Ref ServiceOwnersEmailContact, !Ref "AWS::NoValue"] - Key: Confidentiality Value: !If [IsConfidentiality, !Ref Confidentiality, !Ref "AWS::NoValue"] - Key: Compliance Value: !If [IsCompliance, !Ref Compliance, !Ref "AWS::NoValue"] AuroraDBSecondInstance: Condition: IsReplica Type: AWS::RDS::DBInstance DependsOn: - AuroraDBFirstInstance Properties: CopyTagsToSnapshot: true DBClusterIdentifier: !Ref AuroraDBCluster DBInstanceClass: db.serverless Engine: aurora-postgresql #EngineVersion: !Ref DBEngineVersion DBParameterGroupName: Ref: DBParamGroup MonitoringInterval: !If [IsProd, 1, 0] MonitoringRoleArn: !If [IsProd, !GetAtt MonitoringIAMRole.Arn, !Ref "AWS::NoValue"] AutoMinorVersionUpgrade: !If [IsProd, 'false', 'true'] DBSubnetGroupName: !Ref DBSubnetGroup PubliclyAccessible: false EnablePerformanceInsights: true #PerformanceInsightsKMSKeyId: '/alias/aws/rds' PerformanceInsightsRetentionPeriod: !If [IsProd, 731, 7] Tags: - Key: EnvironmentStage Value: !Ref EnvironmentStage - Key: Application Value: !If [IsApplication, !Ref Application, !Ref "AWS::NoValue"] - Key: ApplicationVersion Value: !If [IsApplicationVersion, !Ref ApplicationVersion, !Ref "AWS::NoValue"] - Key: ProjectCostCenter Value: !If [IsProjectCostCenter, !Ref ProjectCostCenter, !Ref "AWS::NoValue"] - Key: ServiceOwnersEmailContact Value: !If [IsServiceOwnersEmailContact, !Ref ServiceOwnersEmailContact, !Ref "AWS::NoValue"] - Key: Confidentiality Value: !If [IsConfidentiality, !Ref Confidentiality, !Ref "AWS::NoValue"] - Key: Compliance Value: !If [IsCompliance, !Ref Compliance, !Ref "AWS::NoValue"] CPUUtilizationAlarm1: Type: "AWS::CloudWatch::Alarm" Properties: ActionsEnabled: true AlarmActions: - Ref: DBSNSTopic AlarmDescription: 'CPU_Utilization' Dimensions: - Name: DBInstanceIdentifier Value: Ref: AuroraDBFirstInstance MetricName: CPUUtilization Statistic: Maximum Namespace: 'AWS/RDS' Threshold: '80' Unit: Percent ComparisonOperator: 'GreaterThanOrEqualToThreshold' Period: '60' EvaluationPeriods: '5' TreatMissingData: 'notBreaching' CPUUtilizationAlarm2: Condition: IsReplica Type: "AWS::CloudWatch::Alarm" Properties: ActionsEnabled: true AlarmActions: - Ref: DBSNSTopic AlarmDescription: 'CPU_Utilization' Dimensions: - Name: DBInstanceIdentifier Value: Ref: AuroraDBSecondInstance MetricName: CPUUtilization Statistic: Maximum Namespace: 'AWS/RDS' Threshold: '80' Unit: Percent ComparisonOperator: 'GreaterThanOrEqualToThreshold' Period: '60' EvaluationPeriods: '5' TreatMissingData: 'notBreaching' MaxUsedTxIDsAlarm1: Type: "AWS::CloudWatch::Alarm" Properties: ActionsEnabled: true AlarmActions: - Ref: DBSNSTopic AlarmDescription: 'Maximum Used Transaction IDs' Dimensions: - Name: DBInstanceIdentifier Value: Ref: AuroraDBFirstInstance MetricName: 'MaximumUsedTransactionIDs' Statistic: Average Namespace: 'AWS/RDS' Threshold: '600000000' Unit: Count ComparisonOperator: 'GreaterThanOrEqualToThreshold' Period: '60' EvaluationPeriods: '5' TreatMissingData: 'notBreaching' MaxUsedTxIDsAlarm2: Condition: IsReplica Type: "AWS::CloudWatch::Alarm" Properties: ActionsEnabled: true AlarmActions: - Ref: DBSNSTopic AlarmDescription: 'Maximum Used Transaction IDs' Dimensions: - Name: DBInstanceIdentifier Value: Ref: AuroraDBSecondInstance MetricName: 'MaximumUsedTransactionIDs' Statistic: Average Namespace: 'AWS/RDS' Threshold: '600000000' Unit: Count ComparisonOperator: 'GreaterThanOrEqualToThreshold' Period: '60' EvaluationPeriods: '5' TreatMissingData: 'notBreaching' DatabaseClusterEventSubscription: Type: 'AWS::RDS::EventSubscription' Properties: EventCategories: - failover - failure - notification SnsTopicArn: !Ref DBSNSTopic SourceIds: [!Ref AuroraDBCluster] SourceType: 'db-cluster' DatabaseInstanceEventSubscription: Type: 'AWS::RDS::EventSubscription' Properties: EventCategories: - availability - configuration change - deletion - failover - failure - maintenance - notification - recovery SnsTopicArn: !Ref DBSNSTopic SourceIds: - !Ref AuroraDBFirstInstance - !If [IsReplica, !Ref AuroraDBSecondInstance, !Ref "AWS::NoValue"] SourceType: 'db-instance' DBParameterGroupEventSubscription: Type: 'AWS::RDS::EventSubscription' Properties: EventCategories: - configuration change SnsTopicArn: !Ref DBSNSTopic SourceIds: [!Ref DBParamGroup] SourceType: 'db-parameter-group' ############################################################################### # Outputs ############################################################################### Outputs: ClusterEndpoint: Description: 'Aurora Cluster/Writer Endpoint' Value: !GetAtt 'AuroraDBCluster.Endpoint.Address' ReaderEndpoint: Description: 'Aurora Reader Endpoint' Value: !GetAtt 'AuroraDBCluster.ReadEndpoint.Address' Port: Description: 'Aurora Endpoint Port' Value: !GetAtt 'AuroraDBCluster.Endpoint.Port' DBUsername: Description: 'Database admin username' Value: !Ref DBUsername DBName: Description: 'Database Name' Value: !Ref DBName PSQLCommandLine: Description: PSQL Command Line Value: !Join - '' - - 'psql --host=' - !GetAtt 'AuroraDBCluster.Endpoint.Address' - ' --port=' - !GetAtt 'AuroraDBCluster.Endpoint.Port' - ' --username=' - !Ref DBUsername - ' --dbname=' - !Ref DBName