AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::SecretsManager-2020-07-23 Description: 'CloudFormation Template to create Aurora MySQL Cluster 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: 3306 ConstraintDescription: 'Must be in the range [1150-65535]' MinValue: 1150 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: 'Aurora-MySQL8.0.23' AllowedValues: - 'Aurora-MySQL8.0.23' 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. Pre-prod and Prod will create read-replicas.' 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 - MinCapacity - MaxCapacity - DBPort - DBUsername - DBEngineVersion - DBSnapshotName - NotificationList - Label: default: 'Networking' Parameters: - ParentVPCStack - ParentSSHBastionStack - ParentDMSStack - Label: default: 'Optional Tags' Parameters: - Application - ApplicationVersion - ProjectCostCenter - ServiceOwnersEmailContact - Confidentiality - Compliance ############################################################################### # Mappings ############################################################################### Mappings: DBFamilyMap: "Aurora-MySQL8.0.23": "family": "aurora-mysql8.0" DBEngineVersionMap: "Aurora-MySQL8.0.23": "engineversion": "8.0.mysql_aurora.3.02.0" ############################################################################### # 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 MySQL Cluster Parameter Group for Cloudformation Stack ", !Ref DBName ] ] Family: !FindInMap [DBFamilyMap, !Ref DBEngineVersion, "family"] Parameters: time_zone: UTC server_audit_logging: 1 server_audit_events: 'QUERY_DCL,QUERY_DDL,CONNECT' DBParamGroup: Type: AWS::RDS::DBParameterGroup Properties: Description: !Join [ "- ", [ "Aurora MySQL Database Instance Parameter Group for Cloudformation Stack ", !Ref DBName ] ] Family: !FindInMap [DBFamilyMap, !Ref DBEngineVersion, "family"] Parameters: slow_query_log: 1 long_query_time: 5 log_output: 'FILE' innodb_print_all_deadlocks: 1 AuroraMasterSecret: Condition: IsNotUseDBSnapshot Type: AWS::SecretsManager::Secret Properties: Name: !Join ['/', [!Ref EnvironmentStage, 'aurora-mysql', !Ref 'AWS::StackName']] Description: !Join ['', ['Aurora MySQL Master User Secret ', 'for CloudFormation Stack ', !Ref 'AWS::StackName']] Tags: - Key: EnvironmentStage Value: !Ref EnvironmentStage - Key: DatabaseEngine Value: 'Aurora MySQL' - 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: 'MySQLSingleUser' RotationRules: AutomaticallyAfterDays: 30 AuroraDBCluster: Type: AWS::RDS::DBCluster DeletionPolicy: Snapshot UpdateReplacePolicy: Snapshot Properties: Engine: aurora-mysql #EngineVersion: 8.0.mysql_aurora.3.02.0 EngineVersion: !FindInMap [DBEngineVersionMap, !Ref DBEngineVersion, "engineversion"] 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, !Ref AuroraKMSCMK, !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-mysql 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: !Ref AuroraKMSCMK 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 DBInstanceClass: db.serverless DBClusterIdentifier: !Ref AuroraDBCluster Engine: aurora-mysql 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: !Ref AuroraKMSCMK 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' 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 MySQLCommandLine: Description: MySQL Command Line Value: !Join - '' - - 'mysql -h ' - !GetAtt 'AuroraDBCluster.Endpoint.Address' - ' -P ' - !GetAtt 'AuroraDBCluster.Endpoint.Port' - ' -u ' - !Ref DBUsername - ' -p ' - !Ref DBName