diff --git a/internal/pkg/addon/addon_integration_test.go b/internal/pkg/addon/addon_integration_test.go index 7f516dc9c38..2885305355f 100644 --- a/internal/pkg/addon/addon_integration_test.go +++ b/internal/pkg/addon/addon_integration_test.go @@ -24,7 +24,7 @@ func TestAddons(t *testing.T) { outFileName string }{ "aurora": { - addonMarshaler: addon.NewRDSTemplate(addon.RDSProps{ + addonMarshaler: addon.NewServerlessV2Template(addon.RDSProps{ ClusterName: "aurora", Engine: "MySQL", InitialDBName: "main", diff --git a/internal/pkg/addon/storage.go b/internal/pkg/addon/storage.go index c3a3d1393a0..b973460938e 100644 --- a/internal/pkg/addon/storage.go +++ b/internal/pkg/addon/storage.go @@ -15,11 +15,19 @@ import ( ) const ( - dynamoDbTemplatePath = "addons/ddb/cf.yml" - s3TemplatePath = "addons/s3/cf.yml" - rdsTemplatePath = "addons/aurora/cf.yml" - rdsRDWSTemplatePath = "addons/aurora/rdws/cf.yml" - rdsRDWSParamsPath = "addons/aurora/rdws/addons.parameters.yml" + dynamoDbTemplatePath = "addons/ddb/cf.yml" + s3TemplatePath = "addons/s3/cf.yml" + rdsTemplatePath = "addons/aurora/cf.yml" + rdsV2TemplatePath = "addons/aurora/serverlessv2.yml" + rdsRDWSTemplatePath = "addons/aurora/rdws/cf.yml" + rdsRDWSV2TemplatePath = "addons/aurora/rdws/serverlessv2.yml" + rdsRDWSParamsPath = "addons/aurora/rdws/addons.parameters.yml" +) + +const ( + // Aurora Serverless versions. + auroraServerlessVersionV1 = "v1" + auroraServerlessVersionV2 = "v2" ) const ( @@ -82,8 +90,12 @@ type RDSTemplate struct { // MarshalBinary serializes the content of the template into binary. func (r *RDSTemplate) MarshalBinary() ([]byte, error) { path := rdsTemplatePath - if r.WorkloadType == manifest.RequestDrivenWebServiceType { + if r.WorkloadType != manifest.RequestDrivenWebServiceType && r.auroraServerlessVersion == auroraServerlessVersionV2 { + path = rdsV2TemplatePath + } else if r.WorkloadType == manifest.RequestDrivenWebServiceType && r.auroraServerlessVersion == auroraServerlessVersionV1 { path = rdsRDWSTemplatePath + } else if r.WorkloadType == manifest.RequestDrivenWebServiceType && r.auroraServerlessVersion == auroraServerlessVersionV2 { + path = rdsRDWSV2TemplatePath } content, err := r.parser.Parse(path, *r, template.WithFuncs(storageTemplateFunctions)) if err != nil { @@ -147,16 +159,28 @@ func NewDDBTemplate(input *DynamoDBProps) *DynamoDBTemplate { // RDSProps holds RDS-specific properties for addon.NewRDSTemplate(). type RDSProps struct { - WorkloadType string // The type of the workload associated with the RDS addon. - ClusterName string // The name of the cluster. - Engine string // The engine type of the RDS Aurora Serverless cluster. - InitialDBName string // The name of the initial database created inside the cluster. - ParameterGroup string // The parameter group to use for the cluster. - Envs []string // The copilot environments found inside the current app. + WorkloadType string // The type of the workload associated with the RDS addon. + ClusterName string // The name of the cluster. + auroraServerlessVersion string // The version of Aurora Serverless. + Engine string // The engine type of the RDS Aurora Serverless cluster. + InitialDBName string // The name of the initial database created inside the cluster. + ParameterGroup string // The parameter group to use for the cluster. + Envs []string // The copilot environments found inside the current app. +} + +// NewServerlessV1Template creates a new RDS marshaler which can be used to write an Aurora Serverless v1 CloudFormation template. +func NewServerlessV1Template(input RDSProps) *RDSTemplate { + input.auroraServerlessVersion = auroraServerlessVersionV1 + return &RDSTemplate{ + RDSProps: input, + + parser: template.New(), + } } -// NewRDSTemplate creates a new RDS marshaler which can be used to write a RDS CloudFormation template. -func NewRDSTemplate(input RDSProps) *RDSTemplate { +// NewServerlessV2Template creates a new RDS marshaler which can be used to write an Aurora Serverless v2 CloudFormation template. +func NewServerlessV2Template(input RDSProps) *RDSTemplate { + input.auroraServerlessVersion = auroraServerlessVersionV2 return &RDSTemplate{ RDSProps: input, diff --git a/internal/pkg/addon/storage_test.go b/internal/pkg/addon/storage_test.go index 96574e7412a..cf148ae8396 100644 --- a/internal/pkg/addon/storage_test.go +++ b/internal/pkg/addon/storage_test.go @@ -111,6 +111,7 @@ func TestS3Template_MarshalBinary(t *testing.T) { func TestRDSTemplate_MarshalBinary(t *testing.T) { testCases := map[string]struct { workloadType string + version string engine string mockDependencies func(ctrl *gomock.Controller, r *RDSTemplate) @@ -118,7 +119,8 @@ func TestRDSTemplate_MarshalBinary(t *testing.T) { wantedError error }{ "error parsing template": { - engine: RDSEngineTypePostgreSQL, + version: auroraServerlessVersionV1, + engine: RDSEngineTypePostgreSQL, mockDependencies: func(ctrl *gomock.Controller, r *RDSTemplate) { m := mocks.NewMockParser(ctrl) r.parser = m @@ -126,8 +128,9 @@ func TestRDSTemplate_MarshalBinary(t *testing.T) { }, wantedError: errors.New("some error"), }, - "renders postgresql content": { - engine: RDSEngineTypePostgreSQL, + "renders postgresql v1 content": { + version: auroraServerlessVersionV1, + engine: RDSEngineTypePostgreSQL, mockDependencies: func(ctrl *gomock.Controller, r *RDSTemplate) { m := mocks.NewMockParser(ctrl) r.parser = m @@ -137,8 +140,9 @@ func TestRDSTemplate_MarshalBinary(t *testing.T) { }, wantedBinary: []byte("psql"), }, - "renders mysql content": { - engine: RDSEngineTypeMySQL, + "renders mysql v1 content": { + version: auroraServerlessVersionV1, + engine: RDSEngineTypeMySQL, mockDependencies: func(ctrl *gomock.Controller, r *RDSTemplate) { m := mocks.NewMockParser(ctrl) r.parser = m @@ -148,8 +152,9 @@ func TestRDSTemplate_MarshalBinary(t *testing.T) { }, wantedBinary: []byte("mysql"), }, - "renders rdws rds template": { + "renders rdws rds v1 template": { workloadType: "Request-Driven Web Service", + version: auroraServerlessVersionV1, engine: RDSEngineTypeMySQL, mockDependencies: func(ctrl *gomock.Controller, r *RDSTemplate) { m := mocks.NewMockParser(ctrl) @@ -159,6 +164,42 @@ func TestRDSTemplate_MarshalBinary(t *testing.T) { }, wantedBinary: []byte("mysql"), }, + "renders postgresql v2 content": { + version: auroraServerlessVersionV2, + engine: RDSEngineTypePostgreSQL, + mockDependencies: func(ctrl *gomock.Controller, r *RDSTemplate) { + m := mocks.NewMockParser(ctrl) + r.parser = m + m.EXPECT().Parse(gomock.Eq(rdsV2TemplatePath), *r, gomock.Any()). + Return(&template.Content{Buffer: bytes.NewBufferString("psql")}, nil) + + }, + wantedBinary: []byte("psql"), + }, + "renders mysql v2 content": { + version: auroraServerlessVersionV2, + engine: RDSEngineTypeMySQL, + mockDependencies: func(ctrl *gomock.Controller, r *RDSTemplate) { + m := mocks.NewMockParser(ctrl) + r.parser = m + m.EXPECT().Parse(gomock.Eq(rdsV2TemplatePath), *r, gomock.Any()). + Return(&template.Content{Buffer: bytes.NewBufferString("mysql")}, nil) + + }, + wantedBinary: []byte("mysql"), + }, + "renders rdws rds v2 template": { + workloadType: "Request-Driven Web Service", + version: auroraServerlessVersionV2, + engine: RDSEngineTypeMySQL, + mockDependencies: func(ctrl *gomock.Controller, r *RDSTemplate) { + m := mocks.NewMockParser(ctrl) + r.parser = m + m.EXPECT().Parse(gomock.Eq(rdsRDWSV2TemplatePath), *r, gomock.Any()). + Return(&template.Content{Buffer: bytes.NewBufferString("mysql")}, nil) + }, + wantedBinary: []byte("mysql"), + }, } for name, tc := range testCases { @@ -168,8 +209,9 @@ func TestRDSTemplate_MarshalBinary(t *testing.T) { defer ctrl.Finish() addon := &RDSTemplate{ RDSProps: RDSProps{ - WorkloadType: tc.workloadType, - Engine: tc.engine, + WorkloadType: tc.workloadType, + auroraServerlessVersion: tc.version, + Engine: tc.engine, }, } tc.mockDependencies(ctrl, addon) diff --git a/internal/pkg/addon/testdata/storage/aurora.yml b/internal/pkg/addon/testdata/storage/aurora.yml index a23529546d3..d7d636086ea 100644 --- a/internal/pkg/addon/testdata/storage/aurora.yml +++ b/internal/pkg/addon/testdata/storage/aurora.yml @@ -11,59 +11,55 @@ Parameters: # Customize your Aurora Serverless cluster by setting the default value of the following parameters. auroraDBName: Type: String - Description: The name of the initial database to be created in the DB cluster. + Description: The name of the initial database to be created in the Aurora Serverless v2 cluster. Default: main # Cannot have special characters # Naming constraints: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints - auroraDBAutoPauseSeconds: - Type: Number - Description: The duration in seconds before the cluster pauses. - Default: 1000 Mappings: - auroraEnvScalingConfigurationMap: + auroraEnvScalingConfigurationMap: test: - "DBMinCapacity": 1 # AllowedValues: [1, 2, 4, 8, 16, 32, 64, 128, 256] - "DBMaxCapacity": 8 # AllowedValues: [1, 2, 4, 8, 16, 32, 64, 128, 256] + "DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128 + "DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128 + All: - "DBMinCapacity": 1 # AllowedValues: [1, 2, 4, 8, 16, 32, 64, 128, 256] - "DBMaxCapacity": 8 # AllowedValues: [1, 2, 4, 8, 16, 32, 64, 128, 256] + "DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128 + "DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128 + Resources: auroraDBSubnetGroup: - Type: 'AWS::RDS::DBSubnetGroup' + Type: "AWS::RDS::DBSubnetGroup" Properties: - DBSubnetGroupDescription: Group of Copilot private subnets for Aurora cluster. + DBSubnetGroupDescription: Group of Copilot private subnets for Aurora Serverless v2 cluster. SubnetIds: - !Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' }] + !Split [",", { "Fn::ImportValue": !Sub "${App}-${Env}-PrivateSubnets" }] auroraSecurityGroup: Metadata: - 'aws:copilot:description': 'A security group for your workload to access the DB cluster aurora' - Type: 'AWS::EC2::SecurityGroup' + "aws:copilot:description": "A security group for your workload to access the Aurora Serverless v2 cluster aurora" + Type: "AWS::EC2::SecurityGroup" Properties: - GroupDescription: !Sub 'The Security Group for ${Name} to access DB cluster aurora.' + GroupDescription: !Sub "The Security Group for ${Name} to access Aurora Serverless v2 cluster aurora." VpcId: - Fn::ImportValue: - !Sub '${App}-${Env}-VpcId' + Fn::ImportValue: !Sub "${App}-${Env}-VpcId" Tags: - Key: Name - Value: !Sub 'copilot-${App}-${Env}-${Name}-Aurora' + Value: !Sub "copilot-${App}-${Env}-${Name}-Aurora" auroraDBClusterSecurityGroup: Metadata: - "aws:copilot:description": A security group for your DB cluster aurora + "aws:copilot:description": "A security group for your Aurora Serverless v2 cluster aurora" Type: AWS::EC2::SecurityGroup Properties: - GroupDescription: The Security Group for the database cluster. + GroupDescription: The Security Group for the Aurora Serverless v2 cluster. SecurityGroupIngress: - ToPort: 3306 FromPort: 3306 IpProtocol: tcp - Description: !Sub 'From the Aurora Security Group of the workload ${Name}.' + Description: !Sub "From the Aurora Security Group of the workload ${Name}." SourceSecurityGroupId: !Ref auroraSecurityGroup VpcId: - Fn::ImportValue: - !Sub '${App}-${Env}-VpcId' + Fn::ImportValue: !Sub "${App}-${Env}-VpcId" auroraAuroraSecret: Metadata: - 'aws:copilot:description': A Secrets Manager secret to store your DB credentials + "aws:copilot:description": "A Secrets Manager secret to store your DB credentials" Type: AWS::SecretsManager::Secret Properties: Description: !Sub Aurora main user secret for ${AWS::StackName} @@ -75,35 +71,63 @@ Resources: PasswordLength: 16 auroraDBClusterParameterGroup: Metadata: - 'aws:copilot:description': A DB parameter group for engine configuration values - Type: 'AWS::RDS::DBClusterParameterGroup' + "aws:copilot:description": "A DB parameter group for engine configuration values" + Type: "AWS::RDS::DBClusterParameterGroup" Properties: - Description: !Ref 'AWS::StackName' - Family: 'aurora-mysql5.7' + Description: !Ref "AWS::StackName" + Family: "aurora-mysql8.0" Parameters: - character_set_client: 'utf8' + character_set_client: "utf8" auroraDBCluster: Metadata: - 'aws:copilot:description': The aurora Aurora Serverless database cluster - Type: 'AWS::RDS::DBCluster' + "aws:copilot:description": "The aurora Aurora Serverless v2 database cluster" + Type: "AWS::RDS::DBCluster" Properties: MasterUsername: - !Join [ "", [ '{{resolve:secretsmanager:', !Ref auroraAuroraSecret, ":SecretString:username}}" ]] + !Join [ + "", + [ + "{{resolve:secretsmanager:", + !Ref auroraAuroraSecret, + ":SecretString:username}}", + ], + ] MasterUserPassword: - !Join [ "", [ '{{resolve:secretsmanager:', !Ref auroraAuroraSecret, ":SecretString:password}}" ]] + !Join [ + "", + [ + "{{resolve:secretsmanager:", + !Ref auroraAuroraSecret, + ":SecretString:password}}", + ], + ] DatabaseName: !Ref auroraDBName - Engine: 'aurora-mysql' - EngineVersion: '5.7.mysql_aurora.2.07.1' - EngineMode: serverless + Engine: "aurora-mysql" + EngineVersion: "8.0.mysql_aurora.3.02.0" DBClusterParameterGroupName: !Ref auroraDBClusterParameterGroup DBSubnetGroupName: !Ref auroraDBSubnetGroup VpcSecurityGroupIds: - !Ref auroraDBClusterSecurityGroup - ScalingConfiguration: - AutoPause: true - MinCapacity: !FindInMap [auroraEnvScalingConfigurationMap, All, DBMinCapacity] - MaxCapacity: !FindInMap [auroraEnvScalingConfigurationMap, All, DBMaxCapacity] - SecondsUntilAutoPause: !Ref auroraDBAutoPauseSeconds + ServerlessV2ScalingConfiguration: + # Replace "All" below with "!Ref Env" to set different autoscaling limits per environment. + MinCapacity: + !FindInMap [auroraEnvScalingConfigurationMap, All, DBMinCapacity] + MaxCapacity: + !FindInMap [auroraEnvScalingConfigurationMap, All, DBMaxCapacity] + auroraDBWriterInstance: + Metadata: + "aws:copilot:description": "The aurora Aurora Serverless v2 writer instance" + Type: "AWS::RDS::DBInstance" + Properties: + DBClusterIdentifier: !Ref auroraDBCluster + DBInstanceClass: db.serverless + Engine: "aurora-mysql" + PromotionTier: 1 + AvailabilityZone: !Select + - 0 + - !GetAZs + Ref: AWS::Region + auroraSecretAuroraClusterAttachment: Type: AWS::SecretsManager::SecretTargetAttachment Properties: @@ -117,4 +141,3 @@ Outputs: auroraSecurityGroup: Description: "The security group to attach to the workload." Value: !Ref auroraSecurityGroup - diff --git a/internal/pkg/cli/flag.go b/internal/pkg/cli/flag.go index f5b9f9cc90c..4121a8bfa1f 100644 --- a/internal/pkg/cli/flag.go +++ b/internal/pkg/cli/flag.go @@ -64,15 +64,16 @@ const ( noSubscriptionFlag = "no-subscribe" subscribeTopicsFlag = "subscribe-topics" - storageTypeFlag = "storage-type" - storagePartitionKeyFlag = "partition-key" - storageSortKeyFlag = "sort-key" - storageNoSortFlag = "no-sort" - storageLSIConfigFlag = "lsi" - storageNoLSIFlag = "no-lsi" - storageRDSEngineFlag = "engine" - storageRDSInitialDBFlag = "initial-db" - storageRDSParameterGroupFlag = "parameter-group" + storageTypeFlag = "storage-type" + storagePartitionKeyFlag = "partition-key" + storageSortKeyFlag = "sort-key" + storageNoSortFlag = "no-sort" + storageLSIConfigFlag = "lsi" + storageNoLSIFlag = "no-lsi" + storageAuroraServerlessVersionFlag = "serverless-version" + storageRDSEngineFlag = "engine" + storageRDSInitialDBFlag = "initial-db" + storageRDSParameterGroupFlag = "parameter-group" taskGroupNameFlag = "task-group-name" countFlag = "count" @@ -278,7 +279,8 @@ Must be of the format ':'.` storageNoLSIFlagDescription = `Optional. Don't ask about configuring alternate sort keys.` storageLSIConfigFlagDescription = `Optional. Attribute to use as an alternate sort key. May be specified up to 5 times. Must be of the format ':'.` - storageRDSEngineFlagDescription = `The database engine used in the cluster. + storageAuroraServerlessVersionFlagDescription = `Optional. Aurora Serverless version. Must be either "v1" or "v2".` + storageRDSEngineFlagDescription = `The database engine used in the cluster. Must be either "MySQL" or "PostgreSQL".` storageRDSInitialDBFlagDescription = "The initial database to create in the cluster." storageRDSParameterGroupFlagDescription = "Optional. The name of the parameter group to associate with the cluster." diff --git a/internal/pkg/cli/storage_init.go b/internal/pkg/cli/storage_init.go index d57b1e5cc12..4e0942f9786 100644 --- a/internal/pkg/cli/storage_init.go +++ b/internal/pkg/cli/storage_init.go @@ -132,12 +132,21 @@ var ( // RDS Aurora Serverless specific constants and variables. const ( + auroraServerlessVersionV1 = "v1" + auroraServerlessVersionV2 = "v2" + defaultAuroraServerlessVersion = auroraServerlessVersionV2 + fmtRDSStorageNameDefault = "%s-cluster" engineTypeMySQL = "MySQL" engineTypePostgreSQL = "PostgreSQL" ) +var auroraServerlessVersions = []string{ + auroraServerlessVersionV1, + auroraServerlessVersionV2, +} + var engineTypes = []string{ engineTypeMySQL, engineTypePostgreSQL, @@ -158,9 +167,10 @@ type initStorageVars struct { noSort bool // RDS Aurora Serverless specific values collected via flags or prompts - rdsEngine string - rdsParameterGroup string - rdsInitialDBName string + auroraServerlessVersion string + rdsEngine string + rdsParameterGroup string + rdsInitialDBName string } type initStorageOpts struct { @@ -239,6 +249,12 @@ func (o *initStorageOpts) Validate() error { return err } + if o.auroraServerlessVersion != "" { + if err := o.validateServerlessVersion(); err != nil { + return err + } + } + if o.rdsEngine != "" { if err := validateEngine(o.rdsEngine); err != nil { return err @@ -544,6 +560,16 @@ func (o *initStorageOpts) askDynamoLSIConfig() error { } } +func (o *initStorageOpts) validateServerlessVersion() error { + for _, valid := range auroraServerlessVersions { + if o.auroraServerlessVersion == valid { + return nil + } + } + fmtErrInvalidServerlessVersion := "invalid Aurora Serverless version %s: must be one of %s" + return fmt.Errorf(fmtErrInvalidServerlessVersion, o.auroraServerlessVersion, prettify(auroraServerlessVersions)) +} + func (o *initStorageOpts) askAuroraEngineType() error { if o.rdsEngine != "" { return nil @@ -736,15 +762,23 @@ func (o *initStorageOpts) newRDSTemplate() (*addon.RDSTemplate, error) { if err != nil { return nil, err } - - return addon.NewRDSTemplate(addon.RDSProps{ + props := addon.RDSProps{ ClusterName: o.storageName, Engine: engine, InitialDBName: o.rdsInitialDBName, ParameterGroup: o.rdsParameterGroup, Envs: envs, WorkloadType: o.workloadType, - }), nil + } + + switch v := o.auroraServerlessVersion; v { + case auroraServerlessVersionV1: + return addon.NewServerlessV1Template(props), nil + case auroraServerlessVersionV2: + return addon.NewServerlessV2Template(props), nil + default: + return nil, fmt.Errorf("unknown Aurora serverless version %q", v) + } } func (o *initStorageOpts) environmentNames() ([]string, error) { @@ -828,8 +862,10 @@ Resource names are injected into your containers as environment variables for ea /code $ copilot storage init -n my-table -t DynamoDB -w frontend --partition-key Email:S --sort-key UserId:N --no-lsi Create a DynamoDB table with multiple alternate sort keys. /code $ copilot storage init -n my-table -t DynamoDB -w frontend --partition-key Email:S --sort-key UserId:N --lsi Points:N --lsi Goodness:N - Create an RDS Aurora Serverless cluster using PostgreSQL as the database engine. - /code $ copilot storage init -n my-cluster -t Aurora -w frontend --engine PostgreSQL`, + Create an RDS Aurora Serverless v2 cluster using PostgreSQL as the database engine. + /code $ copilot storage init -n my-cluster -t Aurora -w frontend --engine PostgreSQL --initial-db testdb + Create an RDS Aurora Serverless v1 cluster using MySQL as the database engine. + /code $ copilot storage init -n my-cluster -t Aurora --serverless-version v1 -w frontend --engine MySQL --initial-db testdb`, RunE: runCmdE(func(cmd *cobra.Command, args []string) error { opts, err := newStorageInitOpts(vars) if err != nil { @@ -848,6 +884,7 @@ Resource names are injected into your containers as environment variables for ea cmd.Flags().BoolVar(&vars.noLSI, storageNoLSIFlag, false, storageNoLSIFlagDescription) cmd.Flags().BoolVar(&vars.noSort, storageNoSortFlag, false, storageNoSortFlagDescription) + cmd.Flags().StringVar(&vars.auroraServerlessVersion, storageAuroraServerlessVersionFlag, defaultAuroraServerlessVersion, storageAuroraServerlessVersionFlagDescription) cmd.Flags().StringVar(&vars.rdsEngine, storageRDSEngineFlag, "", storageRDSEngineFlagDescription) cmd.Flags().StringVar(&vars.rdsInitialDBName, storageRDSInitialDBFlag, "", storageRDSInitialDBFlagDescription) cmd.Flags().StringVar(&vars.rdsParameterGroup, storageRDSParameterGroupFlag, "", storageRDSParameterGroupFlagDescription) @@ -865,6 +902,7 @@ Resource names are injected into your containers as environment variables for ea ddbFlags.AddFlag(cmd.Flags().Lookup(storageNoLSIFlag)) auroraFlags := pflag.NewFlagSet("Aurora Serverless", pflag.ContinueOnError) + auroraFlags.AddFlag(cmd.Flags().Lookup(storageAuroraServerlessVersionFlag)) auroraFlags.AddFlag(cmd.Flags().Lookup(storageRDSEngineFlag)) auroraFlags.AddFlag(cmd.Flags().Lookup(storageRDSInitialDBFlag)) auroraFlags.AddFlag(cmd.Flags().Lookup(storageRDSParameterGroupFlag)) diff --git a/internal/pkg/cli/storage_init_test.go b/internal/pkg/cli/storage_init_test.go index 06dccd24f05..6d0b88ad0a7 100644 --- a/internal/pkg/cli/storage_init_test.go +++ b/internal/pkg/cli/storage_init_test.go @@ -20,16 +20,17 @@ import ( func TestStorageInitOpts_Validate(t *testing.T) { testCases := map[string]struct { - inAppName string - inStorageType string - inSvcName string - inStorageName string - inPartition string - inSort string - inLSISorts []string - inNoSort bool - inNoLSI bool - inEngine string + inAppName string + inStorageType string + inSvcName string + inStorageName string + inPartition string + inSort string + inLSISorts []string + inNoSort bool + inNoLSI bool + inServerlessVersion string + inEngine string mockWs func(m *mocks.MockwsAddonManager) mockStore func(m *mocks.Mockstore) @@ -173,6 +174,28 @@ func TestStorageInitOpts_Validate(t *testing.T) { wantedErr: errors.New("invalid engine type mysql: must be one of \"MySQL\", \"PostgreSQL\""), }, + "successfully validates aurora serverless version v1": { + mockWs: func(m *mocks.MockwsAddonManager) {}, + mockStore: func(m *mocks.Mockstore) {}, + inAppName: "bowie", + inStorageType: rdsStorageType, + inServerlessVersion: auroraServerlessVersionV1, + }, + "successfully validates aurora serverless version v2": { + mockWs: func(m *mocks.MockwsAddonManager) {}, + mockStore: func(m *mocks.Mockstore) {}, + inAppName: "bowie", + inStorageType: rdsStorageType, + inServerlessVersion: auroraServerlessVersionV2, + }, + "invalid aurora serverless version": { + mockWs: func(m *mocks.MockwsAddonManager) {}, + mockStore: func(m *mocks.Mockstore) {}, + inAppName: "bowie", + inStorageType: rdsStorageType, + inServerlessVersion: "weird-serverless-version", + wantedErr: errors.New("invalid Aurora Serverless version weird-serverless-version: must be one of \"v1\", \"v2\""), + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { @@ -185,15 +208,16 @@ func TestStorageInitOpts_Validate(t *testing.T) { tc.mockStore(mockStore) opts := initStorageOpts{ initStorageVars: initStorageVars{ - storageType: tc.inStorageType, - storageName: tc.inStorageName, - workloadName: tc.inSvcName, - partitionKey: tc.inPartition, - sortKey: tc.inSort, - lsiSorts: tc.inLSISorts, - noLSI: tc.inNoLSI, - noSort: tc.inNoSort, - rdsEngine: tc.inEngine, + storageType: tc.inStorageType, + storageName: tc.inStorageName, + workloadName: tc.inSvcName, + partitionKey: tc.inPartition, + sortKey: tc.inSort, + lsiSorts: tc.inLSISorts, + noLSI: tc.inNoLSI, + noSort: tc.inNoSort, + auroraServerlessVersion: tc.inServerlessVersion, + rdsEngine: tc.inEngine, }, appName: tc.inAppName, ws: mockWs, @@ -222,8 +246,9 @@ func TestStorageInitOpts_Ask(t *testing.T) { wantedPartitionKey = "DogName:String" wantedSortKey = "PhotoId:Number" - wantedInitialDBName = "mydb" - wantedDBEngine = engineTypePostgreSQL + wantedServerlessVersion = auroraServerlessVersionV2 + wantedInitialDBName = "mydb" + wantedDBEngine = engineTypePostgreSQL ) testCases := map[string]struct { inAppName string @@ -236,8 +261,9 @@ func TestStorageInitOpts_Ask(t *testing.T) { inNoLSI bool inNoSort bool - inDBEngine string - inInitialDBName string + inServerlessVersion string + inDBEngine string + inInitialDBName string mockPrompt func(m *mocks.Mockprompter) mockCfg func(m *mocks.MockwsSelector) @@ -330,11 +356,12 @@ func TestStorageInitOpts_Ask(t *testing.T) { wantedErr: nil, }, "Asks for cluster name for RDS storage": { - inAppName: wantedAppName, - inSvcName: wantedSvcName, - inStorageType: rdsStorageType, - inDBEngine: wantedDBEngine, - inInitialDBName: wantedInitialDBName, + inAppName: wantedAppName, + inSvcName: wantedSvcName, + inStorageType: rdsStorageType, + inServerlessVersion: wantedServerlessVersion, + inDBEngine: wantedDBEngine, + inInitialDBName: wantedInitialDBName, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get( @@ -351,11 +378,12 @@ func TestStorageInitOpts_Ask(t *testing.T) { wantedErr: nil, wantedVars: &initStorageVars{ - storageType: rdsStorageType, - storageName: wantedBucketName, - workloadName: wantedSvcName, - rdsEngine: wantedDBEngine, - rdsInitialDBName: wantedInitialDBName, + storageType: rdsStorageType, + storageName: wantedBucketName, + workloadName: wantedSvcName, + auroraServerlessVersion: wantedServerlessVersion, + rdsEngine: wantedDBEngine, + rdsInitialDBName: wantedInitialDBName, }, }, "error if storage name not returned": { @@ -769,8 +797,9 @@ func TestStorageInitOpts_Ask(t *testing.T) { inSvcName: wantedSvcName, inStorageName: wantedBucketName, - inStorageType: rdsStorageType, - inInitialDBName: wantedInitialDBName, + inStorageType: rdsStorageType, + inServerlessVersion: wantedServerlessVersion, + inInitialDBName: wantedInitialDBName, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().SelectOne(gomock.Eq(storageInitRDSDBEnginePrompt), gomock.Any(), gomock.Any(), gomock.Any()). @@ -782,11 +811,12 @@ func TestStorageInitOpts_Ask(t *testing.T) { }, wantedVars: &initStorageVars{ - storageType: rdsStorageType, - storageName: wantedBucketName, - workloadName: wantedSvcName, - rdsInitialDBName: wantedInitialDBName, - rdsEngine: wantedDBEngine, + storageType: rdsStorageType, + storageName: wantedBucketName, + workloadName: wantedSvcName, + auroraServerlessVersion: wantedServerlessVersion, + rdsInitialDBName: wantedInitialDBName, + rdsEngine: wantedDBEngine, }, }, "error if engine not gotten": { @@ -794,8 +824,9 @@ func TestStorageInitOpts_Ask(t *testing.T) { inSvcName: wantedSvcName, inStorageName: wantedBucketName, - inStorageType: rdsStorageType, - inInitialDBName: wantedInitialDBName, + inStorageType: rdsStorageType, + inServerlessVersion: wantedServerlessVersion, + inInitialDBName: wantedInitialDBName, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().SelectOne(storageInitRDSDBEnginePrompt, gomock.Any(), gomock.Any(), gomock.Any()). @@ -812,8 +843,9 @@ func TestStorageInitOpts_Ask(t *testing.T) { inSvcName: wantedSvcName, inStorageName: wantedBucketName, - inStorageType: rdsStorageType, - inDBEngine: wantedDBEngine, + inStorageType: rdsStorageType, + inServerlessVersion: wantedServerlessVersion, + inDBEngine: wantedDBEngine, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(gomock.Eq(storageInitRDSInitialDBNamePrompt), gomock.Any(), gomock.Any(), gomock.Any()). @@ -825,11 +857,12 @@ func TestStorageInitOpts_Ask(t *testing.T) { }, wantedVars: &initStorageVars{ - storageType: rdsStorageType, - storageName: wantedBucketName, - workloadName: wantedSvcName, - rdsEngine: wantedDBEngine, - rdsInitialDBName: wantedInitialDBName, + storageType: rdsStorageType, + storageName: wantedBucketName, + workloadName: wantedSvcName, + auroraServerlessVersion: wantedServerlessVersion, + rdsEngine: wantedDBEngine, + rdsInitialDBName: wantedInitialDBName, }, }, "error if initial database name not gotten": { @@ -837,8 +870,9 @@ func TestStorageInitOpts_Ask(t *testing.T) { inSvcName: wantedSvcName, inStorageName: wantedBucketName, - inStorageType: rdsStorageType, - inDBEngine: wantedDBEngine, + inStorageType: rdsStorageType, + inServerlessVersion: wantedServerlessVersion, + inDBEngine: wantedDBEngine, mockPrompt: func(m *mocks.Mockprompter) { m.EXPECT().Get(storageInitRDSInitialDBNamePrompt, gomock.Any(), gomock.Any(), gomock.Any()). @@ -872,8 +906,9 @@ func TestStorageInitOpts_Ask(t *testing.T) { noLSI: tc.inNoLSI, noSort: tc.inNoSort, - rdsEngine: tc.inDBEngine, - rdsInitialDBName: tc.inInitialDBName, + auroraServerlessVersion: tc.inServerlessVersion, + rdsEngine: tc.inDBEngine, + rdsInitialDBName: tc.inInitialDBName, }, appName: tc.inAppName, sel: mockConfig, @@ -921,9 +956,10 @@ func TestStorageInitOpts_Execute(t *testing.T) { inNoLSI bool inNoSort bool - inEngine string - inInitialDBName string - inParameterGroup string + inServerlessVersion string + inEngine string + inInitialDBName string + inParameterGroup string mockWs func(m *mocks.MockwsAddonManager) mockStore func(m *mocks.Mockstore) @@ -976,11 +1012,12 @@ func TestStorageInitOpts_Execute(t *testing.T) { wantedErr: nil, }, "happy calls for RDS with LBWS": { - inSvcName: wantedSvcName, - inStorageType: rdsStorageType, - inStorageName: "mycluster", - inEngine: engineTypeMySQL, - inParameterGroup: "mygroup", + inSvcName: wantedSvcName, + inStorageType: rdsStorageType, + inStorageName: "mycluster", + inServerlessVersion: auroraServerlessVersionV1, + inEngine: engineTypeMySQL, + inParameterGroup: "mygroup", mockWs: func(m *mocks.MockwsAddonManager) { m.EXPECT().ReadWorkloadManifest(wantedSvcName).Return([]byte("type: Load Balanced Web Service"), nil) @@ -992,11 +1029,12 @@ func TestStorageInitOpts_Execute(t *testing.T) { wantedErr: nil, }, "happy calls for RDS with a RDWS": { - inSvcName: wantedSvcName, - inStorageType: rdsStorageType, - inStorageName: "mycluster", - inEngine: engineTypeMySQL, - inParameterGroup: "mygroup", + inSvcName: wantedSvcName, + inStorageType: rdsStorageType, + inStorageName: "mycluster", + inServerlessVersion: auroraServerlessVersionV1, + inEngine: engineTypeMySQL, + inParameterGroup: "mygroup", mockWs: func(m *mocks.MockwsAddonManager) { m.EXPECT().ReadWorkloadManifest(wantedSvcName).Return([]byte("type: Request-Driven Web Service"), nil) @@ -1069,8 +1107,9 @@ func TestStorageInitOpts_Execute(t *testing.T) { noLSI: tc.inNoLSI, noSort: tc.inNoSort, - rdsEngine: tc.inEngine, - rdsParameterGroup: tc.inParameterGroup, + auroraServerlessVersion: tc.inServerlessVersion, + rdsEngine: tc.inEngine, + rdsParameterGroup: tc.inParameterGroup, }, appName: tc.inAppName, ws: mockAddon, @@ -1092,5 +1131,4 @@ func TestStorageInitOpts_Execute(t *testing.T) { } }) } - } diff --git a/internal/pkg/template/templates/addons/aurora/rdws/serverlessv2.yml b/internal/pkg/template/templates/addons/aurora/rdws/serverlessv2.yml new file mode 100644 index 00000000000..f748e503627 --- /dev/null +++ b/internal/pkg/template/templates/addons/aurora/rdws/serverlessv2.yml @@ -0,0 +1,173 @@ +Parameters: + App: + Type: String + Description: Your application's name. + Env: + Type: String + Description: The environment name your service, job, or workflow is being deployed to. + Name: + Type: String + Description: The name of the service, job, or workflow being deployed. + ServiceSecurityGroupId: + Type: String + Description: The security group associated with the VPC connector. + # Customize your Aurora Serverless cluster by setting the default value of the following parameters. + {{logicalIDSafe .ClusterName}}DBName: + Type: String + Description: The name of the initial database to be created in the Aurora Serverless v2 cluster. + Default: {{.InitialDBName}} + # Cannot have special characters + # Naming constraints: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints +Mappings: + {{logicalIDSafe .ClusterName}}EnvScalingConfigurationMap: {{range $env := .Envs}} + {{$env}}: + "DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128 + "DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128 + {{end}} + All: + "DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128 + "DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128 + +Resources: + {{logicalIDSafe .ClusterName}}DBSubnetGroup: + Type: 'AWS::RDS::DBSubnetGroup' + Properties: + DBSubnetGroupDescription: Group of Copilot private subnets for Aurora Serverless v2 cluster. + SubnetIds: + !Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' }] + {{logicalIDSafe .ClusterName}}DBClusterSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your Aurora Serverless v2 cluster {{logicalIDSafe .ClusterName}}' + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: The Security Group for the Aurora Serverless v2 cluster. + SecurityGroupIngress: + {{- if eq .Engine "MySQL"}} + - ToPort: 3306 + FromPort: 3306 + {{- else}} + - ToPort: 5432 + FromPort: 5432 + {{- end}} + IpProtocol: tcp + Description: !Sub 'From the Aurora Security Group of the workload ${Name}.' + SourceSecurityGroupId: !Ref ServiceSecurityGroupId + VpcId: + Fn::ImportValue: + !Sub '${App}-${Env}-VpcId' + + {{logicalIDSafe .ClusterName}}AuroraSecret: + Metadata: + 'aws:copilot:description': 'A Secrets Manager secret to store your DB credentials' + Type: AWS::SecretsManager::Secret + Properties: + Description: !Sub Aurora main user secret for ${AWS::StackName} + GenerateSecretString: + {{- if eq .Engine "MySQL"}} + SecretStringTemplate: '{"username": "admin"}' + {{- else}} + SecretStringTemplate: '{"username": "postgres"}' + {{- end}} + GenerateStringKey: "password" + ExcludePunctuation: true + IncludeSpace: false + PasswordLength: 16 + + {{logicalIDSafe .ClusterName}}AuroraSecretAccessPolicy: + Metadata: + 'aws:copilot:description': 'An IAM ManagedPolicy for your service to access the DB credentials secret' + Type: AWS::IAM::ManagedPolicy + Properties: + Description: !Sub + - Grants read access to the ${Secret} secret + - { Secret: !Ref {{logicalIDSafe .ClusterName}}AuroraSecret } + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: SecretActions + Effect: Allow + Action: + - 'secretsmanager:GetSecretValue' + Resource: + - !Ref {{logicalIDSafe .ClusterName}}AuroraSecret + + {{- if .ParameterGroup}} + # {{logicalIDSafe .ClusterName}}DBClusterParameterGroup: + # Type: 'AWS::RDS::DBClusterParameterGroup' + # Properties: + # Description: !Ref 'AWS::StackName' + # Family: 'aurora-mysql8.0' + # Parameters: + # character_set_client: 'utf8' + {{- else}} + {{logicalIDSafe .ClusterName}}DBClusterParameterGroup: + Metadata: + 'aws:copilot:description': 'A DB parameter group for engine configuration values' + Type: 'AWS::RDS::DBClusterParameterGroup' + Properties: + Description: !Ref 'AWS::StackName' + {{- if eq .Engine "MySQL"}} + Family: 'aurora-mysql8.0' + Parameters: + character_set_client: 'utf8' + {{- else}} + Family: 'aurora-postgresql14' + Parameters: + client_encoding: 'UTF8' + {{- end}} + {{- end}} + {{logicalIDSafe .ClusterName}}DBCluster: + Metadata: + 'aws:copilot:description': 'The {{logicalIDSafe .ClusterName}} Aurora Serverless v2 database cluster' + Type: 'AWS::RDS::DBCluster' + Properties: + MasterUsername: + !Join [ "", [ {{`'{{resolve:secretsmanager:'`}}, !Ref {{logicalIDSafe .ClusterName}}AuroraSecret, ":SecretString:username}}" ]] + MasterUserPassword: + !Join [ "", [ {{`'{{resolve:secretsmanager:'`}}, !Ref {{logicalIDSafe .ClusterName}}AuroraSecret, ":SecretString:password}}" ]] + DatabaseName: !Ref {{logicalIDSafe .ClusterName}}DBName + {{- if eq .Engine "MySQL"}} + Engine: 'aurora-mysql' + EngineVersion: '8.0.mysql_aurora.3.02.0' + {{- else}} + Engine: 'aurora-postgresql' + EngineVersion: '14.4' + {{- end}} + DBClusterParameterGroupName: {{- if .ParameterGroup}} {{.ParameterGroup}} {{- else}} !Ref {{logicalIDSafe .ClusterName}}DBClusterParameterGroup {{- end}} + DBSubnetGroupName: !Ref {{logicalIDSafe .ClusterName}}DBSubnetGroup + VpcSecurityGroupIds: + - !Ref {{logicalIDSafe .ClusterName}}DBClusterSecurityGroup + ServerlessV2ScalingConfiguration: + # Replace "All" below with "!Ref Env" to set different autoscaling limits per environment. + MinCapacity: !FindInMap [{{logicalIDSafe .ClusterName}}EnvScalingConfigurationMap, All, DBMinCapacity] + MaxCapacity: !FindInMap [{{logicalIDSafe .ClusterName}}EnvScalingConfigurationMap, All, DBMaxCapacity] + {{logicalIDSafe .ClusterName}}DBWriterInstance: + Metadata: + 'aws:copilot:description': 'The {{logicalIDSafe .ClusterName}} Aurora Serverless v2 writer instance' + Type: 'AWS::RDS::DBInstance' + Properties: + DBClusterIdentifier: !Ref {{logicalIDSafe .ClusterName}}DBCluster + DBInstanceClass: db.serverless + {{- if eq .Engine "MySQL"}} + Engine: 'aurora-mysql' + {{- else}} + Engine: 'aurora-postgresql' + {{- end}} + PromotionTier: 1 + AvailabilityZone: !Select + - 0 + - !GetAZs + Ref: AWS::Region + {{logicalIDSafe .ClusterName}}SecretAuroraClusterAttachment: + Type: AWS::SecretsManager::SecretTargetAttachment + Properties: + SecretId: !Ref {{logicalIDSafe .ClusterName}}AuroraSecret + TargetId: !Ref {{logicalIDSafe .ClusterName}}DBCluster + TargetType: AWS::RDS::DBCluster +Outputs: + {{logicalIDSafe .ClusterName}}AuroraSecretAccessPolicy: # Automatically augment your instance role with this managed policy. + Description: "Add the IAM ManagedPolicy to your instance role" + Value: !Ref {{logicalIDSafe .ClusterName}}AuroraSecretAccessPolicy + {{logicalIDSafe .ClusterName}}Secret: # Inject this secret ARN in your manifest file. + Description: "The secret ARN that holds the database username and password in JSON format. Fields are 'host', 'port', 'dbname', 'username', 'password', 'dbClusterIdentifier' and 'engine'" + Value: !Ref {{logicalIDSafe .ClusterName}}AuroraSecret diff --git a/internal/pkg/template/templates/addons/aurora/serverlessv2.yml b/internal/pkg/template/templates/addons/aurora/serverlessv2.yml new file mode 100644 index 00000000000..667a3996a03 --- /dev/null +++ b/internal/pkg/template/templates/addons/aurora/serverlessv2.yml @@ -0,0 +1,163 @@ +Parameters: + App: + Type: String + Description: Your application's name. + Env: + Type: String + Description: The environment name your service, job, or workflow is being deployed to. + Name: + Type: String + Description: The name of the service, job, or workflow being deployed. + # Customize your Aurora Serverless cluster by setting the default value of the following parameters. + {{logicalIDSafe .ClusterName}}DBName: + Type: String + Description: The name of the initial database to be created in the Aurora Serverless v2 cluster. + Default: {{.InitialDBName}} + # Cannot have special characters + # Naming constraints: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints +Mappings: + {{logicalIDSafe .ClusterName}}EnvScalingConfigurationMap: {{range $env := .Envs}} + {{$env}}: + "DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128 + "DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128 + {{end}} + All: + "DBMinCapacity": 0.5 # AllowedValues: from 0.5 through 128 + "DBMaxCapacity": 8 # AllowedValues: from 0.5 through 128 + +Resources: + {{logicalIDSafe .ClusterName}}DBSubnetGroup: + Type: 'AWS::RDS::DBSubnetGroup' + Properties: + DBSubnetGroupDescription: Group of Copilot private subnets for Aurora Serverless v2 cluster. + SubnetIds: + !Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' }] + {{logicalIDSafe .ClusterName}}SecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your workload to access the Aurora Serverless v2 cluster {{logicalIDSafe .ClusterName}}' + Type: 'AWS::EC2::SecurityGroup' + Properties: + GroupDescription: !Sub 'The Security Group for ${Name} to access Aurora Serverless v2 cluster {{logicalIDSafe .ClusterName}}.' + VpcId: + Fn::ImportValue: + !Sub '${App}-${Env}-VpcId' + Tags: + - Key: Name + Value: !Sub 'copilot-${App}-${Env}-${Name}-Aurora' + {{logicalIDSafe .ClusterName}}DBClusterSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your Aurora Serverless v2 cluster {{logicalIDSafe .ClusterName}}' + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: The Security Group for the Aurora Serverless v2 cluster. + SecurityGroupIngress: + {{- if eq .Engine "MySQL"}} + - ToPort: 3306 + FromPort: 3306 + {{- else}} + - ToPort: 5432 + FromPort: 5432 + {{- end}} + IpProtocol: tcp + Description: !Sub 'From the Aurora Security Group of the workload ${Name}.' + SourceSecurityGroupId: !Ref {{logicalIDSafe .ClusterName}}SecurityGroup + VpcId: + Fn::ImportValue: + !Sub '${App}-${Env}-VpcId' + {{logicalIDSafe .ClusterName}}AuroraSecret: + Metadata: + 'aws:copilot:description': 'A Secrets Manager secret to store your DB credentials' + Type: AWS::SecretsManager::Secret + Properties: + Description: !Sub Aurora main user secret for ${AWS::StackName} + GenerateSecretString: + {{- if eq .Engine "MySQL"}} + SecretStringTemplate: '{"username": "admin"}' + {{- else}} + SecretStringTemplate: '{"username": "postgres"}' + {{- end}} + GenerateStringKey: "password" + ExcludePunctuation: true + IncludeSpace: false + PasswordLength: 16 + {{- if .ParameterGroup}} + # {{logicalIDSafe .ClusterName}}DBClusterParameterGroup: + # Type: 'AWS::RDS::DBClusterParameterGroup' + # Properties: + # Description: !Ref 'AWS::StackName' + # Family: 'aurora-mysql8.0' + # Parameters: + # character_set_client: 'utf8' + {{- else}} + {{logicalIDSafe .ClusterName}}DBClusterParameterGroup: + Metadata: + 'aws:copilot:description': 'A DB parameter group for engine configuration values' + Type: 'AWS::RDS::DBClusterParameterGroup' + Properties: + Description: !Ref 'AWS::StackName' + {{- if eq .Engine "MySQL"}} + Family: 'aurora-mysql8.0' + Parameters: + character_set_client: 'utf8' + {{- else}} + Family: 'aurora-postgresql14' + Parameters: + client_encoding: 'UTF8' + {{- end}} + {{- end}} + {{logicalIDSafe .ClusterName}}DBCluster: + Metadata: + 'aws:copilot:description': 'The {{logicalIDSafe .ClusterName}} Aurora Serverless v2 database cluster' + Type: 'AWS::RDS::DBCluster' + Properties: + MasterUsername: + !Join [ "", [ {{`'{{resolve:secretsmanager:'`}}, !Ref {{logicalIDSafe .ClusterName}}AuroraSecret, ":SecretString:username}}" ]] + MasterUserPassword: + !Join [ "", [ {{`'{{resolve:secretsmanager:'`}}, !Ref {{logicalIDSafe .ClusterName}}AuroraSecret, ":SecretString:password}}" ]] + DatabaseName: !Ref {{logicalIDSafe .ClusterName}}DBName + {{- if eq .Engine "MySQL"}} + Engine: 'aurora-mysql' + EngineVersion: '8.0.mysql_aurora.3.02.0' + {{- else}} + Engine: 'aurora-postgresql' + EngineVersion: '14.4' + {{- end}} + DBClusterParameterGroupName: {{- if .ParameterGroup}} {{.ParameterGroup}} {{- else}} !Ref {{logicalIDSafe .ClusterName}}DBClusterParameterGroup {{- end}} + DBSubnetGroupName: !Ref {{logicalIDSafe .ClusterName}}DBSubnetGroup + VpcSecurityGroupIds: + - !Ref {{logicalIDSafe .ClusterName}}DBClusterSecurityGroup + ServerlessV2ScalingConfiguration: + # Replace "All" below with "!Ref Env" to set different autoscaling limits per environment. + MinCapacity: !FindInMap [{{logicalIDSafe .ClusterName}}EnvScalingConfigurationMap, All, DBMinCapacity] + MaxCapacity: !FindInMap [{{logicalIDSafe .ClusterName}}EnvScalingConfigurationMap, All, DBMaxCapacity] + {{logicalIDSafe .ClusterName}}DBWriterInstance: + Metadata: + 'aws:copilot:description': 'The {{logicalIDSafe .ClusterName}} Aurora Serverless v2 writer instance' + Type: 'AWS::RDS::DBInstance' + Properties: + DBClusterIdentifier: !Ref {{logicalIDSafe .ClusterName}}DBCluster + DBInstanceClass: db.serverless + {{- if eq .Engine "MySQL"}} + Engine: 'aurora-mysql' + {{- else}} + Engine: 'aurora-postgresql' + {{- end}} + PromotionTier: 1 + AvailabilityZone: !Select + - 0 + - !GetAZs + Ref: AWS::Region + + {{logicalIDSafe .ClusterName}}SecretAuroraClusterAttachment: + Type: AWS::SecretsManager::SecretTargetAttachment + Properties: + SecretId: !Ref {{logicalIDSafe .ClusterName}}AuroraSecret + TargetId: !Ref {{logicalIDSafe .ClusterName}}DBCluster + TargetType: AWS::RDS::DBCluster +Outputs: + {{logicalIDSafe .ClusterName}}Secret: # injected as {{envVarSecret .ClusterName | toSnakeCase}} environment variable by Copilot. + Description: "The JSON secret that holds the database username and password. Fields are 'host', 'port', 'dbname', 'username', 'password', 'dbClusterIdentifier' and 'engine'" + Value: !Ref {{logicalIDSafe .ClusterName}}AuroraSecret + {{logicalIDSafe .ClusterName}}SecurityGroup: + Description: "The security group to attach to the workload." + Value: !Ref {{logicalIDSafe .ClusterName}}SecurityGroup