From e0e5e034a198425ec9c55c219398df0e71b10815 Mon Sep 17 00:00:00 2001 From: Hyeonsoo David Lee Date: Wed, 1 Jul 2020 17:47:06 +0900 Subject: [PATCH] feat(rds): database proxy (#8476) closes #8475 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/README.md | 26 + 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/index.ts | 1 + packages/@aws-cdk/aws-rds/lib/instance.ts | 16 + packages/@aws-cdk/aws-rds/lib/proxy.ts | 451 ++++++++++++++ .../aws-rds/test/integ.proxy.expected.json | 561 ++++++++++++++++++ packages/@aws-cdk/aws-rds/test/integ.proxy.ts | 25 + packages/@aws-cdk/aws-rds/test/test.proxy.ts | 156 +++++ .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 5 + 10 files changed, 1258 insertions(+) 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 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 64d7b7b19528b..7b73fdfe92373 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -189,3 +189,29 @@ new DatabaseCluster(this, 'dbcluster', { s3ExportBuckets: [ exportBucket ] }); ``` + +### Creating a Database 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'; +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 = dbInstance.addProxy('proxy', { + connectionBorrowTimeout: cdk.Duration.seconds(30), + maxConnectionsPercent: 50, + secret, + vpc, +}); +``` 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/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/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 new file mode 100644 index 0000000000000..4e8112ed9ffbc --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/proxy.ts @@ -0,0 +1,451 @@ +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 { CfnDBCluster, CfnDBInstance, CfnDBProxy, CfnDBProxyTargetGroup } from './rds.generated'; + +/** + * SessionPinningFilter + * + * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-pinning + */ +export class SessionPinningFilter { + /** + * You can opt out of session pinning for the following kinds of application statements: + * + * - Setting session variables and configuration settings. + */ + 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, + ) {} +} + +/** + * 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 class ProxyTarget { + /** + * From instance + * + * @param instance RDS database instance + */ + public static fromInstance(instance: IDatabaseInstance): ProxyTarget { + return new ProxyTarget(instance); + } + + /** + * From cluster + * + * @param cluster RDS database cluster + */ + public static fromCluster(cluster: IDatabaseCluster): ProxyTarget { + return new ProxyTarget(undefined, cluster); + } + + private constructor(private readonly dbInstance?: IDatabaseInstance, private readonly dbCluster?: IDatabaseCluster) {} + + /** + * Bind this target to the specified database proxy. + */ + 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': + engineFamily = 'MYSQL'; + break; + case 'aurora-postgresql': + case 'postgres': + engineFamily = 'POSTGRESQL'; + break; + default: + throw new Error(`Unsupported engine type - ${engine}`); + } + + return { + engineFamily, + dbClusters: this.dbCluster ? [ this.dbCluster ] : undefined, + dbInstances: this.dbInstance ? [ this.dbInstance ] : undefined, + }; + } +} + +/** + * 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[]; +} + +/** + * 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. + * 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; + + /** + * 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[]; + + /** + * 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; + + /** + * 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 true + */ + 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. + * + * @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[]; + + /** + * The subnets used by the proxy. + * + * @default - the VPC default strategy if not specified. + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * The VPC to associate with the new proxy. + */ + 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 + */ +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; +} + +/** + * RDS Database Proxy + * + * @resource AWS::RDS::DBProxy + */ +export class DatabaseProxy extends cdk.Resource + implements IDatabaseProxy, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { + /** + * Import an existing database proxy. + */ + public static fromDatabaseProxyAttributes( + scope: cdk.Construct, + id: string, + attrs: DatabaseProxyAttributes, + ): IDatabaseProxy { + class Import extends cdk.Resource implements IDatabaseProxy { + public readonly dbProxyName = attrs.dbProxyName; + public readonly dbProxyArn = attrs.dbProxyArn; + public readonly endpoint = attrs.endpoint; + } + return new Import(scope, id); + } + + /** + * 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; + + private readonly resource: CfnDBProxy; + + constructor(scope: cdk.Construct, id: string, props: DatabaseProxyProps) { + super(scope, id, { physicalName: props.dbProxyName || id }); + + const role = props.role || new iam.Role(this, 'IAMRole', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + }); + + props.secret.grantRead(role); + + this.connections = new ec2.Connections({ securityGroups: props.securityGroups }); + + const bindResult = props.proxyTarget.bind(this); + + 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: bindResult.engineFamily, + idleClientTimeout: props.idleClientTimeout?.toSeconds(), + requireTls: props.requireTLS ?? true, + roleArn: role.roleArn, + vpcSecurityGroupIds: props.securityGroups?.map(_ => _.securityGroupId), + vpcSubnetIds: props.vpc.selectSubnets(props.vpcSubnets).subnetIds, + }); + + this.dbProxyName = this.resource.ref; + this.dbProxyArn = this.resource.attrDbProxyArn; + this.endpoint = this.resource.attrEndpoint; + + 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: bindResult.dbClusters?.map((c) => c.clusterIdentifier), + connectionPoolConfigurationInfo: toConnectionPoolConfigurationInfo(props), + }); + } + + /** + * 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( + props: DatabaseProxyProps, +): CfnDBProxyTargetGroup.ConnectionPoolConfigurationInfoFormatProperty { + return { + 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 new file mode 100644 index 0000000000000..d02477bf1a86a --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.expected.json @@ -0,0 +1,561 @@ +{ + "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" + } + } + }, + "dbProxyIAMRoleDefaultPolicy99AB98F3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "dbInstanceSecretAttachment88CFBDAE" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "dbProxyIAMRoleDefaultPolicy99AB98F3", + "Roles": [ + { + "Ref": "dbProxyIAMRole662F3AB8" + } + ] + } + }, + "dbProxy3B89EAF2": { + "Type": "AWS::RDS::DBProxy", + "Properties": { + "Auth": [ + { + "AuthScheme": "SECRETS", + "IAMAuth": "DISABLED", + "SecretArn": { + "Ref": "dbInstanceSecretAttachment88CFBDAE" + } + } + ], + "DBProxyName": "dbProxy", + "EngineFamily": "POSTGRESQL", + "RequireTLS": true, + "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" + } + ] + } + } + } +} 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..22f5535237d77 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.proxy.ts @@ -0,0 +1,25 @@ +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', { + borrowTimeout: cdk.Duration.seconds(30), + maxConnectionsPercent: 50, + secret: dbInstance.secret!, + proxyTarget: rds.ProxyTarget.fromInstance(dbInstance), + vpc, +}); + +app.synth(); 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..f0b6bfbf0c91f --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/test.proxy.ts @@ -0,0 +1,156 @@ +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', { + 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', + RequireTLS: true, + RoleArn: { + 'Fn::GetAtt': [ + 'ProxyIAMRole2FE8AB0F', + 'Arn', + ], + }, + VpcSubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + }, + }, ResourcePart.CompleteDefinition)); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBProxyTargetGroup', { + Properties: { + DBProxyName: { + Ref: 'ProxyCB0DFB71', + }, + ConnectionPoolConfigurationInfo: {}, + 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', { + 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', + RequireTLS: true, + RoleArn: { + 'Fn::GetAtt': [ + 'ProxyIAMRole2FE8AB0F', + 'Arn', + ], + }, + VpcSubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + }, + }, ResourcePart.CompleteDefinition)); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBProxyTargetGroup', { + Properties: { + DBProxyName: { + Ref: 'ProxyCB0DFB71', + }, + ConnectionPoolConfigurationInfo: {}, + DBClusterIdentifiers: [ + { + Ref: 'DatabaseB269D8BB', + }, + ], + DBInstanceIdentifiers: [ + { + Ref: 'DatabaseInstance1844F58FD', + }, + { + Ref: 'DatabaseInstance2AA380DEE', + }, + ], + }, + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index c0abdd48832c8..787d832764d54 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 */