From d243230d348e6aebb3e4769c3f24e562a2bbe260 Mon Sep 17 00:00:00 2001 From: Hyeonsoo Lee Date: Wed, 10 Jun 2020 23:37:17 +0900 Subject: [PATCH 1/7] feat(rds): Add L2 DatabaseProxy (AWS::RDS::DBProxy) closes #8475 --- packages/@aws-cdk/aws-rds/README.md | 33 ++ packages/@aws-cdk/aws-rds/lib/index.ts | 1 + packages/@aws-cdk/aws-rds/lib/proxy.ts | 414 +++++++++++++ .../aws-rds/test/integ.proxy.expected.json | 556 ++++++++++++++++++ packages/@aws-cdk/aws-rds/test/integ.proxy.ts | 28 + .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 5 + packages/aws-cdk/test/init.test.ts | 2 +- 7 files changed, 1038 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-rds/lib/proxy.ts create mode 100644 packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json create mode 100644 packages/@aws-cdk/aws-rds/test/integ.proxy.ts diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 64d7b7b19528b..dfe2623c172bf 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -189,3 +189,36 @@ new DatabaseCluster(this, 'dbcluster', { s3ExportBuckets: [ exportBucket ] }); ``` + +### Creating a Database Proxy + +For RDS Proxy, See [Amazon RDS Proxy](https://aws.amazon.com/rds/proxy/) + +```ts +import * as cdk from '@aws-cdk/core'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as rds from '@aws-cdk/aws-rds'; +import * as secrets from '@aws-cdk/aws-secretsmanager'; + +const vpc: ec2.IVpc = ...; +const securityGroup: ec2.ISecurityGroup = ...; +const secret: secrets.ISecret = ...; +const dbInstance: rds.IDatabaseInstance = ...; + +const proxy = new rds.DatabaseProxy(this, 'proxy', { + connectionPoolConfiguration: { + connectionBorrowTimeout: cdk.Duration.seconds(30), + maxConnectionsPercent: 50, + }, + dbInstance, + engineFamily: rds.DatabaseProxyEngine.POSTGRESQL, + secret, + vpc, +}); + +... + +proxy.dbProxyName; +proxy.dbProxyArn; +proxy.endpoint; +``` diff --git a/packages/@aws-cdk/aws-rds/lib/index.ts b/packages/@aws-cdk/aws-rds/lib/index.ts index 83e4a99bc4a07..41489d58fc11d 100644 --- a/packages/@aws-cdk/aws-rds/lib/index.ts +++ b/packages/@aws-cdk/aws-rds/lib/index.ts @@ -6,6 +6,7 @@ export * from './database-secret'; export * from './endpoint'; export * from './option-group'; export * from './instance'; +export * from './proxy'; // AWS::RDS CloudFormation Resources: export * from './rds.generated'; diff --git a/packages/@aws-cdk/aws-rds/lib/proxy.ts b/packages/@aws-cdk/aws-rds/lib/proxy.ts new file mode 100644 index 0000000000000..b602cddc897cd --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -0,0 +1,414 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import * as cdk from '@aws-cdk/core'; +import { IDatabaseCluster } from './cluster-ref'; +import { IDatabaseInstance } from './instance'; +import { CfnDBProxy, CfnDBProxyTargetGroup } from './rds.generated'; + +/** + * The kinds of databases that the proxy can connect to. + * This value determines which database network protocol the proxy recognizes when it interprets network traffic to + * and from the database. + * The engine family applies to MySQL and PostgreSQL for both RDS and Aurora. + */ +export enum DatabaseProxyEngine { + /** + * MYSQL + */ + MYSQL = 'MYSQL', + + /** + * POSTGRESQL + */ + POSTGRESQL = 'POSTGRESQL', +} + +/** + * Specifies the settings that control the size and behavior of the connection pool. + */ +export interface ConnectionPoolConfiguration { + /** + * The number of seconds for a proxy to wait for a connection to become available in the connection pool. + * Only applies when the proxy has opened its maximum number of connections and all connections are busy with client + * sessions. + * + * between 1 and 3600, or 0 representing unlimited + * + * @default cdk.Duration.seconds(120) + */ + readonly connectionBorrowTimeout?: cdk.Duration; + + /** + * One or more SQL statements for the proxy to run when opening each new database connection. + * Typically used with SET statements to make sure that each connection has identical settings such as time zone + * and character set. + * For multiple statements, use semicolons as the separator. + * You can also include multiple variables in a single SET statement, such as SET x=1, y=2. + * + * not currently supported for PostgreSQL. + * + * @default - no initialization query + */ + readonly initQuery?: string; + + /** + * The maximum size of the connection pool for each target in a target group. + * For Aurora MySQL, it is expressed as a percentage of the max_connections setting for the RDS DB instance or Aurora DB + * cluster used by the target group. + * + * 1-100 + * + * @default 100 + */ + readonly maxConnectionsPercent?: number; + + /** + * Controls how actively the proxy closes idle database connections in the connection pool. + * A high value enables the proxy to leave a high percentage of idle connections open. + * A low value causes the proxy to close idle client connections and return the underlying database connections + * to the connection pool. + * For Aurora MySQL, it is expressed as a percentage of the max_connections setting for the RDS DB instance + * or Aurora DB cluster used by the target group. + * + * between 0 and MaxConnectionsPercent + * + * @default 50 + */ + readonly maxIdleConnectionsPercent?: number; + + /** + * Each item in the list represents a class of SQL operations that normally cause all later statements in a session + * using a proxy to be pinned to the same underlying database connection. + * Including an item in the list exempts that class of SQL operations from the pinning behavior. + * + * @default - no session pinning filters + */ + readonly sessionPinningFilters?: string[]; +} + +/** + * Construction properties for a DatabaseProxy + */ +export interface DatabaseProxyProps { + /** + * One or more DB cluster identifiers. + * + * @default - default + */ + readonly dbCluster?: IDatabaseCluster; + + /** + * One or more DB instance identifiers. + * + * @default - default + */ + readonly dbInstance?: IDatabaseInstance; + + /** + * The identifier for the proxy. + * This name must be unique for all proxies owned by your AWS account in the specified AWS Region. + * An identifier must begin with a letter and must contain only ASCII letters, digits, and hyphens; + * it can't end with a hyphen or contain two consecutive hyphens. + * + * @default - Generated by CloudFormation (recommended) + */ + readonly dbProxyName?: string; + + /** + * Specifies the settings that control the size and behavior of the connection pool. + * + * @default - default + */ + readonly connectionPoolConfiguration?: ConnectionPoolConfiguration; + + /** + * Whether the proxy includes detailed information about SQL statements in its logs. + * This information helps you to debug issues involving SQL behavior or the performance and scalability of the proxy connections. + * The debug information includes the text of SQL statements that you submit through the proxy. + * Thus, only enable this setting when needed for debugging, and only when you have security measures in place to safeguard any sensitive + * information that appears in the logs. + * + * @default false + */ + readonly debugLogging?: boolean; + + /** + * Whether to require or disallow AWS Identity and Access Management (IAM) authentication for connections to the proxy. + * + * @default false + */ + readonly iamAuth?: boolean; + + /** + * The number of seconds that a connection to the proxy can be inactive before the proxy disconnects it. + * You can set this value higher or lower than the connection timeout limit for the associated database. + * + * @default cdk.Duration.minutes(30) + */ + readonly idleClientTimeout?: cdk.Duration; + + /** + * The kinds of databases that the proxy can connect to. + * This value determines which database network protocol the proxy recognizes when it interprets network traffic to + * and from the database. + * The engine family applies to MySQL and PostgreSQL for both RDS and Aurora. + */ + readonly engineFamily: DatabaseProxyEngine; + + /** + * A Boolean parameter that specifies whether Transport Layer Security (TLS) encryption is required for connections to the proxy. + * By enabling this setting, you can enforce encrypted TLS connections to the proxy. + * + * @default false + */ + readonly requireTLS?: boolean; + + /** + * The secret that the proxy uses to authenticate to the RDS DB instance or Aurora DB cluster. + * These secrets are stored within Amazon Secrets Manager. + * + * @default - no secret + */ + readonly secret: secretsmanager.ISecret; + + /** + * One or more VPC security groups to associate with the new proxy. + * + * @default - No security groups + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * One or more VPC subnets to associate with the new proxy. + * + * @default - Private Subnets in VPC + */ + readonly subnets?: ec2.ISubnet[]; + + /** + * The VPC to associate with the new proxy. + */ + readonly vpc: ec2.IVpc; +} + +/** + * Properties that describe an existing DB Proxy + */ +export interface DatabaseProxyAttributes { + /** + * DB Proxy Name + */ + readonly dbProxyName: string; + + /** + * DB Proxy ARN + */ + readonly dbProxyArn: string; + + /** + * Endpoint + */ + readonly endpoint: string; + + /** + * The security groups of the instance. + */ + readonly securityGroups: ec2.ISecurityGroup[]; +} + +/** + * DB Proxy + */ +export interface IDatabaseProxy extends cdk.IResource { + /** + * DB Proxy Name + * + * @attribute + */ + readonly dbProxyName: string; + + /** + * DB Proxy ARN + * + * @attribute + */ + readonly dbProxyArn: string; + + /** + * Endpoint + * + * @attribute + */ + readonly endpoint: string; +} + +/** + * A new or imported database proxy + */ +export abstract class DatabaseProxyBase extends cdk.Resource implements IDatabaseProxy { + /** + * Import an existing database proxy. + */ + public static fromDatabaseProxyAttributes( + scope: cdk.Construct, + id: string, + attrs: DatabaseProxyAttributes, + ): IDatabaseProxy { + class Import extends DatabaseProxyBase implements IDatabaseProxy { + public readonly dbProxyName = attrs.dbProxyName; + public readonly dbProxyArn = attrs.dbProxyArn; + public readonly endpoint = attrs.endpoint; + } + return new Import(scope, id); + } + + public abstract readonly dbProxyName: string; + public abstract readonly dbProxyArn: string; + public abstract readonly endpoint: string; +} + +/** + * RDS Database Proxy + * + * @resource AWS::RDS::DBProxy + */ +export class DatabaseProxy extends DatabaseProxyBase + implements IDatabaseProxy, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { + /** + * DB Proxy Name + * + * @attribute + */ + public readonly dbProxyName: string; + + /** + * DB Proxy ARN + * + * @attribute + */ + public readonly dbProxyArn: string; + + /** + * Endpoint + * + * @attribute + */ + public readonly endpoint: string; + + /** + * Access to network connections. + */ + public readonly connections: ec2.Connections; + + protected readonly resource: CfnDBProxy; + + constructor(scope: cdk.Construct, id: string, props: DatabaseProxyProps) { + super(scope, id, { physicalName: props.dbProxyName || id }); + + if (props.dbInstance && props.dbCluster) { + throw new Error('Only one of dbInstance or dbCluster can be provided'); + } + if (!props.dbInstance && !props.dbCluster) { + throw new Error('One of dbInstance or dbCluster is required!'); + } + + const role = new iam.Role(this, 'IAMRole', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + inlinePolicies: { + 0: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: [ + 'secretsmanager:DescribeSecret', + 'secretsmanager:ListSecretVersionIds', + 'secretsmanager:GetResourcePolicy', + 'secretsmanager:GetSecretValue', + ], + resources: [props.secret.secretArn], + }), + ], + }), + }, + }); + + let vpcSubnetIds; + if (props.subnets && props.subnets.length > 0) { + vpcSubnetIds = props.subnets?.map((_) => _.subnetId); + } else { + vpcSubnetIds = props.vpc.privateSubnets.map((_) => _.subnetId); + } + + this.connections = new ec2.Connections({ securityGroups: props.securityGroups }); + + this.resource = new CfnDBProxy(this, 'Resource', { + auth: [ + { + authScheme: 'SECRETS', + iamAuth: props.iamAuth ? 'REQUIRED' : 'DISABLED', + secretArn: props.secret.secretArn, + }, + ], + dbProxyName: this.physicalName, + debugLogging: props.debugLogging, + engineFamily: props.engineFamily, + idleClientTimeout: props.idleClientTimeout?.toSeconds(), + requireTls: props.requireTLS, + roleArn: role.roleArn, + vpcSecurityGroupIds: props.securityGroups?.map((_) => _.securityGroupId), + vpcSubnetIds, + }); + + /** + * A target group is a collection of databases that the proxy can connect to. + * Currently, you can specify only one RDS DB instance or Aurora DB cluster. + */ + let dbInstanceIdentifiers; + if (props.dbCluster) { + dbInstanceIdentifiers = props.dbCluster.instanceIdentifiers; + } else if (props.dbInstance) { + dbInstanceIdentifiers = [props.dbInstance.instanceIdentifier]; + } + + let dbClusterIdentifiers; + if (props.dbCluster) { + dbClusterIdentifiers = [props.dbCluster.clusterIdentifier]; + } + + this.dbProxyName = this.resource.ref; + this.dbProxyArn = this.resource.attrDbProxyArn; + this.endpoint = this.resource.attrEndpoint; + + new CfnDBProxyTargetGroup(this, 'ProxyTargetGroup', { + dbProxyName: this.dbProxyName, + dbInstanceIdentifiers, + dbClusterIdentifiers, + connectionPoolConfigurationInfo: toConnectionPoolConfigurationInfo(props.connectionPoolConfiguration), + }); + } + + /** + * Renders the secret attachment target specifications. + */ + public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps { + return { + targetId: this.dbProxyName, + targetType: secretsmanager.AttachmentTargetType.RDS_DB_PROXY, + }; + } +} + +/** + * ConnectionPoolConfiguration (L2 => L1) + */ +function toConnectionPoolConfigurationInfo( + config?: ConnectionPoolConfiguration, +): CfnDBProxyTargetGroup.ConnectionPoolConfigurationInfoFormatProperty { + return { + connectionBorrowTimeout: config?.connectionBorrowTimeout?.toSeconds(), + initQuery: config?.initQuery, + maxConnectionsPercent: config?.maxConnectionsPercent, + maxIdleConnectionsPercent: config?.maxIdleConnectionsPercent, + sessionPinningFilters: config?.sessionPinningFilters, + }; +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json b/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json new file mode 100644 index 0000000000000..f42e75da92889 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json @@ -0,0 +1,556 @@ +{ + "Resources": { + "vpcA2121C38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc" + } + ] + } + }, + "vpcPublicSubnet1Subnet2E65531E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTable48A2DF9B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTableAssociation5D3F4579": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + } + } + }, + "vpcPublicSubnet1DefaultRoute10708846": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet1EIPDA49DCBE": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1NATGateway9C16659E": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet1EIPDA49DCBE", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet2Subnet009B674F": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableEB40D4CB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableAssociation21F81B59": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + } + } + }, + "vpcPublicSubnet2DefaultRouteA1EC0F60": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet2EIP9B3743B1": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2NATGateway9B8AE11A": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet2EIP9B3743B1", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPrivateSubnet1Subnet934893E8": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableB41A48CC": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableAssociation67945127": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + } + } + }, + "vpcPrivateSubnet1DefaultRoute1AA8E2E5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet1NATGateway9C16659E" + } + } + }, + "vpcPrivateSubnet2Subnet7031C2BA": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTable7280F23E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTableAssociation007E94D3": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + } + } + }, + "vpcPrivateSubnet2DefaultRouteB0E07F99": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet2NATGateway9B8AE11A" + } + } + }, + "vpcIGWE57CBDCA": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-proxy/vpc" + } + ] + } + }, + "vpcVPCGW7984C166": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "InternetGatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + } + }, + "dbInstanceSubnetGroupD062EC9E": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnet group for dbInstance database", + "SubnetIds": [ + { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + }, + { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + } + ] + } + }, + "dbInstanceSecurityGroupA58A00A3": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for dbInstance database", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "vpcA2121C38" + } + } + }, + "dbInstanceSecret032D3661": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "Description": { + "Fn::Join": [ + "", + [ + "Generated by the CDK for stack: ", + { + "Ref": "AWS::StackName" + } + ] + ] + }, + "GenerateSecretString": { + "ExcludeCharacters": "\"@/\\", + "GenerateStringKey": "password", + "PasswordLength": 30, + "SecretStringTemplate": "{\"username\":\"master\"}" + } + } + }, + "dbInstanceSecretAttachment88CFBDAE": { + "Type": "AWS::SecretsManager::SecretTargetAttachment", + "Properties": { + "SecretId": { + "Ref": "dbInstanceSecret032D3661" + }, + "TargetId": { + "Ref": "dbInstance4076B1EC" + }, + "TargetType": "AWS::RDS::DBInstance" + } + }, + "dbInstance4076B1EC": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t3.medium", + "AllocatedStorage": "100", + "CopyTagsToSnapshot": true, + "DBSubnetGroupName": { + "Ref": "dbInstanceSubnetGroupD062EC9E" + }, + "DeletionProtection": true, + "Engine": "postgres", + "MasterUsername": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "dbInstanceSecret032D3661" + }, + ":SecretString:username::}}" + ] + ] + }, + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "dbInstanceSecret032D3661" + }, + ":SecretString:password::}}" + ] + ] + }, + "StorageType": "gp2", + "VPCSecurityGroups": [ + { + "Fn::GetAtt": [ + "dbInstanceSecurityGroupA58A00A3", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Snapshot" + }, + "dbProxyIAMRole662F3AB8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "rds.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:DescribeSecret", + "secretsmanager:ListSecretVersionIds", + "secretsmanager:GetResourcePolicy", + "secretsmanager:GetSecretValue" + ], + "Effect": "Allow", + "Resource": { + "Ref": "dbInstanceSecretAttachment88CFBDAE" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "0" + } + ] + } + }, + "dbProxy3B89EAF2": { + "Type": "AWS::RDS::DBProxy", + "Properties": { + "Auth": [ + { + "AuthScheme": "SECRETS", + "IAMAuth": "DISABLED", + "SecretArn": { + "Ref": "dbInstanceSecretAttachment88CFBDAE" + } + } + ], + "DBProxyName": "dbProxy", + "EngineFamily": "POSTGRESQL", + "RoleArn": { + "Fn::GetAtt": [ + "dbProxyIAMRole662F3AB8", + "Arn" + ] + }, + "VpcSubnetIds": [ + { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + }, + { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + } + ] + } + }, + "dbProxyProxyTargetGroup8DA26A77": { + "Type": "AWS::RDS::DBProxyTargetGroup", + "Properties": { + "DBProxyName": { + "Ref": "dbProxy3B89EAF2" + }, + "ConnectionPoolConfigurationInfo": { + "ConnectionBorrowTimeout": 30, + "MaxConnectionsPercent": 50 + }, + "DBInstanceIdentifiers": [ + { + "Ref": "dbInstance4076B1EC" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts new file mode 100644 index 0000000000000..48bae3bd8bb35 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts @@ -0,0 +1,28 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as rds from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-rds-proxy'); + +const vpc = new ec2.Vpc(stack, 'vpc', { maxAzs: 2 }); + +const dbInstance = new rds.DatabaseInstance(stack, 'dbInstance', { + engine: rds.DatabaseInstanceEngine.POSTGRES, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM), + masterUsername: 'master', + vpc, +}); + +new rds.DatabaseProxy(stack, 'dbProxy', { + connectionPoolConfiguration: { + connectionBorrowTimeout: cdk.Duration.seconds(30), + maxConnectionsPercent: 50, + }, + dbInstance, + engineFamily: rds.DatabaseProxyEngine.POSTGRESQL, + secret: dbInstance.secret!, + vpc, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 91cf18a7a8229..9e5084ee8ef49 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -360,6 +360,11 @@ export enum AttachmentTargetType { */ RDS_DB_CLUSTER = 'AWS::RDS::DBCluster', + /** + * AWS::RDS::DBProxy + */ + RDS_DB_PROXY = 'AWS::RDS::DBProxy', + /** * AWS::Redshift::Cluster */ diff --git a/packages/aws-cdk/test/init.test.ts b/packages/aws-cdk/test/init.test.ts index 0dc7bae16aec1..62839687db7e1 100644 --- a/packages/aws-cdk/test/init.test.ts +++ b/packages/aws-cdk/test/init.test.ts @@ -48,7 +48,7 @@ cliTest('git directory does not throw off the initer!', async (workDir) => { expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); -test('verify "future flags" are added to cdk.json', async () => { +xtest('verify "future flags" are added to cdk.json', async () => { // This is a lot to test, and it can be slow-ish, especially when ran with other tests. jest.setTimeout(30_000); From 8ff44186cb44655cb80624ad7867bf8a7df42540 Mon Sep 17 00:00:00 2001 From: Hyeonsoo Lee Date: Wed, 17 Jun 2020 18:56:07 +0900 Subject: [PATCH 2/7] feat(rds): Add L2 DatabaseProxy (AWS::RDS::DBProxy) closes #8475 --- packages/@aws-cdk/aws-rds/README.md | 14 +- packages/@aws-cdk/aws-rds/lib/cluster-ref.ts | 6 + packages/@aws-cdk/aws-rds/lib/cluster.ts | 11 ++ packages/@aws-cdk/aws-rds/lib/instance.ts | 16 ++ packages/@aws-cdk/aws-rds/lib/proxy.ts | 163 ++++++++++-------- packages/@aws-cdk/aws-rds/test/integ.proxy.ts | 8 +- packages/@aws-cdk/aws-rds/test/test.proxy.ts | 154 +++++++++++++++++ packages/aws-cdk/test/init.test.ts | 2 +- 8 files changed, 286 insertions(+), 88 deletions(-) create mode 100644 packages/@aws-cdk/aws-rds/test/test.proxy.ts diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index dfe2623c172bf..f3afa2b46cff0 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -192,7 +192,10 @@ new DatabaseCluster(this, 'dbcluster', { ### Creating a Database Proxy -For RDS Proxy, See [Amazon RDS Proxy](https://aws.amazon.com/rds/proxy/) +Amazon RDS Proxy sits between your application and your relational database to efficiently manage +connections to the database and improve scalability of the application. Learn more about at [Amazon RDS Proxy](https://aws.amazon.com/rds/proxy/) + +The following code configures an RDS Proxy for a `DatabaseInstance`. ```ts import * as cdk from '@aws-cdk/core'; @@ -205,20 +208,13 @@ const securityGroup: ec2.ISecurityGroup = ...; const secret: secrets.ISecret = ...; const dbInstance: rds.IDatabaseInstance = ...; -const proxy = new rds.DatabaseProxy(this, 'proxy', { +const proxy = dbInstance.addProxy('proxy', { connectionPoolConfiguration: { connectionBorrowTimeout: cdk.Duration.seconds(30), maxConnectionsPercent: 50, }, - dbInstance, engineFamily: rds.DatabaseProxyEngine.POSTGRESQL, secret, vpc, }); - -... - -proxy.dbProxyName; -proxy.dbProxyArn; -proxy.endpoint; ``` diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts b/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts index 20e0d66b499eb..0db0389b9cb67 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts @@ -2,6 +2,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import { IResource } from '@aws-cdk/core'; import { Endpoint } from './endpoint'; +import { DatabaseProxy, DatabaseProxyOptions } from './proxy'; /** * Create a clustered database with a given number of instances. @@ -33,6 +34,11 @@ export interface IDatabaseCluster extends IResource, ec2.IConnectable, secretsma * Endpoints which address each individual replica. */ readonly instanceEndpoints: Endpoint[]; + + /** + * Add a new db proxy to this cluster. + */ + addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy; } /** diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index b11bdb40a84ff..51392769e64dd 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -9,6 +9,7 @@ import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; import { ClusterParameterGroup, IParameterGroup } from './parameter-group'; import { BackupProps, DatabaseClusterEngine, InstanceProps, Login, RotationMultiUserOptions } from './props'; +import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy'; import { CfnDBCluster, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated'; /** @@ -239,6 +240,16 @@ abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster */ public abstract readonly connections: ec2.Connections; + /** + * Add a new db proxy to this cluster. + */ + public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { + return new DatabaseProxy(this, id, { + proxyTarget: ProxyTarget.fromCluster(this), + ...options, + }); + } + /** * Renders the secret attachment target specifications. */ diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 7af0ebe13a58c..c2fc8d4b0e8b7 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -11,6 +11,7 @@ import { Endpoint } from './endpoint'; import { IOptionGroup } from './option-group'; import { IParameterGroup } from './parameter-group'; import { DatabaseClusterEngine, RotationMultiUserOptions } from './props'; +import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy'; import { CfnDBInstance, CfnDBInstanceProps, CfnDBSubnetGroup } from './rds.generated'; /** @@ -46,6 +47,11 @@ export interface IDatabaseInstance extends IResource, ec2.IConnectable, secretsm */ readonly instanceEndpoint: Endpoint; + /** + * Add a new db proxy to this instance. + */ + addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy; + /** * Defines a CloudWatch event rule which triggers for instance events. Use * `rule.addEventPattern(pattern)` to specify a filter. @@ -111,6 +117,16 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase */ public abstract readonly connections: ec2.Connections; + /** + * Add a new db proxy to this instance. + */ + public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { + return new DatabaseProxy(this, id, { + proxyTarget: ProxyTarget.fromInstance(this), + ...options, + }); + } + /** * Defines a CloudWatch event rule which triggers for instance events. Use * `rule.addEventPattern(pattern)` to specify a filter. diff --git a/packages/@aws-cdk/aws-rds/lib/proxy.ts b/packages/@aws-cdk/aws-rds/lib/proxy.ts index b602cddc897cd..a3d93b1f7d341 100644 --- a/packages/@aws-cdk/aws-rds/lib/proxy.ts +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -12,7 +12,7 @@ import { CfnDBProxy, CfnDBProxyTargetGroup } from './rds.generated'; * and from the database. * The engine family applies to MySQL and PostgreSQL for both RDS and Aurora. */ -export enum DatabaseProxyEngine { +export enum ProxyEngineFamily { /** * MYSQL */ @@ -24,20 +24,34 @@ export enum DatabaseProxyEngine { POSTGRESQL = 'POSTGRESQL', } +/** + * SessionPinningFilter + * + * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-pinning + */ +export enum SessionPinningFilter { + /** + * You can opt out of session pinning for the following kinds of application statements: + * + * - Setting session variables and configuration settings. + */ + EXCLUDE_VARIABLE_SETS = 'EXCLUDE_VARIABLE_SETS' +} + /** * Specifies the settings that control the size and behavior of the connection pool. */ -export interface ConnectionPoolConfiguration { +export interface ProxyConnectionPool { /** - * The number of seconds for a proxy to wait for a connection to become available in the connection pool. + * The duration for a proxy to wait for a connection to become available in the connection pool. * Only applies when the proxy has opened its maximum number of connections and all connections are busy with client * sessions. * - * between 1 and 3600, or 0 representing unlimited + * Value must be between 1 second and 1 hour, or `Duration.seconds(0)` to represent unlimited. * * @default cdk.Duration.seconds(120) */ - readonly connectionBorrowTimeout?: cdk.Duration; + readonly borrowTimeout?: cdk.Duration; /** * One or more SQL statements for the proxy to run when opening each new database connection. @@ -84,27 +98,53 @@ export interface ConnectionPoolConfiguration { * * @default - no session pinning filters */ - readonly sessionPinningFilters?: string[]; + readonly sessionPinningFilters?: SessionPinningFilter[]; } /** - * Construction properties for a DatabaseProxy + * Proxy target: Instance or Cluster + * + * A target group is a collection of databases that the proxy can connect to. + * Currently, you can specify only one RDS DB instance or Aurora DB cluster. */ -export interface DatabaseProxyProps { +export class ProxyTarget { /** - * One or more DB cluster identifiers. + * From instance * - * @default - default + * @param instance RDS database instance */ - readonly dbCluster?: IDatabaseCluster; + public static fromInstance(instance: IDatabaseInstance): ProxyTarget { + return new ProxyTarget([instance.instanceIdentifier]); + } /** - * One or more DB instance identifiers. + * From cluster * - * @default - default + * @param cluster RDS database cluster */ - readonly dbInstance?: IDatabaseInstance; + public static fromCluster(cluster: IDatabaseCluster): ProxyTarget { + return new ProxyTarget(cluster.instanceIdentifiers, [cluster.clusterIdentifier]); + } + private constructor( + /** + * One or more DB instance identifiers. + */ + public readonly dbInstanceIdentifiers: string[], + + /** + * One or more DB cluster identifiers. + * + * @default undefined + */ + public readonly dbClusterIdentifiers?: string[], + ) {} +} + +/** + * Options for a new DatabaseProxy + */ +export interface DatabaseProxyOptions { /** * The identifier for the proxy. * This name must be unique for all proxies owned by your AWS account in the specified AWS Region. @@ -120,7 +160,7 @@ export interface DatabaseProxyProps { * * @default - default */ - readonly connectionPoolConfiguration?: ConnectionPoolConfiguration; + readonly connectionPool?: ProxyConnectionPool; /** * Whether the proxy includes detailed information about SQL statements in its logs. @@ -154,7 +194,7 @@ export interface DatabaseProxyProps { * and from the database. * The engine family applies to MySQL and PostgreSQL for both RDS and Aurora. */ - readonly engineFamily: DatabaseProxyEngine; + readonly engineFamily: ProxyEngineFamily; /** * A Boolean parameter that specifies whether Transport Layer Security (TLS) encryption is required for connections to the proxy. @@ -180,11 +220,11 @@ export interface DatabaseProxyProps { readonly securityGroups?: ec2.ISecurityGroup[]; /** - * One or more VPC subnets to associate with the new proxy. + * The subnets used by the proxy. * - * @default - Private Subnets in VPC + * @default - the VPC default strategy if not specified. */ - readonly subnets?: ec2.ISubnet[]; + readonly vpcSubnets?: ec2.SubnetSelection; /** * The VPC to associate with the new proxy. @@ -192,6 +232,16 @@ export interface DatabaseProxyProps { readonly vpc: ec2.IVpc; } +/** + * Construction properties for a DatabaseProxy + */ +export interface DatabaseProxyProps extends DatabaseProxyOptions { + /** + * DB proxy target: Instance or Cluster + */ + readonly proxyTarget: ProxyTarget +} + /** * Properties that describe an existing DB Proxy */ @@ -244,9 +294,12 @@ export interface IDatabaseProxy extends cdk.IResource { } /** - * A new or imported database proxy + * RDS Database Proxy + * + * @resource AWS::RDS::DBProxy */ -export abstract class DatabaseProxyBase extends cdk.Resource implements IDatabaseProxy { +export class DatabaseProxy extends cdk.Resource + implements IDatabaseProxy, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { /** * Import an existing database proxy. */ @@ -255,7 +308,7 @@ export abstract class DatabaseProxyBase extends cdk.Resource implements IDatabas id: string, attrs: DatabaseProxyAttributes, ): IDatabaseProxy { - class Import extends DatabaseProxyBase implements IDatabaseProxy { + class Import extends cdk.Resource implements IDatabaseProxy { public readonly dbProxyName = attrs.dbProxyName; public readonly dbProxyArn = attrs.dbProxyArn; public readonly endpoint = attrs.endpoint; @@ -263,18 +316,6 @@ export abstract class DatabaseProxyBase extends cdk.Resource implements IDatabas return new Import(scope, id); } - public abstract readonly dbProxyName: string; - public abstract readonly dbProxyArn: string; - public abstract readonly endpoint: string; -} - -/** - * RDS Database Proxy - * - * @resource AWS::RDS::DBProxy - */ -export class DatabaseProxy extends DatabaseProxyBase - implements IDatabaseProxy, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { /** * DB Proxy Name * @@ -306,13 +347,6 @@ export class DatabaseProxy extends DatabaseProxyBase constructor(scope: cdk.Construct, id: string, props: DatabaseProxyProps) { super(scope, id, { physicalName: props.dbProxyName || id }); - if (props.dbInstance && props.dbCluster) { - throw new Error('Only one of dbInstance or dbCluster can be provided'); - } - if (!props.dbInstance && !props.dbCluster) { - throw new Error('One of dbInstance or dbCluster is required!'); - } - const role = new iam.Role(this, 'IAMRole', { assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), inlinePolicies: { @@ -332,13 +366,6 @@ export class DatabaseProxy extends DatabaseProxyBase }, }); - let vpcSubnetIds; - if (props.subnets && props.subnets.length > 0) { - vpcSubnetIds = props.subnets?.map((_) => _.subnetId); - } else { - vpcSubnetIds = props.vpc.privateSubnets.map((_) => _.subnetId); - } - this.connections = new ec2.Connections({ securityGroups: props.securityGroups }); this.resource = new CfnDBProxy(this, 'Resource', { @@ -355,35 +382,20 @@ export class DatabaseProxy extends DatabaseProxyBase idleClientTimeout: props.idleClientTimeout?.toSeconds(), requireTls: props.requireTLS, roleArn: role.roleArn, - vpcSecurityGroupIds: props.securityGroups?.map((_) => _.securityGroupId), - vpcSubnetIds, + vpcSecurityGroupIds: props.securityGroups?.map(_ => _.securityGroupId), + vpcSubnetIds: props.vpc.selectSubnets(props.vpcSubnets).subnetIds, }); - /** - * A target group is a collection of databases that the proxy can connect to. - * Currently, you can specify only one RDS DB instance or Aurora DB cluster. - */ - let dbInstanceIdentifiers; - if (props.dbCluster) { - dbInstanceIdentifiers = props.dbCluster.instanceIdentifiers; - } else if (props.dbInstance) { - dbInstanceIdentifiers = [props.dbInstance.instanceIdentifier]; - } - - let dbClusterIdentifiers; - if (props.dbCluster) { - dbClusterIdentifiers = [props.dbCluster.clusterIdentifier]; - } - this.dbProxyName = this.resource.ref; this.dbProxyArn = this.resource.attrDbProxyArn; this.endpoint = this.resource.attrEndpoint; + const { dbInstanceIdentifiers, dbClusterIdentifiers } = props.proxyTarget; new CfnDBProxyTargetGroup(this, 'ProxyTargetGroup', { dbProxyName: this.dbProxyName, dbInstanceIdentifiers, dbClusterIdentifiers, - connectionPoolConfigurationInfo: toConnectionPoolConfigurationInfo(props.connectionPoolConfiguration), + connectionPoolConfigurationInfo: toConnectionPoolConfigurationInfo(props.connectionPool), }); } @@ -402,13 +414,16 @@ export class DatabaseProxy extends DatabaseProxyBase * ConnectionPoolConfiguration (L2 => L1) */ function toConnectionPoolConfigurationInfo( - config?: ConnectionPoolConfiguration, -): CfnDBProxyTargetGroup.ConnectionPoolConfigurationInfoFormatProperty { + config?: ProxyConnectionPool, +): undefined | CfnDBProxyTargetGroup.ConnectionPoolConfigurationInfoFormatProperty { + if (!config) { + return undefined; + } return { - connectionBorrowTimeout: config?.connectionBorrowTimeout?.toSeconds(), - initQuery: config?.initQuery, - maxConnectionsPercent: config?.maxConnectionsPercent, - maxIdleConnectionsPercent: config?.maxIdleConnectionsPercent, - sessionPinningFilters: config?.sessionPinningFilters, + connectionBorrowTimeout: config.borrowTimeout?.toSeconds(), + initQuery: config.initQuery, + maxConnectionsPercent: config.maxConnectionsPercent, + maxIdleConnectionsPercent: config.maxIdleConnectionsPercent, + sessionPinningFilters: config.sessionPinningFilters, }; } diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts index 48bae3bd8bb35..44c57d254c0e7 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts @@ -15,13 +15,13 @@ const dbInstance = new rds.DatabaseInstance(stack, 'dbInstance', { }); new rds.DatabaseProxy(stack, 'dbProxy', { - connectionPoolConfiguration: { - connectionBorrowTimeout: cdk.Duration.seconds(30), + connectionPool: { + borrowTimeout: cdk.Duration.seconds(30), maxConnectionsPercent: 50, }, - dbInstance, - engineFamily: rds.DatabaseProxyEngine.POSTGRESQL, + engineFamily: rds.ProxyEngineFamily.POSTGRESQL, secret: dbInstance.secret!, + proxyTarget: rds.ProxyTarget.fromInstance(dbInstance), vpc, }); diff --git a/packages/@aws-cdk/aws-rds/test/test.proxy.ts b/packages/@aws-cdk/aws-rds/test/test.proxy.ts new file mode 100644 index 0000000000000..742f78621bb3b --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/test.proxy.ts @@ -0,0 +1,154 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as rds from '../lib'; + +export = { + 'create a DB proxy from an instance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.MYSQL, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + masterUsername: 'admin', + vpc, + }); + + // WHEN + new rds.DatabaseProxy(stack, 'Proxy', { + engineFamily: rds.ProxyEngineFamily.MYSQL, + proxyTarget: rds.ProxyTarget.fromInstance(instance), + secret: instance.secret!, + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBProxy', { + Properties: { + Auth: [ + { + AuthScheme: 'SECRETS', + IAMAuth: 'DISABLED', + SecretArn: { + Ref: 'InstanceSecretAttachment83BEE581', + }, + }, + ], + DBProxyName: 'Proxy', + EngineFamily: 'MYSQL', + RoleArn: { + 'Fn::GetAtt': [ + 'ProxyIAMRole2FE8AB0F', + 'Arn', + ], + }, + VpcSubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + }, + }, ResourcePart.CompleteDefinition)); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBProxyTargetGroup', { + Properties: { + DBProxyName: { + Ref: 'ProxyCB0DFB71', + }, + DBInstanceIdentifiers: [ + { + Ref: 'InstanceC1063A87', + }, + ], + }, + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + 'create a DB proxy from a cluster'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new rds.DatabaseCluster(stack, 'Database', { + engine: rds.DatabaseClusterEngine.AURORA_POSTGRESQL, + engineVersion: '10.7', + masterUser: { + username: 'admin', + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + }); + + // WHEN + new rds.DatabaseProxy(stack, 'Proxy', { + engineFamily: rds.ProxyEngineFamily.POSTGRESQL, + proxyTarget: rds.ProxyTarget.fromCluster(cluster), + secret: cluster.secret!, + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBProxy', { + Properties: { + Auth: [ + { + AuthScheme: 'SECRETS', + IAMAuth: 'DISABLED', + SecretArn: { + Ref: 'DatabaseSecretAttachmentE5D1B020', + }, + }, + ], + DBProxyName: 'Proxy', + EngineFamily: 'POSTGRESQL', + RoleArn: { + 'Fn::GetAtt': [ + 'ProxyIAMRole2FE8AB0F', + 'Arn', + ], + }, + VpcSubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + }, + }, ResourcePart.CompleteDefinition)); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBProxyTargetGroup', { + Properties: { + DBProxyName: { + Ref: 'ProxyCB0DFB71', + }, + DBClusterIdentifiers: [ + { + Ref: 'DatabaseB269D8BB', + }, + ], + DBInstanceIdentifiers: [ + { + Ref: 'DatabaseInstance1844F58FD', + }, + { + Ref: 'DatabaseInstance2AA380DEE', + }, + ], + }, + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, +}; diff --git a/packages/aws-cdk/test/init.test.ts b/packages/aws-cdk/test/init.test.ts index 62839687db7e1..0dc7bae16aec1 100644 --- a/packages/aws-cdk/test/init.test.ts +++ b/packages/aws-cdk/test/init.test.ts @@ -48,7 +48,7 @@ cliTest('git directory does not throw off the initer!', async (workDir) => { expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); -xtest('verify "future flags" are added to cdk.json', async () => { +test('verify "future flags" are added to cdk.json', async () => { // This is a lot to test, and it can be slow-ish, especially when ran with other tests. jest.setTimeout(30_000); From 31ca83cc28d8d449cbb6fca441931afb5a2cd274 Mon Sep 17 00:00:00 2001 From: Hyeonsoo David Lee Date: Thu, 25 Jun 2020 02:43:21 +0900 Subject: [PATCH 3/7] feat(rds): database proxy --- packages/@aws-cdk/aws-rds/README.md | 1 - packages/@aws-cdk/aws-rds/lib/proxy.ts | 93 ++++++++++++------- .../aws-rds/test/integ.proxy.expected.json | 44 +++++---- packages/@aws-cdk/aws-rds/test/integ.proxy.ts | 1 - packages/@aws-cdk/aws-rds/test/test.proxy.ts | 2 - 5 files changed, 85 insertions(+), 56 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index f3afa2b46cff0..8d6a481ba2444 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -213,7 +213,6 @@ const proxy = dbInstance.addProxy('proxy', { connectionBorrowTimeout: cdk.Duration.seconds(30), maxConnectionsPercent: 50, }, - engineFamily: rds.DatabaseProxyEngine.POSTGRESQL, secret, vpc, }); diff --git a/packages/@aws-cdk/aws-rds/lib/proxy.ts b/packages/@aws-cdk/aws-rds/lib/proxy.ts index a3d93b1f7d341..0acb20f00a4a4 100644 --- a/packages/@aws-cdk/aws-rds/lib/proxy.ts +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -4,7 +4,7 @@ import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; import { IDatabaseCluster } from './cluster-ref'; import { IDatabaseInstance } from './instance'; -import { CfnDBProxy, CfnDBProxyTargetGroup } from './rds.generated'; +import { CfnDBCluster, CfnDBInstance, CfnDBProxy, CfnDBProxyTargetGroup } from './rds.generated'; /** * The kinds of databases that the proxy can connect to. @@ -29,13 +29,27 @@ export enum ProxyEngineFamily { * * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-pinning */ -export enum SessionPinningFilter { +export class SessionPinningFilter { /** * You can opt out of session pinning for the following kinds of application statements: * * - Setting session variables and configuration settings. */ - EXCLUDE_VARIABLE_SETS = 'EXCLUDE_VARIABLE_SETS' + public static readonly EXCLUDE_VARIABLE_SETS = new SessionPinningFilter('EXCLUDE_VARIABLE_SETS'); + + /** + * custom filter + */ + public static of(filterName: string): SessionPinningFilter { + return new SessionPinningFilter(filterName); + } + + private constructor( + /** + * Filter name + */ + public readonly filterName: string, + ) {} } /** @@ -114,7 +128,9 @@ export class ProxyTarget { * @param instance RDS database instance */ public static fromInstance(instance: IDatabaseInstance): ProxyTarget { - return new ProxyTarget([instance.instanceIdentifier]); + const _instance = instance.node.defaultChild as CfnDBInstance; + const engine = ProxyTarget.fromEngine(_instance.engine!); + return new ProxyTarget(engine, [instance.instanceIdentifier]); } /** @@ -123,10 +139,37 @@ export class ProxyTarget { * @param cluster RDS database cluster */ public static fromCluster(cluster: IDatabaseCluster): ProxyTarget { - return new ProxyTarget(cluster.instanceIdentifiers, [cluster.clusterIdentifier]); + const _cluster = cluster.node.defaultChild as CfnDBCluster; + const engine = ProxyTarget.fromEngine(_cluster.engine); + return new ProxyTarget(engine, cluster.instanceIdentifiers, [cluster.clusterIdentifier]); + } + + /** + * @deprecated - will be removed after https://github.com/aws/aws-cdk/pull/8686 + */ + private static fromEngine(engine: string): ProxyEngineFamily { + switch (engine) { + case 'aurora': + case 'aurora-mysql': + case 'mysql': + return ProxyEngineFamily.MYSQL; + case 'aurora-postgresql': + case 'postgres': + return ProxyEngineFamily.POSTGRESQL; + default: + throw new Error(`Unsupported engine type - ${engine}`); + } } private constructor( + /** + * The kinds of databases that the proxy can connect to. + * This value determines which database network protocol the proxy recognizes when it interprets network traffic to + * and from the database. + * The engine family applies to MySQL and PostgreSQL for both RDS and Aurora. + */ + public readonly engineFamily: ProxyEngineFamily, + /** * One or more DB instance identifiers. */ @@ -188,14 +231,6 @@ export interface DatabaseProxyOptions { */ readonly idleClientTimeout?: cdk.Duration; - /** - * The kinds of databases that the proxy can connect to. - * This value determines which database network protocol the proxy recognizes when it interprets network traffic to - * and from the database. - * The engine family applies to MySQL and PostgreSQL for both RDS and Aurora. - */ - readonly engineFamily: ProxyEngineFamily; - /** * A Boolean parameter that specifies whether Transport Layer Security (TLS) encryption is required for connections to the proxy. * By enabling this setting, you can enforce encrypted TLS connections to the proxy. @@ -204,6 +239,13 @@ export interface DatabaseProxyOptions { */ readonly requireTLS?: boolean; + /** + * IAM role that the proxy uses to access secrets in AWS Secrets Manager. + * + * @default - A role will automatically be created + */ + readonly role?: iam.IRole; + /** * The secret that the proxy uses to authenticate to the RDS DB instance or Aurora DB cluster. * These secrets are stored within Amazon Secrets Manager. @@ -342,30 +384,17 @@ export class DatabaseProxy extends cdk.Resource */ public readonly connections: ec2.Connections; - protected readonly resource: CfnDBProxy; + private readonly resource: CfnDBProxy; constructor(scope: cdk.Construct, id: string, props: DatabaseProxyProps) { super(scope, id, { physicalName: props.dbProxyName || id }); - const role = new iam.Role(this, 'IAMRole', { + const role = props.role || new iam.Role(this, 'IAMRole', { assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), - inlinePolicies: { - 0: new iam.PolicyDocument({ - statements: [ - new iam.PolicyStatement({ - actions: [ - 'secretsmanager:DescribeSecret', - 'secretsmanager:ListSecretVersionIds', - 'secretsmanager:GetResourcePolicy', - 'secretsmanager:GetSecretValue', - ], - resources: [props.secret.secretArn], - }), - ], - }), - }, }); + props.secret.grantRead(role); + this.connections = new ec2.Connections({ securityGroups: props.securityGroups }); this.resource = new CfnDBProxy(this, 'Resource', { @@ -378,7 +407,7 @@ export class DatabaseProxy extends cdk.Resource ], dbProxyName: this.physicalName, debugLogging: props.debugLogging, - engineFamily: props.engineFamily, + engineFamily: props.proxyTarget.engineFamily, idleClientTimeout: props.idleClientTimeout?.toSeconds(), requireTls: props.requireTLS, roleArn: role.roleArn, @@ -424,6 +453,6 @@ function toConnectionPoolConfigurationInfo( initQuery: config.initQuery, maxConnectionsPercent: config.maxConnectionsPercent, maxIdleConnectionsPercent: config.maxIdleConnectionsPercent, - sessionPinningFilters: config.sessionPinningFilters, + sessionPinningFilters: config.sessionPinningFilters?.map(_ => _.filterName), }; } diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json b/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json index f42e75da92889..45a1e58e9ff1f 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json @@ -480,27 +480,31 @@ } ], "Version": "2012-10-17" + } + } + }, + "dbProxyIAMRoleDefaultPolicy99AB98F3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "dbInstanceSecretAttachment88CFBDAE" + } + } + ], + "Version": "2012-10-17" }, - "Policies": [ + "PolicyName": "dbProxyIAMRoleDefaultPolicy99AB98F3", + "Roles": [ { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "secretsmanager:DescribeSecret", - "secretsmanager:ListSecretVersionIds", - "secretsmanager:GetResourcePolicy", - "secretsmanager:GetSecretValue" - ], - "Effect": "Allow", - "Resource": { - "Ref": "dbInstanceSecretAttachment88CFBDAE" - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "0" + "Ref": "dbProxyIAMRole662F3AB8" } ] } @@ -553,4 +557,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts index 44c57d254c0e7..a5766380f327a 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts @@ -19,7 +19,6 @@ new rds.DatabaseProxy(stack, 'dbProxy', { borrowTimeout: cdk.Duration.seconds(30), maxConnectionsPercent: 50, }, - engineFamily: rds.ProxyEngineFamily.POSTGRESQL, secret: dbInstance.secret!, proxyTarget: rds.ProxyTarget.fromInstance(dbInstance), vpc, diff --git a/packages/@aws-cdk/aws-rds/test/test.proxy.ts b/packages/@aws-cdk/aws-rds/test/test.proxy.ts index 742f78621bb3b..3591c6ad80586 100644 --- a/packages/@aws-cdk/aws-rds/test/test.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/test.proxy.ts @@ -18,7 +18,6 @@ export = { // WHEN new rds.DatabaseProxy(stack, 'Proxy', { - engineFamily: rds.ProxyEngineFamily.MYSQL, proxyTarget: rds.ProxyTarget.fromInstance(instance), secret: instance.secret!, vpc, @@ -90,7 +89,6 @@ export = { // WHEN new rds.DatabaseProxy(stack, 'Proxy', { - engineFamily: rds.ProxyEngineFamily.POSTGRESQL, proxyTarget: rds.ProxyTarget.fromCluster(cluster), secret: cluster.secret!, vpc, From f51fbeebb660b7af6dda4bdb3b47f17f41e7f678 Mon Sep 17 00:00:00 2001 From: Hyeonsoo Lee Date: Fri, 26 Jun 2020 10:29:30 +0900 Subject: [PATCH 4/7] feat(rds): database proxy --- packages/@aws-cdk/aws-rds/README.md | 6 +- packages/@aws-cdk/aws-rds/lib/proxy.ts | 143 ++++++++---------- .../aws-rds/test/integ.proxy.expected.json | 1 + packages/@aws-cdk/aws-rds/test/integ.proxy.ts | 6 +- packages/@aws-cdk/aws-rds/test/test.proxy.ts | 4 + 5 files changed, 73 insertions(+), 87 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 8d6a481ba2444..7b73fdfe92373 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -209,10 +209,8 @@ const secret: secrets.ISecret = ...; const dbInstance: rds.IDatabaseInstance = ...; const proxy = dbInstance.addProxy('proxy', { - connectionPoolConfiguration: { - connectionBorrowTimeout: cdk.Duration.seconds(30), - maxConnectionsPercent: 50, - }, + connectionBorrowTimeout: cdk.Duration.seconds(30), + maxConnectionsPercent: 50, secret, vpc, }); diff --git a/packages/@aws-cdk/aws-rds/lib/proxy.ts b/packages/@aws-cdk/aws-rds/lib/proxy.ts index 0acb20f00a4a4..f008e71bdd256 100644 --- a/packages/@aws-cdk/aws-rds/lib/proxy.ts +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -52,69 +52,6 @@ export class SessionPinningFilter { ) {} } -/** - * Specifies the settings that control the size and behavior of the connection pool. - */ -export interface ProxyConnectionPool { - /** - * The duration for a proxy to wait for a connection to become available in the connection pool. - * Only applies when the proxy has opened its maximum number of connections and all connections are busy with client - * sessions. - * - * Value must be between 1 second and 1 hour, or `Duration.seconds(0)` to represent unlimited. - * - * @default cdk.Duration.seconds(120) - */ - readonly borrowTimeout?: cdk.Duration; - - /** - * One or more SQL statements for the proxy to run when opening each new database connection. - * Typically used with SET statements to make sure that each connection has identical settings such as time zone - * and character set. - * For multiple statements, use semicolons as the separator. - * You can also include multiple variables in a single SET statement, such as SET x=1, y=2. - * - * not currently supported for PostgreSQL. - * - * @default - no initialization query - */ - readonly initQuery?: string; - - /** - * The maximum size of the connection pool for each target in a target group. - * For Aurora MySQL, it is expressed as a percentage of the max_connections setting for the RDS DB instance or Aurora DB - * cluster used by the target group. - * - * 1-100 - * - * @default 100 - */ - readonly maxConnectionsPercent?: number; - - /** - * Controls how actively the proxy closes idle database connections in the connection pool. - * A high value enables the proxy to leave a high percentage of idle connections open. - * A low value causes the proxy to close idle client connections and return the underlying database connections - * to the connection pool. - * For Aurora MySQL, it is expressed as a percentage of the max_connections setting for the RDS DB instance - * or Aurora DB cluster used by the target group. - * - * between 0 and MaxConnectionsPercent - * - * @default 50 - */ - readonly maxIdleConnectionsPercent?: number; - - /** - * Each item in the list represents a class of SQL operations that normally cause all later statements in a session - * using a proxy to be pinned to the same underlying database connection. - * Including an item in the list exempts that class of SQL operations from the pinning behavior. - * - * @default - no session pinning filters - */ - readonly sessionPinningFilters?: SessionPinningFilter[]; -} - /** * Proxy target: Instance or Cluster * @@ -199,11 +136,62 @@ export interface DatabaseProxyOptions { readonly dbProxyName?: string; /** - * Specifies the settings that control the size and behavior of the connection pool. + * The duration for a proxy to wait for a connection to become available in the connection pool. + * Only applies when the proxy has opened its maximum number of connections and all connections are busy with client + * sessions. + * + * Value must be between 1 second and 1 hour, or `Duration.seconds(0)` to represent unlimited. + * + * @default cdk.Duration.seconds(120) + */ + readonly borrowTimeout?: cdk.Duration; + + /** + * One or more SQL statements for the proxy to run when opening each new database connection. + * Typically used with SET statements to make sure that each connection has identical settings such as time zone + * and character set. + * For multiple statements, use semicolons as the separator. + * You can also include multiple variables in a single SET statement, such as SET x=1, y=2. + * + * not currently supported for PostgreSQL. + * + * @default - no initialization query + */ + readonly initQuery?: string; + + /** + * The maximum size of the connection pool for each target in a target group. + * For Aurora MySQL, it is expressed as a percentage of the max_connections setting for the RDS DB instance or Aurora DB + * cluster used by the target group. + * + * 1-100 + * + * @default 100 + */ + readonly maxConnectionsPercent?: number; + + /** + * Controls how actively the proxy closes idle database connections in the connection pool. + * A high value enables the proxy to leave a high percentage of idle connections open. + * A low value causes the proxy to close idle client connections and return the underlying database connections + * to the connection pool. + * For Aurora MySQL, it is expressed as a percentage of the max_connections setting for the RDS DB instance + * or Aurora DB cluster used by the target group. * - * @default - default + * between 0 and MaxConnectionsPercent + * + * @default 50 + */ + readonly maxIdleConnectionsPercent?: number; + + /** + * Each item in the list represents a class of SQL operations that normally cause all later statements in a session + * using a proxy to be pinned to the same underlying database connection. + * Including an item in the list exempts that class of SQL operations from the pinning behavior. + * + * @default - no session pinning filters */ - readonly connectionPool?: ProxyConnectionPool; + readonly sessionPinningFilters?: SessionPinningFilter[]; /** * Whether the proxy includes detailed information about SQL statements in its logs. @@ -235,7 +223,7 @@ export interface DatabaseProxyOptions { * A Boolean parameter that specifies whether Transport Layer Security (TLS) encryption is required for connections to the proxy. * By enabling this setting, you can enforce encrypted TLS connections to the proxy. * - * @default false + * @default true */ readonly requireTLS?: boolean; @@ -409,7 +397,7 @@ export class DatabaseProxy extends cdk.Resource debugLogging: props.debugLogging, engineFamily: props.proxyTarget.engineFamily, idleClientTimeout: props.idleClientTimeout?.toSeconds(), - requireTls: props.requireTLS, + requireTls: props.requireTLS ?? true, roleArn: role.roleArn, vpcSecurityGroupIds: props.securityGroups?.map(_ => _.securityGroupId), vpcSubnetIds: props.vpc.selectSubnets(props.vpcSubnets).subnetIds, @@ -424,7 +412,7 @@ export class DatabaseProxy extends cdk.Resource dbProxyName: this.dbProxyName, dbInstanceIdentifiers, dbClusterIdentifiers, - connectionPoolConfigurationInfo: toConnectionPoolConfigurationInfo(props.connectionPool), + connectionPoolConfigurationInfo: toConnectionPoolConfigurationInfo(props), }); } @@ -443,16 +431,13 @@ export class DatabaseProxy extends cdk.Resource * ConnectionPoolConfiguration (L2 => L1) */ function toConnectionPoolConfigurationInfo( - config?: ProxyConnectionPool, -): undefined | CfnDBProxyTargetGroup.ConnectionPoolConfigurationInfoFormatProperty { - if (!config) { - return undefined; - } + props: DatabaseProxyProps, +): CfnDBProxyTargetGroup.ConnectionPoolConfigurationInfoFormatProperty { return { - connectionBorrowTimeout: config.borrowTimeout?.toSeconds(), - initQuery: config.initQuery, - maxConnectionsPercent: config.maxConnectionsPercent, - maxIdleConnectionsPercent: config.maxIdleConnectionsPercent, - sessionPinningFilters: config.sessionPinningFilters?.map(_ => _.filterName), + connectionBorrowTimeout: props.borrowTimeout?.toSeconds(), + initQuery: props.initQuery, + maxConnectionsPercent: props.maxConnectionsPercent, + maxIdleConnectionsPercent: props.maxIdleConnectionsPercent, + sessionPinningFilters: props.sessionPinningFilters?.map(_ => _.filterName), }; } diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json b/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json index 45a1e58e9ff1f..d02477bf1a86a 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json @@ -523,6 +523,7 @@ ], "DBProxyName": "dbProxy", "EngineFamily": "POSTGRESQL", + "RequireTLS": true, "RoleArn": { "Fn::GetAtt": [ "dbProxyIAMRole662F3AB8", diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts index a5766380f327a..22f5535237d77 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts @@ -15,10 +15,8 @@ const dbInstance = new rds.DatabaseInstance(stack, 'dbInstance', { }); new rds.DatabaseProxy(stack, 'dbProxy', { - connectionPool: { - borrowTimeout: cdk.Duration.seconds(30), - maxConnectionsPercent: 50, - }, + borrowTimeout: cdk.Duration.seconds(30), + maxConnectionsPercent: 50, secret: dbInstance.secret!, proxyTarget: rds.ProxyTarget.fromInstance(dbInstance), vpc, diff --git a/packages/@aws-cdk/aws-rds/test/test.proxy.ts b/packages/@aws-cdk/aws-rds/test/test.proxy.ts index 3591c6ad80586..f0b6bfbf0c91f 100644 --- a/packages/@aws-cdk/aws-rds/test/test.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/test.proxy.ts @@ -37,6 +37,7 @@ export = { ], DBProxyName: 'Proxy', EngineFamily: 'MYSQL', + RequireTLS: true, RoleArn: { 'Fn::GetAtt': [ 'ProxyIAMRole2FE8AB0F', @@ -60,6 +61,7 @@ export = { DBProxyName: { Ref: 'ProxyCB0DFB71', }, + ConnectionPoolConfigurationInfo: {}, DBInstanceIdentifiers: [ { Ref: 'InstanceC1063A87', @@ -108,6 +110,7 @@ export = { ], DBProxyName: 'Proxy', EngineFamily: 'POSTGRESQL', + RequireTLS: true, RoleArn: { 'Fn::GetAtt': [ 'ProxyIAMRole2FE8AB0F', @@ -131,6 +134,7 @@ export = { DBProxyName: { Ref: 'ProxyCB0DFB71', }, + ConnectionPoolConfigurationInfo: {}, DBClusterIdentifiers: [ { Ref: 'DatabaseB269D8BB', From 4a8eb857c012dae3414a2b76567fc405d4ea635d Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 29 Jun 2020 11:05:34 +0100 Subject: [PATCH 5/7] adjustments to the proxy target API --- packages/@aws-cdk/aws-rds/lib/cluster.ts | 2 +- packages/@aws-cdk/aws-rds/lib/instance.ts | 2 +- packages/@aws-cdk/aws-rds/lib/proxy.ts | 116 ++++++++++-------- packages/@aws-cdk/aws-rds/test/integ.proxy.ts | 2 +- packages/@aws-cdk/aws-rds/test/test.proxy.ts | 4 +- 5 files changed, 67 insertions(+), 59 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 51392769e64dd..bdc5926bd6ba7 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -245,7 +245,7 @@ abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster */ public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { return new DatabaseProxy(this, id, { - proxyTarget: ProxyTarget.fromCluster(this), + proxyTarget: ProxyTarget.atCluster(this), ...options, }); } diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index c2fc8d4b0e8b7..8603092e463b1 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -122,7 +122,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase */ public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { return new DatabaseProxy(this, id, { - proxyTarget: ProxyTarget.fromInstance(this), + proxyTarget: ProxyTarget.atInstance(this), ...options, }); } diff --git a/packages/@aws-cdk/aws-rds/lib/proxy.ts b/packages/@aws-cdk/aws-rds/lib/proxy.ts index f008e71bdd256..6b0bc8bad8d8b 100644 --- a/packages/@aws-cdk/aws-rds/lib/proxy.ts +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -6,24 +6,6 @@ import { IDatabaseCluster } from './cluster-ref'; import { IDatabaseInstance } from './instance'; import { CfnDBCluster, CfnDBInstance, CfnDBProxy, CfnDBProxyTargetGroup } from './rds.generated'; -/** - * The kinds of databases that the proxy can connect to. - * This value determines which database network protocol the proxy recognizes when it interprets network traffic to - * and from the database. - * The engine family applies to MySQL and PostgreSQL for both RDS and Aurora. - */ -export enum ProxyEngineFamily { - /** - * MYSQL - */ - MYSQL = 'MYSQL', - - /** - * POSTGRESQL - */ - POSTGRESQL = 'POSTGRESQL', -} - /** * SessionPinningFilter * @@ -64,10 +46,8 @@ export class ProxyTarget { * * @param instance RDS database instance */ - public static fromInstance(instance: IDatabaseInstance): ProxyTarget { - const _instance = instance.node.defaultChild as CfnDBInstance; - const engine = ProxyTarget.fromEngine(_instance.engine!); - return new ProxyTarget(engine, [instance.instanceIdentifier]); + public static atInstance(instance: IDatabaseInstance): ProxyTarget { + return new ProxyTarget(instance); } /** @@ -75,50 +55,68 @@ export class ProxyTarget { * * @param cluster RDS database cluster */ - public static fromCluster(cluster: IDatabaseCluster): ProxyTarget { - const _cluster = cluster.node.defaultChild as CfnDBCluster; - const engine = ProxyTarget.fromEngine(_cluster.engine); - return new ProxyTarget(engine, cluster.instanceIdentifiers, [cluster.clusterIdentifier]); + public static atCluster(cluster: IDatabaseCluster): ProxyTarget { + return new ProxyTarget(undefined, cluster); } + private constructor(private readonly dbInstance?: IDatabaseInstance, private readonly dbCluster?: IDatabaseCluster) {} + /** - * @deprecated - will be removed after https://github.com/aws/aws-cdk/pull/8686 + * Bind this target to the specified database proxy. */ - private static fromEngine(engine: string): ProxyEngineFamily { + public bind(_: DatabaseProxy): ProxyTargetConfig { + let engine: string | undefined; + if (this.dbCluster && this.dbInstance) { + throw new Error('Proxy cannot target both database cluster and database instance.'); + } else if (this.dbCluster) { + engine = (this.dbCluster.node.defaultChild as CfnDBCluster).engine; + } else if (this.dbInstance) { + engine = (this.dbInstance.node.defaultChild as CfnDBInstance).engine; + } + + let engineFamily; switch (engine) { case 'aurora': case 'aurora-mysql': case 'mysql': - return ProxyEngineFamily.MYSQL; + engineFamily = 'MYSQL'; + break; case 'aurora-postgresql': case 'postgres': - return ProxyEngineFamily.POSTGRESQL; + engineFamily = 'POSTGRESQL'; + break; default: throw new Error(`Unsupported engine type - ${engine}`); } - } - private constructor( - /** - * The kinds of databases that the proxy can connect to. - * This value determines which database network protocol the proxy recognizes when it interprets network traffic to - * and from the database. - * The engine family applies to MySQL and PostgreSQL for both RDS and Aurora. - */ - public readonly engineFamily: ProxyEngineFamily, - - /** - * One or more DB instance identifiers. - */ - public readonly dbInstanceIdentifiers: string[], + return { + engineFamily, + dbClusters: this.dbCluster ? [ this.dbCluster ] : undefined, + dbInstances: this.dbInstance ? [ this.dbInstance ] : undefined, + }; + } +} - /** - * One or more DB cluster identifiers. - * - * @default undefined - */ - public readonly dbClusterIdentifiers?: string[], - ) {} +/** + * The result of binding a `ProxyTarget` to a `DatabaseProxy`. + */ +export interface ProxyTargetConfig { + /** + * The engine family of the database instance or cluster this proxy connects with. + */ + readonly engineFamily: string; + /** + * The database instances to which this proxy connects. + * Either this or `dbClusters` will be set and the other `undefined`. + * @default - `undefined` if `dbClusters` is set. + */ + readonly dbInstances?: IDatabaseInstance[]; + /** + * The database clusters to which this proxy connects. + * Either this or `dbInstances` will be set and the other `undefined`. + * @default - `undefined` if `dbInstances` is set. + */ + readonly dbClusters?: IDatabaseCluster[]; } /** @@ -385,6 +383,8 @@ export class DatabaseProxy extends cdk.Resource this.connections = new ec2.Connections({ securityGroups: props.securityGroups }); + const bindResult = props.proxyTarget.bind(this); + this.resource = new CfnDBProxy(this, 'Resource', { auth: [ { @@ -395,7 +395,7 @@ export class DatabaseProxy extends cdk.Resource ], dbProxyName: this.physicalName, debugLogging: props.debugLogging, - engineFamily: props.proxyTarget.engineFamily, + engineFamily: bindResult.engineFamily, idleClientTimeout: props.idleClientTimeout?.toSeconds(), requireTls: props.requireTLS ?? true, roleArn: role.roleArn, @@ -407,11 +407,19 @@ export class DatabaseProxy extends cdk.Resource this.dbProxyArn = this.resource.attrDbProxyArn; this.endpoint = this.resource.attrEndpoint; - const { dbInstanceIdentifiers, dbClusterIdentifiers } = props.proxyTarget; + let dbInstanceIdentifiers: string[] | undefined; + if (bindResult.dbClusters) { + // support for only instances of a single cluster + dbInstanceIdentifiers = bindResult.dbClusters[0].instanceIdentifiers; + } else if (bindResult.dbInstances) { + // support for only single instance + dbInstanceIdentifiers = [ bindResult.dbInstances[0].instanceIdentifier ]; + } + new CfnDBProxyTargetGroup(this, 'ProxyTargetGroup', { dbProxyName: this.dbProxyName, dbInstanceIdentifiers, - dbClusterIdentifiers, + dbClusterIdentifiers: bindResult.dbClusters?.map((c) => c.clusterIdentifier), connectionPoolConfigurationInfo: toConnectionPoolConfigurationInfo(props), }); } diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts index 22f5535237d77..b30a0c37f9841 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts @@ -18,7 +18,7 @@ new rds.DatabaseProxy(stack, 'dbProxy', { borrowTimeout: cdk.Duration.seconds(30), maxConnectionsPercent: 50, secret: dbInstance.secret!, - proxyTarget: rds.ProxyTarget.fromInstance(dbInstance), + proxyTarget: rds.ProxyTarget.atInstance(dbInstance), vpc, }); diff --git a/packages/@aws-cdk/aws-rds/test/test.proxy.ts b/packages/@aws-cdk/aws-rds/test/test.proxy.ts index f0b6bfbf0c91f..17bb9d5b65204 100644 --- a/packages/@aws-cdk/aws-rds/test/test.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/test.proxy.ts @@ -18,7 +18,7 @@ export = { // WHEN new rds.DatabaseProxy(stack, 'Proxy', { - proxyTarget: rds.ProxyTarget.fromInstance(instance), + proxyTarget: rds.ProxyTarget.atInstance(instance), secret: instance.secret!, vpc, }); @@ -91,7 +91,7 @@ export = { // WHEN new rds.DatabaseProxy(stack, 'Proxy', { - proxyTarget: rds.ProxyTarget.fromCluster(cluster), + proxyTarget: rds.ProxyTarget.atCluster(cluster), secret: cluster.secret!, vpc, }); From 25f977b97d4132b002a3ece45a023277cd9ebdf7 Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 29 Jun 2020 14:24:18 +0100 Subject: [PATCH 6/7] switch atXXX() to forXXX() --- packages/@aws-cdk/aws-rds/lib/cluster.ts | 2 +- packages/@aws-cdk/aws-rds/lib/instance.ts | 2 +- packages/@aws-cdk/aws-rds/lib/proxy.ts | 4 ++-- packages/@aws-cdk/aws-rds/test/integ.proxy.ts | 2 +- packages/@aws-cdk/aws-rds/test/test.proxy.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index bdc5926bd6ba7..24ddb2c019052 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -245,7 +245,7 @@ abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster */ public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { return new DatabaseProxy(this, id, { - proxyTarget: ProxyTarget.atCluster(this), + proxyTarget: ProxyTarget.forCluster(this), ...options, }); } diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 8603092e463b1..eb73ef99525d1 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -122,7 +122,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase */ public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { return new DatabaseProxy(this, id, { - proxyTarget: ProxyTarget.atInstance(this), + proxyTarget: ProxyTarget.forInstance(this), ...options, }); } diff --git a/packages/@aws-cdk/aws-rds/lib/proxy.ts b/packages/@aws-cdk/aws-rds/lib/proxy.ts index 6b0bc8bad8d8b..1d963f7940b41 100644 --- a/packages/@aws-cdk/aws-rds/lib/proxy.ts +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -46,7 +46,7 @@ export class ProxyTarget { * * @param instance RDS database instance */ - public static atInstance(instance: IDatabaseInstance): ProxyTarget { + public static forInstance(instance: IDatabaseInstance): ProxyTarget { return new ProxyTarget(instance); } @@ -55,7 +55,7 @@ export class ProxyTarget { * * @param cluster RDS database cluster */ - public static atCluster(cluster: IDatabaseCluster): ProxyTarget { + public static forCluster(cluster: IDatabaseCluster): ProxyTarget { return new ProxyTarget(undefined, cluster); } diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts index b30a0c37f9841..1b85948c53449 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts @@ -18,7 +18,7 @@ new rds.DatabaseProxy(stack, 'dbProxy', { borrowTimeout: cdk.Duration.seconds(30), maxConnectionsPercent: 50, secret: dbInstance.secret!, - proxyTarget: rds.ProxyTarget.atInstance(dbInstance), + proxyTarget: rds.ProxyTarget.forInstance(dbInstance), vpc, }); diff --git a/packages/@aws-cdk/aws-rds/test/test.proxy.ts b/packages/@aws-cdk/aws-rds/test/test.proxy.ts index 17bb9d5b65204..881f8cde184b5 100644 --- a/packages/@aws-cdk/aws-rds/test/test.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/test.proxy.ts @@ -18,7 +18,7 @@ export = { // WHEN new rds.DatabaseProxy(stack, 'Proxy', { - proxyTarget: rds.ProxyTarget.atInstance(instance), + proxyTarget: rds.ProxyTarget.forInstance(instance), secret: instance.secret!, vpc, }); @@ -91,7 +91,7 @@ export = { // WHEN new rds.DatabaseProxy(stack, 'Proxy', { - proxyTarget: rds.ProxyTarget.atCluster(cluster), + proxyTarget: rds.ProxyTarget.forCluster(cluster), secret: cluster.secret!, vpc, }); From a68cd96442ed6e1f2f91ed582e451acfa53ea876 Mon Sep 17 00:00:00 2001 From: Hyeonsoo Lee Date: Mon, 29 Jun 2020 23:55:39 +0900 Subject: [PATCH 7/7] Just simply use 'fromXXX' --- packages/@aws-cdk/aws-rds/lib/cluster.ts | 2 +- packages/@aws-cdk/aws-rds/lib/instance.ts | 2 +- packages/@aws-cdk/aws-rds/lib/proxy.ts | 4 ++-- packages/@aws-cdk/aws-rds/test/integ.proxy.ts | 2 +- packages/@aws-cdk/aws-rds/test/test.proxy.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 24ddb2c019052..51392769e64dd 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -245,7 +245,7 @@ abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster */ public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { return new DatabaseProxy(this, id, { - proxyTarget: ProxyTarget.forCluster(this), + proxyTarget: ProxyTarget.fromCluster(this), ...options, }); } diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index eb73ef99525d1..c2fc8d4b0e8b7 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -122,7 +122,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase */ public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { return new DatabaseProxy(this, id, { - proxyTarget: ProxyTarget.forInstance(this), + proxyTarget: ProxyTarget.fromInstance(this), ...options, }); } diff --git a/packages/@aws-cdk/aws-rds/lib/proxy.ts b/packages/@aws-cdk/aws-rds/lib/proxy.ts index 1d963f7940b41..4e8112ed9ffbc 100644 --- a/packages/@aws-cdk/aws-rds/lib/proxy.ts +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -46,7 +46,7 @@ export class ProxyTarget { * * @param instance RDS database instance */ - public static forInstance(instance: IDatabaseInstance): ProxyTarget { + public static fromInstance(instance: IDatabaseInstance): ProxyTarget { return new ProxyTarget(instance); } @@ -55,7 +55,7 @@ export class ProxyTarget { * * @param cluster RDS database cluster */ - public static forCluster(cluster: IDatabaseCluster): ProxyTarget { + public static fromCluster(cluster: IDatabaseCluster): ProxyTarget { return new ProxyTarget(undefined, cluster); } diff --git a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts index 1b85948c53449..22f5535237d77 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts @@ -18,7 +18,7 @@ new rds.DatabaseProxy(stack, 'dbProxy', { borrowTimeout: cdk.Duration.seconds(30), maxConnectionsPercent: 50, secret: dbInstance.secret!, - proxyTarget: rds.ProxyTarget.forInstance(dbInstance), + proxyTarget: rds.ProxyTarget.fromInstance(dbInstance), vpc, }); diff --git a/packages/@aws-cdk/aws-rds/test/test.proxy.ts b/packages/@aws-cdk/aws-rds/test/test.proxy.ts index 881f8cde184b5..f0b6bfbf0c91f 100644 --- a/packages/@aws-cdk/aws-rds/test/test.proxy.ts +++ b/packages/@aws-cdk/aws-rds/test/test.proxy.ts @@ -18,7 +18,7 @@ export = { // WHEN new rds.DatabaseProxy(stack, 'Proxy', { - proxyTarget: rds.ProxyTarget.forInstance(instance), + proxyTarget: rds.ProxyTarget.fromInstance(instance), secret: instance.secret!, vpc, }); @@ -91,7 +91,7 @@ export = { // WHEN new rds.DatabaseProxy(stack, 'Proxy', { - proxyTarget: rds.ProxyTarget.forCluster(cluster), + proxyTarget: rds.ProxyTarget.fromCluster(cluster), secret: cluster.secret!, vpc, });