diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 958fe1e3f4988..9cff2ac89fdc0 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -1,27 +1,8 @@ ## AWS RDS Construct Library -The `aws-cdk-rds` package contains Constructs for setting up RDS instances. - -> Note: the functionality this package is currently limited, as the CDK team is -> focusing on other use cases first. If your use case is not listed below, you -> will have to use achieve it using CloudFormation resources. -> -> If you would like to help improve the state of this library, Pull Requests are -> welcome. - -Supported: - -* Clustered databases - -Not supported: - -* Instance databases -* Setting up from a snapshot - - ### Starting a Clustered Database -To set up a clustered database (like Aurora), create an instance of `DatabaseCluster`. You must +To set up a clustered database (like Aurora), define a `DatabaseCluster`. You must always launch a database in a VPC. Use the `vpcSubnets` attribute to control whether your instances will be launched privately or publicly: @@ -45,33 +26,84 @@ By default, the master password will be generated and stored in AWS Secrets Mana Your cluster will be empty by default. To add a default database upon construction, specify the `defaultDatabaseName` attribute. +### Starting an Instance Database +To set up a instance database, define a `DatabaseInstance`. You must +always launch a database in a VPC. Use the `vpcSubnets` attribute to control whether +your instances will be launched privately or publicly: + +```ts +const instance = new DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.OracleSE1, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + masterUsername: 'syscdk', + vpc +}); +``` +By default, the master password will be generated and stored in AWS Secrets Manager. + +Use `DatabaseInstanceFromSnapshot` and `DatabaseInstanceReadReplica` to create an instance from snapshot or +a source database respectively: + +```ts +new DatabaseInstanceFromSnapshot(stack, 'Instance', { + snapshotIdentifier: 'my-snapshot', + engine: rds.DatabaseInstanceEngine.Postgres, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Large), + vpc +}); + +new DatabaseInstanceReadReplica(stack, 'ReadReplica', { + sourceDatabaseInstance: sourceInstance, + engine: rds.DatabaseInstanceEngine.Postgres, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Large), + vpc +}); +``` +Creating a "production" Oracle database instance with option and parameter groups: + +[example of setting up a production oracle instance](test/integ.instance.lit.ts) + + +### Instance events +To define Amazon CloudWatch event rules for database instances, use the `onEvent` +method: + +```ts +const rule = instance.onEvent('InstanceEvent', { target: new targets.LambdaFunction(fn) }); +``` + ### Connecting -To control who can access the cluster, use the `.connections` attribute. RDS database have +To control who can access the cluster or instance, use the `.connections` attribute. RDS databases have a default port, so you don't need to specify the port: ```ts cluster.connections.allowFromAnyIpv4('Open to the world'); ``` -The endpoints to access your database will be available as the `.clusterEndpoint` and `.readerEndpoint` +The endpoints to access your database cluster will be available as the `.clusterEndpoint` and `.readerEndpoint` attributes: ```ts const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` +For an instance database: +```ts +const address = instance.instanceEndpoint.socketAddress; // "HOSTNAME:PORT" +``` + ### Rotating master password When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: -[example of setting up master password rotation](test/integ.cluster-rotation.lit.ts) +[example of setting up master password rotation for a cluster](test/integ.cluster-rotation.lit.ts) Rotation of the master password is also supported for an existing cluster: ```ts -new RotationSingleUser(stack, 'Rotation', { +new SecretRotation(stack, 'Rotation', { secret: importedSecret, - engine: DatabaseEngine.Oracle, - target: importedCluster, + application: SecretRotationApplication.OracleRotationSingleUser + target: importedCluster, // or importedInstance vpc: importedVpc, }) ``` @@ -87,3 +119,13 @@ The `importedSecret` must be a JSON string with the following format: "port": "" } ``` + +### Metrics +Database instances expose metrics (`cloudwatch.Metric`): +```ts +// The number of database connections in use (average over 5 minutes) +const dbConnections = instance.metricDatabaseConnections(); + +// The average amount of time taken per disk I/O operation (average over 1 minute) +const readLatency = instance.metric('ReadLatency', { statistic: 'Average', periodSec: 60 }); +``` diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts b/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts deleted file mode 100644 index 1ca9c92b14d31..0000000000000 --- a/packages/@aws-cdk/aws-rds/lib/cluster-parameter-group.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Construct, IResource, Resource, Token } from '@aws-cdk/cdk'; -import { Parameters } from './props'; -import { CfnDBClusterParameterGroup } from './rds.generated'; - -/** - * A cluster parameter group - */ -export interface IClusterParameterGroup extends IResource { - /** - * Name of this parameter group - */ - readonly parameterGroupName: string; -} - -/** - * Properties to reference a cluster parameter group - */ -export interface ClusterParameterGroupImportProps { - readonly parameterGroupName: string; -} - -/** - * Properties for a cluster parameter group - */ -export interface ClusterParameterGroupProps { - /** - * Database family of this parameter group - */ - readonly family: string; - - /** - * Description for this parameter group - */ - readonly description: string; - - /** - * The parameters in this parameter group - * - * @default - No parameters. - */ - readonly parameters?: Parameters; -} - -/** - * Defina a cluster parameter group - * - * @resource AWS::RDS::DBClusterParameterGroup - */ -export class ClusterParameterGroup extends Resource implements IClusterParameterGroup { - /** - * Import a parameter group - */ - public static fromParameterGroupName(scope: Construct, id: string, parameterGroupName: string): IClusterParameterGroup { - class Import extends Resource implements IClusterParameterGroup { - public parameterGroupName = parameterGroupName; - } - return new Import(scope, id); - } - - public readonly parameterGroupName: string; - private readonly parameters: Parameters = {}; - - constructor(scope: Construct, id: string, props: ClusterParameterGroupProps) { - super(scope, id); - - const resource = new CfnDBClusterParameterGroup(this, 'Resource', { - description: props.description, - family: props.family, - parameters: new Token(() => this.parameters), - }); - - for (const [key, value] of Object.entries(props.parameters || {})) { - this.setParameter(key, value); - } - - this.parameterGroupName = resource.ref; - } - - /** - * Set a single parameter in this parameter group - */ - public setParameter(key: string, value: string | undefined) { - if (value === undefined && key in this.parameters) { - delete this.parameters[key]; - } - if (value !== undefined) { - this.parameters[key] = value; - } - } - - /** - * Remove a previously-set parameter from this parameter group - */ - public removeParameter(key: string) { - this.setParameter(key, undefined); - } - - /** - * Validate this construct - */ - protected validate(): string[] { - if (Object.keys(this.parameters).length === 0) { - return ['At least one parameter required, call setParameter().']; - } - return []; - } -} diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts b/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts index 6688e4cf00b7f..9250b27ac9766 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts @@ -1,12 +1,12 @@ import ec2 = require('@aws-cdk/aws-ec2'); import secretsmanager = require('@aws-cdk/aws-secretsmanager'); -import cdk = require('@aws-cdk/cdk'); -import { Token } from '@aws-cdk/cdk'; +import { IResource } from '@aws-cdk/cdk'; +import { Endpoint } from './endpoint'; /** * Create a clustered database with a given number of instances. */ -export interface IDatabaseCluster extends cdk.IResource, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { +export interface IDatabaseCluster extends IResource, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { /** * Identifier of the cluster */ @@ -80,33 +80,3 @@ export interface DatabaseClusterAttributes { */ readonly instanceEndpointAddresses: string[]; } - -/** - * Connection endpoint of a database cluster or instance - * - * Consists of a combination of hostname and port. - */ -export class Endpoint { - /** - * The hostname of the endpoint - */ - public readonly hostname: string; - - /** - * The port of the endpoint - */ - public readonly port: number; - - /** - * The combination of "HOSTNAME:PORT" for this endpoint - */ - public readonly socketAddress: string; - - constructor(address: string, port: number) { - this.hostname = address; - this.port = port; - - const portDesc = Token.isToken(port) ? '{IndirectPort}' : port; - this.socketAddress = `${address}:${portDesc}`; - } -} diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 13c502b1df04d..95f634e6a0fe0 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -2,12 +2,13 @@ import ec2 = require('@aws-cdk/aws-ec2'); import kms = require('@aws-cdk/aws-kms'); import secretsmanager = require('@aws-cdk/aws-secretsmanager'); import { Construct, DeletionPolicy, Resource, Token } from '@aws-cdk/cdk'; -import { IClusterParameterGroup } from './cluster-parameter-group'; -import { DatabaseClusterAttributes, Endpoint, IDatabaseCluster } from './cluster-ref'; +import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; import { DatabaseSecret } from './database-secret'; +import { Endpoint } from './endpoint'; +import { IParameterGroup } from './parameter-group'; import { BackupProps, DatabaseClusterEngine, InstanceProps, Login } from './props'; import { CfnDBCluster, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated'; -import { DatabaseEngine, RotationSingleUser, RotationSingleUserOptions } from './rotation-single-user'; +import { SecretRotation, SecretRotationApplication, SecretRotationOptions } from './secret-rotation'; /** * Properties for a new database cluster @@ -111,7 +112,7 @@ export interface DatabaseClusterProps { * * @default - No parameter group. */ - readonly parameterGroup?: IClusterParameterGroup; + readonly parameterGroup?: IParameterGroup; /** * The CloudFormation policy to apply when the cluster and its instances @@ -241,7 +242,7 @@ export class DatabaseCluster extends DatabaseClusterBase { /** * The database engine of this cluster */ - public readonly engine: DatabaseClusterEngine; + private readonly secretRotationApplication: SecretRotationApplication; /** * The VPC where the DB subnet group is created. @@ -286,11 +287,11 @@ export class DatabaseCluster extends DatabaseClusterBase { }); } - this.engine = props.engine; + this.secretRotationApplication = props.engine.secretRotationApplication; const cluster = new CfnDBCluster(this, 'Resource', { // Basic - engine: this.engine, + engine: props.engine.name, dbClusterIdentifier: props.clusterIdentifier, dbSubnetGroupName: subnetGroup.ref, vpcSecurityGroupIds: [this.securityGroupId], @@ -347,7 +348,7 @@ export class DatabaseCluster extends DatabaseClusterBase { const instance = new CfnDBInstance(this, `Instance${instanceIndex}`, { // Link to cluster - engine: props.engine, + engine: props.engine.name, dbClusterIdentifier: cluster.ref, dbInstanceIdentifier: instanceIdentifier, // Instance properties @@ -355,6 +356,7 @@ export class DatabaseCluster extends DatabaseClusterBase { publiclyAccessible, // This is already set on the Cluster. Unclear to me whether it should be repeated or not. Better yes. dbSubnetGroupName: subnetGroup.ref, + dbParameterGroupName: props.instanceProps.parameterGroup && props.instanceProps.parameterGroup.parameterGroupName, }); instance.options.deletionPolicy = deleteReplacePolicy; @@ -375,13 +377,13 @@ export class DatabaseCluster extends DatabaseClusterBase { /** * Adds the single user rotation of the master password to this cluster. */ - public addRotationSingleUser(id: string, options: RotationSingleUserOptions = {}): RotationSingleUser { + public addRotationSingleUser(id: string, options: SecretRotationOptions = {}): SecretRotation { if (!this.secret) { throw new Error('Cannot add single user rotation for a cluster without secret.'); } - return new RotationSingleUser(this, id, { + return new SecretRotation(this, id, { secret: this.secret, - engine: toDatabaseEngine(this.engine), + application: this.secretRotationApplication, vpc: this.vpc, vpcSubnets: this.vpcSubnets, target: this, @@ -396,20 +398,3 @@ export class DatabaseCluster extends DatabaseClusterBase { function databaseInstanceType(instanceType: ec2.InstanceType) { return 'db.' + instanceType.toString(); } - -/** - * Transforms a DatbaseClusterEngine to a DatabaseEngine. - * - * @param engine the engine to transform - */ -function toDatabaseEngine(engine: DatabaseClusterEngine): DatabaseEngine { - switch (engine) { - case DatabaseClusterEngine.Aurora: - case DatabaseClusterEngine.AuroraMysql: - return DatabaseEngine.Mysql; - case DatabaseClusterEngine.AuroraPostgresql: - return DatabaseEngine.Postgres; - default: - throw new Error('Unknown engine'); - } -} diff --git a/packages/@aws-cdk/aws-rds/lib/database-secret.ts b/packages/@aws-cdk/aws-rds/lib/database-secret.ts index 46bb307a35978..977346328114a 100644 --- a/packages/@aws-cdk/aws-rds/lib/database-secret.ts +++ b/packages/@aws-cdk/aws-rds/lib/database-secret.ts @@ -1,6 +1,6 @@ import kms = require('@aws-cdk/aws-kms'); import secretsmanager = require('@aws-cdk/aws-secretsmanager'); -import cdk = require('@aws-cdk/cdk'); +import { Construct } from '@aws-cdk/cdk'; /** * Construction properties for a DatabaseSecret. @@ -25,7 +25,7 @@ export interface DatabaseSecretProps { * @resource AWS::SecretsManager::Secret */ export class DatabaseSecret extends secretsmanager.Secret { - constructor(scope: cdk.Construct, id: string, props: DatabaseSecretProps) { + constructor(scope: Construct, id: string, props: DatabaseSecretProps) { super(scope, id, { encryptionKey: props.encryptionKey, generateSecretString: { diff --git a/packages/@aws-cdk/aws-rds/lib/endpoint.ts b/packages/@aws-cdk/aws-rds/lib/endpoint.ts new file mode 100644 index 0000000000000..e0855a30efa02 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/endpoint.ts @@ -0,0 +1,31 @@ +import { Token } from '@aws-cdk/cdk'; + +/** + * Connection endpoint of a database cluster or instance + * + * Consists of a combination of hostname and port. + */ +export class Endpoint { + /** + * The hostname of the endpoint + */ + public readonly hostname: string; + + /** + * The port of the endpoint + */ + public readonly port: number; + + /** + * The combination of "HOSTNAME:PORT" for this endpoint + */ + public readonly socketAddress: string; + + constructor(address: string, port: number) { + this.hostname = address; + this.port = port; + + const portDesc = Token.isToken(port) ? '{IndirectPort}' : port; + this.socketAddress = `${address}:${portDesc}`; + } +} diff --git a/packages/@aws-cdk/aws-rds/lib/index.ts b/packages/@aws-cdk/aws-rds/lib/index.ts index 149610e4fe159..116e542651658 100644 --- a/packages/@aws-cdk/aws-rds/lib/index.ts +++ b/packages/@aws-cdk/aws-rds/lib/index.ts @@ -1,9 +1,14 @@ export * from './cluster'; export * from './cluster-ref'; export * from './props'; -export * from './cluster-parameter-group'; -export * from './rotation-single-user'; +export * from './parameter-group'; +export * from './secret-rotation'; export * from './database-secret'; +export * from './endpoint'; +export * from './option-group'; +export * from './instance'; // AWS::RDS CloudFormation Resources: export * from './rds.generated'; + +import './rds-augmentations.generated'; diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts new file mode 100644 index 0000000000000..acfb4e095801e --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -0,0 +1,943 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import events = require('@aws-cdk/aws-events'); +import iam = require('@aws-cdk/aws-iam'); +import kms = require('@aws-cdk/aws-kms'); +import lambda = require('@aws-cdk/aws-lambda'); +import logs = require('@aws-cdk/aws-logs'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); +import { Construct, DeletionPolicy, IResource, Resource, SecretValue, Token } from '@aws-cdk/cdk'; +import { DatabaseSecret } from './database-secret'; +import { Endpoint } from './endpoint'; +import { IOptionGroup} from './option-group'; +import { IParameterGroup } from './parameter-group'; +import { DatabaseClusterEngine } from './props'; +import { CfnDBInstance, CfnDBInstanceProps, CfnDBSubnetGroup } from './rds.generated'; +import { SecretRotation, SecretRotationApplication, SecretRotationOptions } from './secret-rotation'; + +export interface IDatabaseInstance extends IResource, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { + /** + * The instance identifier. + */ + readonly instanceIdentifier: string; + + /** + * The instance arn. + */ + readonly instanceArn: string; + + /** + * The instance endpoint address. + * + * @attribute + */ + readonly dbInstanceEndpointAddress: string; + + /** + * The instance endpoint port. + * + * @attribute + */ + readonly dbInstanceEndpointPort: string; + + /** + * The instance endpoint. + */ + readonly instanceEndpoint: Endpoint; + + /** + * The security group identifier of the instance. + */ + readonly securityGroupId: string; + + /** + * Defines a CloudWatch event rule which triggers for instance events. Use + * `rule.addEventPattern(pattern)` to specify a filter. + */ + onEvent(id: string, options: events.OnEventOptions): events.Rule; +} + +/** + * Properties that describe an existing instance + */ +export interface DatabaseInstanceAttributes { + /** + * The instance identifier. + */ + readonly instanceIdentifier: string; + + /** + * The endpoint address. + */ + readonly instanceEndpointAddress: string; + + /** + * The database port. + */ + readonly port: number; + + /** + * The security group identifier of the instance. + */ + readonly securityGroupId: string; +} + +/** + * A new or imported database instance. + */ +export abstract class DatabaseInstanceBase extends Resource implements IDatabaseInstance { + /** + * Import an existing database instance. + */ + public static fromDatabaseInstanceAttributes(scope: Construct, id: string, attrs: DatabaseInstanceAttributes): IDatabaseInstance { + class Import extends DatabaseInstanceBase implements IDatabaseInstance { + public readonly defaultPortRange = new ec2.TcpPort(attrs.port); + public readonly connections = new ec2.Connections({ + securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroup', attrs.securityGroupId)], + defaultPortRange: this.defaultPortRange + }); + public readonly instanceIdentifier = attrs.instanceIdentifier; + public readonly dbInstanceEndpointAddress = attrs.instanceEndpointAddress; + public readonly dbInstanceEndpointPort = attrs.port.toString(); + public readonly instanceEndpoint = new Endpoint(attrs.instanceEndpointAddress, attrs.port); + public readonly securityGroupId = attrs.securityGroupId; + } + + return new Import(scope, id); + } + + public abstract readonly instanceIdentifier: string; + public abstract readonly dbInstanceEndpointAddress: string; + public abstract readonly dbInstanceEndpointPort: string; + public abstract readonly instanceEndpoint: Endpoint; + public abstract readonly connections: ec2.Connections; + public abstract readonly securityGroupId: string; + + /** + * Defines a CloudWatch event rule which triggers for instance events. Use + * `rule.addEventPattern(pattern)` to specify a filter. + */ + public onEvent(id: string, options: events.OnEventOptions) { + const rule = new events.Rule(this, id, options); + rule.addEventPattern({ + source: ['aws.rds'], + resources: [this.instanceArn] + }); + rule.addTarget(options.target); + return rule; + } + + /** + * The instance arn. + */ + public get instanceArn(): string { + return this.node.stack.formatArn({ + service: 'rds', + resource: 'db', + sep: ':', + resourceName: this.instanceIdentifier + }); + } + + /** + * Renders the secret attachment target specifications. + */ + public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps { + return { + targetId: this.instanceIdentifier, + targetType: secretsmanager.AttachmentTargetType.Instance + }; + } +} + +/** + * A database instance engine. Provides mapping to DatabaseEngine used for + * secret rotation. + */ +export class DatabaseInstanceEngine extends DatabaseClusterEngine { + public static readonly MariaDb = new DatabaseInstanceEngine('mariadb', SecretRotationApplication.MariaDbRotationSingleUser); + public static readonly Mysql = new DatabaseInstanceEngine('mysql', SecretRotationApplication.MysqlRotationSingleUser); + public static readonly OracleEE = new DatabaseInstanceEngine('oracle-ee', SecretRotationApplication.OracleRotationSingleUser); + public static readonly OracleSE2 = new DatabaseInstanceEngine('oracle-se2', SecretRotationApplication.OracleRotationSingleUser); + public static readonly OracleSE1 = new DatabaseInstanceEngine('oracle-se1', SecretRotationApplication.OracleRotationSingleUser); + public static readonly OracleSE = new DatabaseInstanceEngine('oracle-se', SecretRotationApplication.OracleRotationSingleUser); + public static readonly Postgres = new DatabaseInstanceEngine('postgres', SecretRotationApplication.PostgresRotationSingleUser); + public static readonly SqlServerEE = new DatabaseInstanceEngine('sqlserver-ee', SecretRotationApplication.SqlServerRotationSingleUser); + public static readonly SqlServerSE = new DatabaseInstanceEngine('sqlserver-se', SecretRotationApplication.SqlServerRotationSingleUser); + public static readonly SqlServerEX = new DatabaseInstanceEngine('sqlserver-ex', SecretRotationApplication.SqlServerRotationSingleUser); + public static readonly SqlServerWeb = new DatabaseInstanceEngine('sqlserver-web', SecretRotationApplication.SqlServerRotationSingleUser); +} + +/** + * The license model. + */ +export enum LicenseModel { + /** + * License included. + */ + LicenseIncluded = 'license-included', + + /** + * Bring your own licencse. + */ + BringYourOwnLicense = 'bring-your-own-license', + + /** + * General public license. + */ + GeneralPublicLicense = 'general-public-license' +} + +/** + * The processor features. + */ +export interface ProcessorFeatures { + /** + * The number of CPU core. + */ + readonly coreCount?: number; + + /** + * The number of threads per core. + */ + readonly threadsPerCore?: number; +} + +/** + * The type of storage. + */ +export enum StorageType { + /** + * Standard. + */ + Standard = 'standard', + + /** + * General purpose (SSD). + */ + GP2 = 'gp2', + + /** + * Provisioned IOPS (SSD). + */ + IO1 = 'io1' +} + +/** + * The retention period for Performance Insight. + */ +export enum PerformanceInsightRetentionPeriod { + /** + * Default retention period of 7 days. + */ + Default = 7, + + /** + * Long term retention period of 2 years. + */ + LongTerm = 731 +} + +/** + * Construction properties for a DatabaseInstanceNew + */ +export interface DatabaseInstanceNewProps { + /** + * The name of the compute and memory capacity classes. + */ + readonly instanceClass: ec2.InstanceType; + + /** + * Specifies if the database instance is a multiple Availability Zone deployment. + * + * @default false + */ + readonly multiAz?: boolean; + + /** + * The name of the Availability Zone where the DB instance will be located. + * + * @default no preference + */ + readonly availabilityZone?: string; + + /** + * The storage type. + * + * @default GP2 + */ + readonly storageType?: StorageType; + + /** + * The number of I/O operations per second (IOPS) that the database provisions. + * The value must be equal to or greater than 1000. + * + * @default no provisioned iops + */ + readonly iops?: number; + + /** + * The number of CPU cores and the number of threads per core. + * + * @default the default number of CPU cores and threads per core for the + * chosen instance class. + * + * See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.html#USER_ConfigureProcessor + */ + readonly processorFeatures?: ProcessorFeatures; + + /** + * A name for the DB instance. If you specify a name, AWS CloudFormation + * converts it to lowercase. + * + * @default a CloudFormation generated name + */ + readonly instanceIdentifier?: string; + + /** + * The VPC network where the DB subnet group should be created. + */ + readonly vpc: ec2.IVpc; + + /** + * The type of subnets to add to the created DB subnet group. + * + * @default private + */ + readonly vpcPlacement?: ec2.SubnetSelection; + + /** + * The port for the instance. + * + * @default the default port for the chosen engine. + */ + readonly port?: number; + + /** + * The option group to associate with the instance. + * + * @default no option group + */ + readonly optionGroup?: IOptionGroup; + + /** + * Whether to enable mapping of AWS Identity and Access Management (IAM) accounts + * to database accounts. + * + * @default false + */ + readonly iamAuthentication?: boolean; + + /** + * The number of days during which automatic DB snapshots are retained. Set + * to zero to disable backups. + * + * @default 1 day + */ + readonly backupRetentionPeriod?: number; + + /** + * The daily time range during which automated backups are performed. + * + * Constraints: + * - Must be in the format `hh24:mi-hh24:mi`. + * - Must be in Universal Coordinated Time (UTC). + * - Must not conflict with the preferred maintenance window. + * - Must be at least 30 minutes. + * + * @default a 30-minute window selected at random from an 8-hour block of + * time for each AWS Region. To see the time blocks available, see + * https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.Maintenance.html#AdjustingTheMaintenanceWindow + */ + readonly preferredBackupWindow?: string; + + /** + * Indicates whether to copy all of the user-defined tags from the + * DB instance to snapshots of the DB instance. + * + * @default true + */ + readonly copyTagsToSnapshot?: boolean; + + /** + * Indicates whether automated backups should be deleted or retained when + * you delete a DB instance. + * + * @default false + */ + readonly deleteAutomatedBackups?: boolean; + + /** + * The interval, in seconds, between points when Amazon RDS collects enhanced + * monitoring metrics for the DB instance. + * + * @default no enhanced monitoring + */ + readonly monitoringInterval?: number; + + /** + * Whether to enable Performance Insights for the DB instance. + * + * @default false + */ + readonly enablePerformanceInsights?: boolean; + + /** + * The amount of time, in days, to retain Performance Insights data. + * + * @default 7 days + */ + readonly performanceInsightRetentionPeriod?: PerformanceInsightRetentionPeriod; + + /** + * The AWS KMS key for encryption of Performance Insights data. + * + * @default default master key + */ + readonly performanceInsightKmsKey?: kms.IKey; + + /** + * The list of log types that need to be enabled for exporting to + * CloudWatch Logs. + * + * @default no log exports + */ + readonly cloudwatchLogsExports?: string[]; + + /** + * The number of days log events are kept in CloudWatch Logs. When updating + * this property, unsetting it doesn't remove the log retention policy. To + * remove the retention policy, set the value to `Infinity`. + * + * @default logs never expire + */ + readonly cloudwatchLogsRetention?: logs.RetentionDays; + + /** + * Indicates that minor engine upgrades are applied automatically to the + * DB instance during the maintenance window. + * + * @default true + */ + readonly autoMinorVersionUpgrade?: boolean; + + // tslint:disable:max-line-length + /** + * The weekly time range (in UTC) during which system maintenance can occur. + * + * Format: `ddd:hh24:mi-ddd:hh24:mi` + * Constraint: Minimum 30-minute window + * + * @default a 30-minute window selected at random from an 8-hour block of + * time for each AWS Region, occurring on a random day of the week. To see + * the time blocks available, see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.Maintenance.html#AdjustingTheMaintenanceWindow + */ + // tslint:enable:max-line-length + readonly preferredMaintenanceWindow?: string; + + /** + * Indicates whether the DB instance should have deletion protection enabled. + * + * @default true + */ + readonly deletionProtection?: boolean; + + /** + * The CloudFormation policy to apply when the instance is removed from the + * stack or replaced during an update. + * + * @default Retain + */ + readonly deleteReplacePolicy?: DeletionPolicy +} + +/** + * A new database instance. + */ +abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IDatabaseInstance { + public readonly securityGroupId: string; + public readonly vpc: ec2.IVpc; + + protected readonly vpcPlacement?: ec2.SubnetSelection; + protected readonly newCfnProps: CfnDBInstanceProps; + protected readonly securityGroup: ec2.SecurityGroup; + + private readonly cloudwatchLogsExports?: string[]; + private readonly cloudwatchLogsRetention?: logs.RetentionDays; + + constructor(scope: Construct, id: string, props: DatabaseInstanceNewProps) { + super(scope, id); + + this.vpc = props.vpc; + this.vpcPlacement = props.vpcPlacement; + + const { subnetIds } = props.vpc.selectSubnets(props.vpcPlacement); + + const subnetGroup = new CfnDBSubnetGroup(this, 'SubnetGroup', { + dbSubnetGroupDescription: `Subnet group for ${this.node.id} database`, + subnetIds + }); + + this.securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { + description: `Security group for ${this.node.id} database`, + vpc: props.vpc + }); + this.securityGroupId = this.securityGroup.securityGroupId; + + let monitoringRole; + if (props.monitoringInterval) { + monitoringRole = new iam.Role(this, 'MonitoringRole', { + assumedBy: new iam.ServicePrincipal('monitoring.rds.amazonaws.com'), + managedPolicyArns: [this.node.stack.formatArn({ + service: 'iam', + region: '', + account: 'aws', + resource: 'policy', + resourceName: 'service-role/AmazonRDSEnhancedMonitoringRole' + })] + }); + } + + const deletionProtection = props.deletionProtection !== undefined ? props.deletionProtection : true; + const storageType = props.storageType || StorageType.GP2; + const iops = storageType === StorageType.IO1 ? (props.iops || 1000) : undefined; + + this.cloudwatchLogsExports = props.cloudwatchLogsExports; + this.cloudwatchLogsRetention = props.cloudwatchLogsRetention; + + this.newCfnProps = { + autoMinorVersionUpgrade: props.autoMinorVersionUpgrade, + availabilityZone: props.multiAz ? undefined : props.availabilityZone, + backupRetentionPeriod: props.backupRetentionPeriod ? props.backupRetentionPeriod.toString() : undefined, + copyTagsToSnapshot: props.copyTagsToSnapshot !== undefined ? props.copyTagsToSnapshot : true, + dbInstanceClass: `db.${props.instanceClass}`, + dbInstanceIdentifier: props.instanceIdentifier, + dbSubnetGroupName: subnetGroup.dbSubnetGroupName, + deleteAutomatedBackups: props.deleteAutomatedBackups, + deletionProtection, + enableCloudwatchLogsExports: this.cloudwatchLogsExports, + enableIamDatabaseAuthentication: props.iamAuthentication, + enablePerformanceInsights: props.enablePerformanceInsights, + iops, + monitoringInterval: props.monitoringInterval, + monitoringRoleArn: monitoringRole && monitoringRole.roleArn, + multiAz: props.multiAz, + optionGroupName: props.optionGroup && props.optionGroup.optionGroupName, + performanceInsightsKmsKeyId: props.enablePerformanceInsights + ? props.performanceInsightKmsKey && props.performanceInsightKmsKey.keyArn + : undefined, + performanceInsightsRetentionPeriod: props.enablePerformanceInsights + ? (props.performanceInsightRetentionPeriod || PerformanceInsightRetentionPeriod.Default) + : undefined, + port: props.port ? props.port.toString() : undefined, + preferredBackupWindow: props.preferredBackupWindow, + preferredMaintenanceWindow: props.preferredMaintenanceWindow, + processorFeatures: props.processorFeatures && renderProcessorFeatures(props.processorFeatures), + publiclyAccessible: props.vpcPlacement && props.vpcPlacement.subnetType === ec2.SubnetType.Public, + storageType, + vpcSecurityGroups: [this.securityGroupId] + }; + } + + protected setLogRetention() { + if (this.cloudwatchLogsExports && this.cloudwatchLogsRetention) { + for (const log of this.cloudwatchLogsExports) { + new lambda.LogRetention(this, `LogRetention${log}`, { + logGroupName: `/aws/rds/instance/${this.instanceIdentifier}/${log}`, + retentionDays: this.cloudwatchLogsRetention + }); + } + } + } +} + +/** + * Construction properties for a DatabaseInstanceSource + */ +export interface DatabaseInstanceSourceProps extends DatabaseInstanceNewProps { + /** + * The database engine. + */ + readonly engine: DatabaseInstanceEngine; + + /** + * The license model. + * + * @default RDS default license model + */ + readonly licenseModel?: LicenseModel; + + /** + * The engine version. To prevent automatic upgrades, be sure to specify the + * full version number. + * + * @default RDS default engine version + */ + readonly engineVersion?: string; + + /** + * Whether to allow major version upgrades. + * + * @default false + */ + readonly allowMajorVersionUpgrade?: boolean; + + /** + * The time zone of the instance. + * + * @default RDS default timezone + */ + readonly timezone?: string; + + /** + * The allocated storage size, specified in gigabytes (GB). + * + * @default 100 + */ + readonly allocatedStorage?: number; + + /** + * The master user password. + * + * @default a Secrets Manager generated password + */ + readonly masterUserPassword?: SecretValue; + + /** + * The KMS key to use to encrypt the secret for the master user password. + * + * @default default master key + */ + readonly secretKmsKey?: kms.IKey; + + /** + * The name of the database. + * + * @default no name + */ + readonly databaseName?: string; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; +} + +/** + * A new source database instance (not a read replica) + */ +abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDatabaseInstance { + public abstract readonly secret?: secretsmanager.ISecret; + + protected readonly sourceCfnProps: CfnDBInstanceProps; + + private readonly secretRotationApplication: SecretRotationApplication; + + constructor(scope: Construct, id: string, props: DatabaseInstanceSourceProps) { + super(scope, id, props); + + this.secretRotationApplication = props.engine.secretRotationApplication; + + this.sourceCfnProps = { + ...this.newCfnProps, + allocatedStorage: props.allocatedStorage ? props.allocatedStorage.toString() : '100', + allowMajorVersionUpgrade: props.allowMajorVersionUpgrade, + dbName: props.databaseName, + dbParameterGroupName: props.parameterGroup && props.parameterGroup.parameterGroupName, + engine: props.engine.name, + engineVersion: props.engineVersion, + licenseModel: props.licenseModel, + timezone: props.timezone + }; + } + + /** + * Adds the single user rotation of the master password to this instance. + */ + public addRotationSingleUser(id: string, options: SecretRotationOptions = {}): SecretRotation { + if (!this.secret) { + throw new Error('Cannot add single user rotation for an instance without secret.'); + } + return new SecretRotation(this, id, { + secret: this.secret, + application: this.secretRotationApplication, + vpc: this.vpc, + vpcSubnets: this.vpcPlacement, + target: this, + ...options + }); + } +} + +/** + * Construction properties for a DatabaseInstance. + */ +export interface DatabaseInstanceProps extends DatabaseInstanceSourceProps { + /** + * The master user name. + */ + readonly masterUsername: string; + + /** + * For supported engines, specifies the character set to associate with the + * DB instance. + * + * @default RDS default character set name + */ + readonly characterSetName?: string; + + /** + * Indicates whether the DB instance is encrypted. + * + * @default false + */ + readonly storageEncrypted?: boolean; + + /** + * The master key that's used to encrypt the DB instance. + * + * @default default master key + */ + readonly kmsKey?: kms.IKey; +} + +/** + * A database instance + * + * @resource AWS::RDS::DBInstance + */ +export class DatabaseInstance extends DatabaseInstanceSource implements IDatabaseInstance { + public readonly instanceIdentifier: string; + public readonly dbInstanceEndpointAddress: string; + public readonly dbInstanceEndpointPort: string; + public readonly instanceEndpoint: Endpoint; + public readonly connections: ec2.Connections; + public readonly secret?: secretsmanager.ISecret; + + constructor(scope: Construct, id: string, props: DatabaseInstanceProps) { + super(scope, id, props); + + let secret; + if (!props.masterUserPassword) { + secret = new DatabaseSecret(this, 'Secret', { + username: props.masterUsername, + encryptionKey: props.secretKmsKey, + }); + } + + const instance = new CfnDBInstance(this, 'Resource', { + ...this.sourceCfnProps, + characterSetName: props.characterSetName, + kmsKeyId: props.kmsKey && props.kmsKey.keyArn, + masterUsername: secret ? secret.secretJsonValue('username').toString() : props.masterUsername, + masterUserPassword: secret + ? secret.secretJsonValue('password').toString() + : (props.masterUserPassword + ? props.masterUserPassword.toString() + : undefined), + storageEncrypted: props.kmsKey ? true : props.storageEncrypted + }); + + this.instanceIdentifier = instance.dbInstanceId; + this.dbInstanceEndpointAddress = instance.dbInstanceEndpointAddress; + this.dbInstanceEndpointPort = instance.dbInstanceEndpointPort; + + // create a number token that represents the port of the instance + const portAttribute = new Token(() => instance.dbInstanceEndpointPort).toNumber(); + this.instanceEndpoint = new Endpoint(instance.dbInstanceEndpointAddress, portAttribute); + + const deleteReplacePolicy = props.deleteReplacePolicy || DeletionPolicy.Retain; + instance.options.deletionPolicy = deleteReplacePolicy; + instance.options.updateReplacePolicy = deleteReplacePolicy; + + if (secret) { + this.secret = secret.addTargetAttachment('AttachedSecret', { + target: this + }); + } + + this.connections = new ec2.Connections({ + securityGroups: [this.securityGroup], + defaultPortRange: new ec2.TcpPort(this.instanceEndpoint.port) + }); + + this.setLogRetention(); + } +} + +/** + * Construction properties for a DatabaseInstanceFromSnapshot. + */ +export interface DatabaseInstanceFromSnapshotProps extends DatabaseInstanceSourceProps { + /** + * The name or Amazon Resource Name (ARN) of the DB snapshot that's used to + * restore the DB instance. If you're restoring from a shared manual DB + * snapshot, you must specify the ARN of the snapshot. + */ + readonly snapshotIdentifier: string; + + /** + * The master user name. + * + * @default inherited from the snapshot + */ + readonly masterUsername?: string; + + /** + * Whether to generate a new master user password and store it in + * Secrets Manager. `masterUsername` must be specified when this property + * is set to true. + * + * @default false + */ + readonly generateMasterUserPassword?: boolean; +} + +/** + * A database instance restored from a snapshot. + * + * @resource AWS::RDS::DBInstance + */ +export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource implements IDatabaseInstance { + public readonly instanceIdentifier: string; + public readonly dbInstanceEndpointAddress: string; + public readonly dbInstanceEndpointPort: string; + public readonly instanceEndpoint: Endpoint; + public readonly connections: ec2.Connections; + public readonly secret?: secretsmanager.ISecret; + + constructor(scope: Construct, id: string, props: DatabaseInstanceFromSnapshotProps) { + super(scope, id, props); + + if (props.generateMasterUserPassword && !props.masterUsername) { + throw new Error('`masterUsername` must be specified when `generateMasterUserPassword` is set to true.'); + } + + let secret; + if (!props.masterUserPassword && props.generateMasterUserPassword && props.masterUsername) { + secret = new DatabaseSecret(this, 'Secret', { + username: props.masterUsername, + encryptionKey: props.secretKmsKey, + }); + } + + const instance = new CfnDBInstance(this, 'Resource', { + ...this.sourceCfnProps, + dbSnapshotIdentifier: props.snapshotIdentifier, + masterUsername: secret ? secret.secretJsonValue('username').toString() : props.masterUsername, + masterUserPassword: secret + ? secret.secretJsonValue('password').toString() + : (props.masterUserPassword + ? props.masterUserPassword.toString() + : undefined), + }); + + this.instanceIdentifier = instance.dbInstanceId; + this.dbInstanceEndpointAddress = instance.dbInstanceEndpointAddress; + this.dbInstanceEndpointPort = instance.dbInstanceEndpointPort; + + // create a number token that represents the port of the instance + const portAttribute = new Token(() => instance.dbInstanceEndpointPort).toNumber(); + this.instanceEndpoint = new Endpoint(instance.dbInstanceEndpointAddress, portAttribute); + + const deleteReplacePolicy = props.deleteReplacePolicy || DeletionPolicy.Retain; + instance.options.deletionPolicy = deleteReplacePolicy; + instance.options.updateReplacePolicy = deleteReplacePolicy; + + if (secret) { + this.secret = secret.addTargetAttachment('AttachedSecret', { + target: this + }); + } + + this.connections = new ec2.Connections({ + securityGroups: [this.securityGroup], + defaultPortRange: new ec2.TcpPort(this.instanceEndpoint.port) + }); + + this.setLogRetention(); + } +} + +/** + * Construction properties for a DatabaseInstanceReadReplica. + */ +export interface DatabaseInstanceReadReplicaProps extends DatabaseInstanceSourceProps { + /** + * The source database instance. + * + * Each DB instance can have a limited number of read replicas. For more + * information, see https://docs.aws.amazon.com/AmazonRDS/latest/DeveloperGuide/USER_ReadRepl.html. + * + */ + readonly sourceDatabaseInstance: IDatabaseInstance; + + /** + * Indicates whether the DB instance is encrypted. + * + * @default false + */ + readonly storageEncrypted?: boolean; + + /** + * The master key that's used to encrypt the DB instance. + * + * @default default master key + */ + readonly kmsKey?: kms.IKey; +} + +/** + * A read replica database instance. + * + * @resource AWS::RDS::DBInstance + */ +export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements IDatabaseInstance { + public readonly instanceIdentifier: string; + public readonly dbInstanceEndpointAddress: string; + public readonly dbInstanceEndpointPort: string; + public readonly instanceEndpoint: Endpoint; + public readonly connections: ec2.Connections; + + constructor(scope: Construct, id: string, props: DatabaseInstanceReadReplicaProps) { + super(scope, id, props); + + const instance = new CfnDBInstance(this, 'Resource', { + ...this.newCfnProps, + sourceDbInstanceIdentifier: props.sourceDatabaseInstance.instanceIdentifier, + kmsKeyId: props.kmsKey && props.kmsKey.keyArn, + storageEncrypted: props.kmsKey ? true : props.storageEncrypted, + }); + + this.instanceIdentifier = instance.dbInstanceId; + this.dbInstanceEndpointAddress = instance.dbInstanceEndpointAddress; + this.dbInstanceEndpointPort = instance.dbInstanceEndpointPort; + + // create a number token that represents the port of the instance + const portAttribute = new Token(() => instance.dbInstanceEndpointPort).toNumber(); + this.instanceEndpoint = new Endpoint(instance.dbInstanceEndpointAddress, portAttribute); + + const deleteReplacePolicy = props.deleteReplacePolicy || DeletionPolicy.Retain; + instance.options.deletionPolicy = deleteReplacePolicy; + instance.options.updateReplacePolicy = deleteReplacePolicy; + + this.connections = new ec2.Connections({ + securityGroups: [this.securityGroup], + defaultPortRange: new ec2.TcpPort(this.instanceEndpoint.port) + }); + + this.setLogRetention(); + } +} + +/** + * Renders the processor features specifications + * + * @param features the processor features + */ +function renderProcessorFeatures(features: ProcessorFeatures): CfnDBInstance.ProcessorFeatureProperty[] | undefined { + const featuresList = Object.entries(features).map(([name, value]) => ({ name, value: value.toString() })); + + return featuresList.length === 0 ? undefined : featuresList; +} diff --git a/packages/@aws-cdk/aws-rds/lib/option-group.ts b/packages/@aws-cdk/aws-rds/lib/option-group.ts new file mode 100644 index 0000000000000..0180d1f3800fc --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/option-group.ts @@ -0,0 +1,167 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import { Construct, IResource, Resource } from '@aws-cdk/cdk'; +import { DatabaseInstanceEngine } from './instance'; +import { CfnOptionGroup } from './rds.generated'; + +/** + * An option group + */ +export interface IOptionGroup extends IResource { + /** + * The name of the option group. + * + * @attribute + */ + readonly optionGroupName: string; +} + +/** + * Reference to an existing option group. + */ +export interface OptionGroupAttributes { + /** + * The name of the option group. + */ + readonly optionGroupName: string; +} + +/** + * Configuration properties for an option. + */ +export interface OptionConfiguration { + /** + * The name of the option. + */ + readonly name: string; + + /** + * The settings for the option. + * + * @default no settings + */ + readonly settings?: { [name: string]: string }; + + /** + * The version for the option. + * + * @default no version + */ + readonly version?: string; + + /** + * The port number that this option uses. If `port` is specified then `vpc` + * must also be specified. + * + * @default no port + */ + readonly port?: number; + + /** + * The VPC where a security group should be created for this option. If `vpc` + * is specified then `port` must also be specified. + */ + readonly vpc?: ec2.IVpc; +} + +/** + * Construction properties for an OptionGroup. + */ +export interface OptionGroupProps { + /** + * The database engine that this option group is associated with. + */ + readonly engine: DatabaseInstanceEngine; + + /** + * The major version number of the database engine that this option group + * is associated with. + */ + readonly majorEngineVersion: string; + + /** + * A description of the option group. + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * The configurations for this option group. + */ + readonly configurations: OptionConfiguration[]; +} + +export class OptionGroup extends Resource implements IOptionGroup { + /** + * Import an existing option group. + */ + public static fromOptionGroupName(scope: Construct, id: string, optionGroupName: string): IOptionGroup { + class Import extends Construct { + public readonly optionGroupName = optionGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the option group. + */ + public readonly optionGroupName: string; + + /** + * The connections object for the options. + */ + public readonly optionConnections: { [key: string]: ec2.Connections } = {}; + + constructor(scope: Construct, id: string, props: OptionGroupProps) { + super(scope, id); + + const optionGroup = new CfnOptionGroup(this, 'Resource', { + engineName: props.engine.name, + majorEngineVersion: props.majorEngineVersion, + optionGroupDescription: props.description || `Option group for ${props.engine.name} ${props.majorEngineVersion}`, + optionConfigurations: this.renderConfigurations(props.configurations) + }); + + this.optionGroupName = optionGroup.optionGroupName; + } + + /** + * Renders the option configurations specifications. + */ + private renderConfigurations(configurations: OptionConfiguration[]): CfnOptionGroup.OptionConfigurationProperty[] { + const configs: CfnOptionGroup.OptionConfigurationProperty[] = []; + for (const config of configurations) { + let configuration: CfnOptionGroup.OptionConfigurationProperty = { + optionName: config.name, + optionSettings: config.settings && Object.entries(config.settings).map(([name, value]) => ({ name, value })), + optionVersion: config.version + }; + + if (config.port) { + if (!config.vpc) { + throw new Error('`port` and `vpc` must be specified together.'); + } + + const securityGroup = new ec2.SecurityGroup(this, `SecurityGroup${config.name}`, { + description: `Security group for ${config.name} option`, + vpc: config.vpc + }); + + this.optionConnections[config.name] = new ec2.Connections({ + securityGroups: [securityGroup], + defaultPortRange: new ec2.TcpPort(config.port) + }); + + configuration = { + ...configuration, + port: config.port, + vpcSecurityGroupMemberships: [securityGroup.securityGroupId] + }; + } + + configs.push(configuration); + } + + return configs; + } +} diff --git a/packages/@aws-cdk/aws-rds/lib/parameter-group.ts b/packages/@aws-cdk/aws-rds/lib/parameter-group.ts new file mode 100644 index 0000000000000..4344faf63072b --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/parameter-group.ts @@ -0,0 +1,119 @@ +import { Construct, IResource, Resource } from '@aws-cdk/cdk'; +import { CfnDBClusterParameterGroup, CfnDBParameterGroup } from './rds.generated'; + +/** + * A parameter group + */ +export interface IParameterGroup extends IResource { + /** + * The name of this parameter group + */ + readonly parameterGroupName: string; +} + +/** + * Reference to an existing parameter group + */ +export interface ParameterGroupAttributes { + /** + * The name of the parameter group + */ + readonly parameterGroupName: string; +} + +/** + * A new cluster or instance parameter group + */ +abstract class ParameterGroupBase extends Resource implements IParameterGroup { + /** + * Imports a parameter group + */ + public static fromParameterGroupName(scope: Construct, id: string, parameterGroupName: string): IParameterGroup { + class Import extends Construct implements IParameterGroup { + public readonly parameterGroupName = parameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public abstract readonly parameterGroupName: string; +} + +/** + * Properties for a parameter group + */ +export interface ParameterGroupProps { + /** + * Database family of this parameter group + */ + readonly family: string; + + /** + * Description for this parameter group + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * The parameters in this parameter group + */ + readonly parameters: { [key: string]: string }; +} + +/** + * A parameter group + * + * @resource AWS::RDS::DBParameterGroup + */ +export class ParameterGroup extends ParameterGroupBase { + /** + * The name of the parameter group + */ + public readonly parameterGroupName: string; + + constructor(scope: Construct, id: string, props: ParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBParameterGroup(this, 'Resource', { + description: props.description || `Parameter group for ${props.family}`, + family: props.family, + parameters: props.parameters, + }); + + this.parameterGroupName = resource.dbParameterGroupName; + } +} + +/** + * Construction properties for a ClusterParameterGroup + */ +// tslint:disable-next-line:no-empty-interface +export interface ClusterParameterGroupProps extends ParameterGroupProps { + +} +/** + * A cluster parameter group + * + * @resource AWS::RDS::DBClusterParameterGroup + */ +export class ClusterParameterGroup extends ParameterGroupBase { + /** + * The name of the parameter group + */ + public readonly parameterGroupName: string; + + constructor(scope: Construct, id: string, props: ClusterParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBClusterParameterGroup(this, 'Resource', { + description: props.description || `Cluster parameter group for ${props.family}`, + family: props.family, + parameters: props.parameters, + }); + + this.parameterGroupName = resource.dbClusterParameterGroupName; + } +} diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index 869b1ec85371a..cde179018c92d 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -1,15 +1,32 @@ import ec2 = require('@aws-cdk/aws-ec2'); import kms = require('@aws-cdk/aws-kms'); import { SecretValue } from '@aws-cdk/cdk'; +import { IParameterGroup } from './parameter-group'; +import { SecretRotationApplication } from './secret-rotation'; /** - * The engine for the database cluster + * A database cluster engine. Provides mapping to the serverless application + * used for secret rotation. */ -export enum DatabaseClusterEngine { - Aurora = 'aurora', - AuroraMysql = 'aurora-mysql', - AuroraPostgresql = 'aurora-postgresql', - Neptune = 'neptune' +export class DatabaseClusterEngine { + public static readonly Aurora = new DatabaseClusterEngine('aurora', SecretRotationApplication.MysqlRotationSingleUser); + public static readonly AuroraMysql = new DatabaseClusterEngine('aurora-mysql', SecretRotationApplication.MysqlRotationSingleUser); + public static readonly AuroraPostgresql = new DatabaseClusterEngine('aurora-postgresql', SecretRotationApplication.PostgresRotationSingleUser); + + /** + * The engine. + */ + public readonly name: string; + + /** + * The secret rotation application. + */ + public readonly secretRotationApplication: SecretRotationApplication; + + constructor(name: string, secretRotationApplication: SecretRotationApplication) { + this.name = name; + this.secretRotationApplication = secretRotationApplication; + } } /** @@ -30,13 +47,24 @@ export interface InstanceProps { /** * Where to place the instances within the VPC + * + * @default private subnets */ readonly vpcSubnets?: ec2.SubnetSelection; /** - * Security group. If not specified a new one will be created. + * Security group. + * + * @default a new security group is created. */ readonly securityGroup?: ec2.ISecurityGroup; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; } /** @@ -89,8 +117,3 @@ export interface Login { */ readonly kmsKey?: kms.IKey; } - -/** - * Type for database parameters - */ -export type Parameters = {[key: string]: any}; diff --git a/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts b/packages/@aws-cdk/aws-rds/lib/secret-rotation.ts similarity index 51% rename from packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts rename to packages/@aws-cdk/aws-rds/lib/secret-rotation.ts index c877d3c4044cc..2131992d69382 100644 --- a/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts +++ b/packages/@aws-cdk/aws-rds/lib/secret-rotation.ts @@ -2,17 +2,26 @@ import ec2 = require('@aws-cdk/aws-ec2'); import lambda = require('@aws-cdk/aws-lambda'); import serverless = require('@aws-cdk/aws-sam'); import secretsmanager = require('@aws-cdk/aws-secretsmanager'); -import cdk = require('@aws-cdk/cdk'); +import { Construct } from '@aws-cdk/cdk'; /** - * A serverless application location. + * A secret rotation serverless application. */ -export class ServerlessApplicationLocation { - public static readonly MariaDbRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSMariaDBRotationSingleUser', '1.0.46'); - public static readonly MysqlRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSMySQLRotationSingleUser', '1.0.74'); - public static readonly OracleRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSOracleRotationSingleUser', '1.0.45'); - public static readonly PostgresRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSPostgreSQLRotationSingleUser', '1.0.75'); - public static readonly SqlServerRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSSQLServerRotationSingleUser', '1.0.74'); +export class SecretRotationApplication { + public static readonly MariaDbRotationSingleUser = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationSingleUser', '1.0.57'); + public static readonly MariaDBRotationMultiUser = new SecretRotationApplication('SecretsManagerRDSMariaDBRotationMultiUser', '1.0.57'); + + public static readonly MysqlRotationSingleUser = new SecretRotationApplication('SecretsManagerRDSMySQLRotationSingleUser', '1.0.85'); + public static readonly MysqlRotationMultiUser = new SecretRotationApplication('SecretsManagerRDSMySQLRotationMultiUser', '1.0.85'); + + public static readonly OracleRotationSingleUser = new SecretRotationApplication('SecretsManagerRDSOracleRotationSingleUser', '1.0.56'); + public static readonly OracleRotationMultiUser = new SecretRotationApplication('SecretsManagerRDSOracleRotationMultiUser', '1.0.56'); + + public static readonly PostgresRotationSingleUser = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationSingleUser', '1.0.86'); + public static readonly PostgreSQLRotationMultiUser = new SecretRotationApplication('SecretsManagerRDSPostgreSQLRotationMultiUser ', '1.0.86'); + + public static readonly SqlServerRotationSingleUser = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationSingleUser', '1.0.57'); + public static readonly SqlServerRotationMultiUser = new SecretRotationApplication('SecretsManagerRDSSQLServerRotationMultiUser', '1.0.57'); public readonly applicationId: string; public readonly semanticVersion: string; @@ -24,39 +33,9 @@ export class ServerlessApplicationLocation { } /** - * The RDS database engine - */ -export enum DatabaseEngine { - /** - * MariaDB - */ - MariaDb = 'mariadb', - - /** - * MySQL - */ - Mysql = 'mysql', - - /** - * Oracle - */ - Oracle = 'oracle', - - /** - * PostgreSQL - */ - Postgres = 'postgres', - - /** - * SQL Server - */ - SqlServer = 'sqlserver' -} - -/** - * Options to add single user rotation to a database instance or cluster. + * Options to add secret rotation to a database instance or cluster. */ -export interface RotationSingleUserOptions { +export interface SecretRotationOptions { /** * Specifies the number of days after the previous rotation before * Secrets Manager triggers the next automatic rotation. @@ -64,19 +43,12 @@ export interface RotationSingleUserOptions { * @default 30 days */ readonly automaticallyAfterDays?: number; - - /** - * The location of the serverless application for the rotation. - * - * @default derived from the target's engine - */ - readonly serverlessApplicationLocation?: ServerlessApplicationLocation } /** - * Construction properties for a RotationSingleUser. + * Construction properties for a SecretRotation. */ -export interface RotationSingleUserProps extends RotationSingleUserOptions { +export interface SecretRotationProps extends SecretRotationOptions { /** * The secret to rotate. It must be a JSON string with the following format: * { @@ -85,7 +57,8 @@ export interface RotationSingleUserProps extends RotationSingleUserOptions { * 'username': , * 'password': , * 'dbname': , - * 'port': + * 'port': , + * 'masterarn': * } * * This is typically the case for a secret referenced from an AWS::SecretsManager::SecretTargetAttachment @@ -94,11 +67,9 @@ export interface RotationSingleUserProps extends RotationSingleUserOptions { readonly secret: secretsmanager.ISecret; /** - * The database engine. Either `serverlessApplicationLocation` or `engine` must be specified. - * - * @default - No engine specified. + * The serverless application for the rotation. */ - readonly engine?: DatabaseEngine; + readonly application: SecretRotationApplication; /** * The VPC where the Lambda rotation function will run. @@ -119,16 +90,12 @@ export interface RotationSingleUserProps extends RotationSingleUserOptions { } /** - * Single user secret rotation for a database instance or cluster. + * Secret rotation for a database instance or cluster. */ -export class RotationSingleUser extends cdk.Construct { - constructor(scope: cdk.Construct, id: string, props: RotationSingleUserProps) { +export class SecretRotation extends Construct { + constructor(scope: Construct, id: string, props: SecretRotationProps) { super(scope, id); - if (!props.serverlessApplicationLocation && !props.engine) { - throw new Error('Either `serverlessApplicationLocation` or `engine` must be specified.'); - } - if (!props.target.connections.defaultPortRange) { throw new Error('The `target` connections must have a default port range.'); } @@ -144,7 +111,7 @@ export class RotationSingleUser extends cdk.Construct { props.target.connections.allowDefaultPortFrom(securityGroup); const application = new serverless.CfnApplication(this, 'Resource', { - location: props.serverlessApplicationLocation || getApplicationLocation(props.engine), + location: props.application, parameters: { endpoint: `https://secretsmanager.${this.node.stack.region}.${this.node.stack.urlSuffix}`, functionName: rotationFunctionName, @@ -161,8 +128,8 @@ export class RotationSingleUser extends cdk.Construct { resourceName: rotationFunctionName })); - // Cannot use rotationLambda.addPermission because it currently does not - // return a cdk.Construct and we need to add a dependency. + // Cannot use rotationLambda.addPermission because it's a no-op on imported + // functions. const permission = new lambda.CfnPermission(this, 'Permission', { action: 'lambda:InvokeFunction', functionName: rotationFunctionName, @@ -177,26 +144,3 @@ export class RotationSingleUser extends cdk.Construct { rotationSchedule.node.addDependency(permission); // Cannot rotate without permission } } - -/** - * Returns the location for the rotation single user application. - * - * @param engine the database engine - * @throws if the engine is not supported - */ -function getApplicationLocation(engine: string = ''): ServerlessApplicationLocation { - switch (engine) { - case DatabaseEngine.MariaDb: - return ServerlessApplicationLocation.MariaDbRotationSingleUser; - case DatabaseEngine.Mysql: - return ServerlessApplicationLocation.MysqlRotationSingleUser; - case DatabaseEngine.Oracle: - return ServerlessApplicationLocation.OracleRotationSingleUser; - case DatabaseEngine.Postgres: - return ServerlessApplicationLocation.PostgresRotationSingleUser; - case DatabaseEngine.SqlServer: - return ServerlessApplicationLocation.SqlServerRotationSingleUser; - default: - throw new Error(`Engine ${engine} not supported for single user rotation.`); - } -} diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 58603670a7ce8..c60e3fb830a42 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -61,31 +61,43 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.33.0", + "@aws-cdk/aws-events-targets": "^0.33.0", "cdk-build-tools": "^0.33.0", "cdk-integ-tools": "^0.33.0", "cfn2ts": "^0.33.0", "pkglint": "^0.33.0" }, "dependencies": { + "@aws-cdk/aws-cloudwatch": "^0.33.0", "@aws-cdk/aws-ec2": "^0.33.0", + "@aws-cdk/aws-events": "^0.33.0", "@aws-cdk/aws-iam": "^0.33.0", "@aws-cdk/aws-kms": "^0.33.0", "@aws-cdk/aws-lambda": "^0.33.0", + "@aws-cdk/aws-logs": "^0.33.0", "@aws-cdk/aws-sam": "^0.33.0", "@aws-cdk/aws-secretsmanager": "^0.33.0", "@aws-cdk/cdk": "^0.33.0" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-cloudwatch": "^0.33.0", "@aws-cdk/aws-ec2": "^0.33.0", + "@aws-cdk/aws-events": "^0.33.0", "@aws-cdk/aws-iam": "^0.33.0", "@aws-cdk/aws-kms": "^0.33.0", "@aws-cdk/aws-lambda": "^0.33.0", + "@aws-cdk/aws-logs": "^0.33.0", "@aws-cdk/aws-sam": "^0.33.0", "@aws-cdk/aws-secretsmanager": "^0.33.0", "@aws-cdk/cdk": "^0.33.0" }, "engines": { "node": ">= 8.10.0" + }, + "awslint": { + "exclude": [ + "construct-base-is-private:@aws-cdk/aws-rds.DatabaseInstanceBase" + ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json index 774d57074bb82..306be57a6bd12 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json @@ -724,7 +724,7 @@ "Properties": { "Location": { "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMySQLRotationSingleUser", - "SemanticVersion": "1.0.74" + "SemanticVersion": "1.0.85" }, "Parameters": { "endpoint": { diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts index 84f843221df86..5e24f1370df2f 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts @@ -3,7 +3,7 @@ import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); import { SecretValue } from '@aws-cdk/cdk'; import { DatabaseCluster, DatabaseClusterEngine } from '../lib'; -import { ClusterParameterGroup } from '../lib/cluster-parameter-group'; +import { ClusterParameterGroup } from '../lib/parameter-group'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-rds-integ'); @@ -13,8 +13,10 @@ const vpc = new ec2.Vpc(stack, 'VPC', { maxAZs: 2 }); const params = new ClusterParameterGroup(stack, 'Params', { family: 'aurora5.6', description: 'A nice parameter group', + parameters: { + character_set_database: 'utf8mb4' + } }); -params.setParameter('character_set_database', 'utf8mb4'); const kmsKey = new kms.Key(stack, 'DbSecurity'); const cluster = new DatabaseCluster(stack, 'Database', { diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json new file mode 100644 index 0000000000000..fa1f1d4ac9f2d --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -0,0 +1,1126 @@ +{ + "Transform": "AWS::Serverless-2016-10-31", + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "ParameterGroup5E32DECB": { + "Type": "AWS::RDS::DBParameterGroup", + "Properties": { + "Description": "Parameter group for oracle-se1-11.2", + "Family": "oracle-se1-11.2", + "Parameters": { + "open_cursors": "2500" + } + } + }, + "OptionGroupSecurityGroupOEM7E39FD8C": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for OEM option", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:1158", + "FromPort": 1158, + "IpProtocol": "tcp", + "ToPort": 1158 + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "OptionGroupACA43DC1": { + "Type": "AWS::RDS::OptionGroup", + "Properties": { + "EngineName": "oracle-se1", + "MajorEngineVersion": "11.2", + "OptionConfigurations": [ + { + "OptionName": "XMLDB" + }, + { + "OptionName": "OEM", + "Port": 1158, + "VpcSecurityGroupMemberships": [ + { + "Fn::GetAtt": [ + "OptionGroupSecurityGroupOEM7E39FD8C", + "GroupId" + ] + } + ] + } + ], + "OptionGroupDescription": "Option group for oracle-se1 11.2" + } + }, + "InstanceSubnetGroupF2CBA54F": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnet group for Instance database", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + } + }, + "InstanceSecurityGroupB4E5FA83": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for Instance database", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "InstanceSecurityGroupfrom00000IndirectPort7D6BC055": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:{IndirectPort}", + "FromPort": { + "Fn::GetAtt": [ + "InstanceC1063A87", + "Endpoint.Port" + ] + }, + "GroupId": { + "Fn::GetAtt": [ + "InstanceSecurityGroupB4E5FA83", + "GroupId" + ] + }, + "ToPort": { + "Fn::GetAtt": [ + "InstanceC1063A87", + "Endpoint.Port" + ] + } + } + }, + "InstanceSecurityGroupfromawscdkrdsinstanceInstanceRotationSecurityGroupBB71D98EIndirectPort60E4E51A": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdkrdsinstanceInstanceRotationSecurityGroupBB71D98E:{IndirectPort}", + "FromPort": { + "Fn::GetAtt": [ + "InstanceC1063A87", + "Endpoint.Port" + ] + }, + "GroupId": { + "Fn::GetAtt": [ + "InstanceSecurityGroupB4E5FA83", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "InstanceRotationSecurityGroupEF8D211E", + "GroupId" + ] + }, + "ToPort": { + "Fn::GetAtt": [ + "InstanceC1063A87", + "Endpoint.Port" + ] + } + } + }, + "InstanceMonitoringRole3E2B4286": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "monitoring.rds.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole" + ] + ] + } + ] + } + }, + "InstanceSecret478E0A47": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": { + "ExcludeCharacters": "\"@/\\", + "GenerateStringKey": "password", + "PasswordLength": 30, + "SecretStringTemplate": "{\"username\":\"syscdk\"}" + } + } + }, + "InstanceSecretAttachedSecretBACA1D43": { + "Type": "AWS::SecretsManager::SecretTargetAttachment", + "Properties": { + "SecretId": { + "Ref": "InstanceSecret478E0A47" + }, + "TargetId": { + "Ref": "InstanceC1063A87" + }, + "TargetType": "AWS::RDS::DBInstance" + } + }, + "InstanceSecretAttachedSecretRotationSchedule275109B7": { + "Type": "AWS::SecretsManager::RotationSchedule", + "Properties": { + "SecretId": { + "Ref": "InstanceSecretAttachedSecretBACA1D43" + }, + "RotationLambdaARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":lambda:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":function:awscdkrdsinstanceInstanceRotation0925DC60" + ] + ] + }, + "RotationRules": { + "AutomaticallyAfterDays": 30 + } + }, + "DependsOn": [ + "InstanceRotationPermission63844D0A" + ] + }, + "InstanceC1063A87": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t2.medium", + "AllocatedStorage": "100", + "AutoMinorVersionUpgrade": false, + "BackupRetentionPeriod": "7", + "CopyTagsToSnapshot": true, + "DBName": "ORCL", + "DBParameterGroupName": { + "Ref": "ParameterGroup5E32DECB" + }, + "DBSubnetGroupName": { + "Ref": "InstanceSubnetGroupF2CBA54F" + }, + "DeletionProtection": true, + "EnableCloudwatchLogsExports": [ + "trace", + "audit", + "alert", + "listener" + ], + "EnablePerformanceInsights": true, + "Engine": "oracle-se1", + "Iops": 1000, + "LicenseModel": "bring-your-own-license", + "MasterUsername": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "InstanceSecret478E0A47" + }, + ":SecretString:username::}}" + ] + ] + }, + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "InstanceSecret478E0A47" + }, + ":SecretString:password::}}" + ] + ] + }, + "MonitoringInterval": 60, + "MonitoringRoleArn": { + "Fn::GetAtt": [ + "InstanceMonitoringRole3E2B4286", + "Arn" + ] + }, + "MultiAZ": true, + "OptionGroupName": { + "Ref": "OptionGroupACA43DC1" + }, + "PerformanceInsightsRetentionPeriod": 7, + "StorageEncrypted": true, + "StorageType": "io1", + "VPCSecurityGroups": [ + { + "Fn::GetAtt": [ + "InstanceSecurityGroupB4E5FA83", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "InstanceLogRetentiontrace487771C8": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/rds/instance/", + { + "Ref": "InstanceC1063A87" + }, + "/trace" + ] + ] + }, + "RetentionInDays": 30 + } + }, + "InstanceLogRetentionaudit55C07CF6": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/rds/instance/", + { + "Ref": "InstanceC1063A87" + }, + "/audit" + ] + ] + }, + "RetentionInDays": 30 + } + }, + "InstanceLogRetentionalert2B4B024B": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/rds/instance/", + { + "Ref": "InstanceC1063A87" + }, + "/alert" + ] + ] + }, + "RetentionInDays": 30 + } + }, + "InstanceLogRetentionlistener232E8C3C": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/rds/instance/", + { + "Ref": "InstanceC1063A87" + }, + "/listener" + ] + ] + }, + "RetentionInDays": 30 + } + }, + "InstanceRotationSecurityGroupEF8D211E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-rds-instance/Instance/Rotation/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "InstanceRotationAA37A997": { + "Type": "AWS::Serverless::Application", + "Properties": { + "Location": { + "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSOracleRotationSingleUser", + "SemanticVersion": "1.0.56" + }, + "Parameters": { + "endpoint": { + "Fn::Join": [ + "", + [ + "https://secretsmanager.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + }, + "functionName": "awscdkrdsinstanceInstanceRotation0925DC60", + "vpcSecurityGroupIds": { + "Fn::GetAtt": [ + "InstanceRotationSecurityGroupEF8D211E", + "GroupId" + ] + }, + "vpcSubnetIds": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + ",", + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + ] + } + } + } + }, + "InstanceRotationPermission63844D0A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": "awscdkrdsinstanceInstanceRotation0925DC60", + "Principal": { + "Fn::Join": [ + "", + [ + "secretsmanager.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + }, + "DependsOn": [ + "InstanceRotationAA37A997" + ] + }, + "InstanceAvailabilityAD5D452C": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.rds" + ], + "resources": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":rds:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":db:", + { + "Ref": "InstanceC1063A87" + } + ] + ] + } + ], + "detail": { + "EventCategories": [ + "availability" + ] + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "Id": "Function" + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "Roles": [ + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3BucketB81211B5" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + ] + }, + "HighCPU94686517": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "Threshold": 90, + "Dimensions": [ + { + "Name": "DBInstanceIdentifier", + "Value": { + "Ref": "InstanceC1063A87" + } + } + ], + "MetricName": "CPUUtilization", + "Namespace": "AWS/RDS", + "Period": 300, + "Statistic": "Average" + } + }, + "FunctionServiceRole675BB04A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Function76856677": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(event);" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionServiceRole675BB04A", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "FunctionServiceRole675BB04A" + ] + }, + "FunctionAllowEventRuleawscdkrdsinstanceInstanceAvailabilityCE39A6A71E819C19": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "InstanceAvailabilityAD5D452C", + "Arn" + ] + } + } + } + }, + "Parameters": { + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3BucketB81211B5": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-rds-instance/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-rds-instance/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeArtifactHash327647CC": { + "Type": "String", + "Description":"Artifact hash for asset \"aws-cdk-rds-instance/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts new file mode 100644 index 0000000000000..a8f1143dc96aa --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.ts @@ -0,0 +1,104 @@ +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); +import ec2 = require('@aws-cdk/aws-ec2'); +import targets = require('@aws-cdk/aws-events-targets'); +import lambda = require('@aws-cdk/aws-lambda'); +import logs = require('@aws-cdk/aws-logs'); +import cdk = require('@aws-cdk/cdk'); +import rds = require('../lib'); + +const app = new cdk.App(); + +class DatabaseInstanceStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAZs: 2 }); + + /// !show + // Set open cursors with parameter group + const parameterGroup = new rds.ParameterGroup(this, 'ParameterGroup', { + family: 'oracle-se1-11.2', + parameters: { + open_cursors: '2500' + } + }); + + /// Add XMLDB and OEM with option group + const optionGroup = new rds.OptionGroup(this, 'OptionGroup', { + engine: rds.DatabaseInstanceEngine.OracleSE1, + majorEngineVersion: '11.2', + configurations: [ + { + name: 'XMLDB' + }, + { + name: 'OEM', + port: 1158, + vpc + } + ] + }); + + // Allow connections to OEM + optionGroup.optionConnections.OEM.connections.allowDefaultPortFromAnyIpv4(); + + // Database instance with production values + const instance = new rds.DatabaseInstance(this, 'Instance', { + engine: rds.DatabaseInstanceEngine.OracleSE1, + licenseModel: rds.LicenseModel.BringYourOwnLicense, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Medium), + multiAz: true, + storageType: rds.StorageType.IO1, + masterUsername: 'syscdk', + vpc, + databaseName: 'ORCL', + storageEncrypted: true, + backupRetentionPeriod: 7, + monitoringInterval: 60, + enablePerformanceInsights: true, + cloudwatchLogsExports: [ + 'trace', + 'audit', + 'alert', + 'listener' + ], + cloudwatchLogsRetention: logs.RetentionDays.OneMonth, + autoMinorVersionUpgrade: false, + optionGroup, + parameterGroup + }); + + // Allow connections on default port from any IPV4 + instance.connections.allowDefaultPortFromAnyIpv4(); + + // Rotate the master user password every 30 days + instance.addRotationSingleUser('Rotation'); + + // Add alarm for high CPU + new cloudwatch.Alarm(this, 'HighCPU', { + metric: instance.metricCPUUtilization(), + threshold: 90, + evaluationPeriods: 1 + }); + + // Trigger Lambda function on instance availability events + const fn = new lambda.Function(this, 'Function', { + code: lambda.Code.inline('exports.handler = (event) => console.log(event);'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 + }); + + const availabilityRule = instance.onEvent('Availability', { target: new targets.LambdaFunction(fn) }); + availabilityRule.addEventPattern({ + detail: { + EventCategories: [ + 'availability' + ] + } + }); + /// !hide + } +} + +new DatabaseInstanceStack(app, 'aws-cdk-rds-instance'); +app.run(); diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 09d7e6a8615d2..2924020ae6198 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -4,7 +4,7 @@ import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); import { SecretValue } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine } from '../lib'; +import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine, ParameterGroup } from '../lib'; export = { 'check that instantiation works'(test: Test) { @@ -145,18 +145,6 @@ export = { test.done(); }, - 'import/export cluster parameter group'(test: Test) { - // GIVEN - const stack = testStack(); - - // WHEN - const imported = ClusterParameterGroup.fromParameterGroupName(stack, 'ImportParams', 'name-of-param-group'); - - // THEN - test.deepEqual(stack.node.resolve(imported.parameterGroupName), 'name-of-param-group'); - test.done(); - }, - 'creates a secret when master credentials are not specified'(test: Test) { // GIVEN const stack = testStack(); @@ -243,7 +231,41 @@ export = { })); test.done(); - } + }, + + 'cluster with instance parameter group'(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const parameterGroup = new ParameterGroup(stack, 'ParameterGroup', { + family: 'hello', + parameters: { + key: 'value' + } + }); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.Aurora, + masterUser: { + username: 'admin', + }, + instanceProps: { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + parameterGroup, + vpc + } + }); + + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + DBParameterGroupName: { + Ref: 'ParameterGroup5E32DECB' + } + })); + + test.done(); + + }, }; function testStack() { diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts new file mode 100644 index 0000000000000..cfd1287ba49d2 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -0,0 +1,380 @@ +import { countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import targets = require('@aws-cdk/aws-events-targets'); +import lambda = require('@aws-cdk/aws-lambda'); +import logs = require('@aws-cdk/aws-logs'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import rds = require('../lib'); + +export = { + 'create a DB instance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.OracleSE1, + licenseModel: rds.LicenseModel.BringYourOwnLicense, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Medium), + multiAz: true, + storageType: rds.StorageType.IO1, + masterUsername: 'syscdk', + vpc, + databaseName: 'ORCL', + storageEncrypted: true, + backupRetentionPeriod: 7, + monitoringInterval: 60, + enablePerformanceInsights: true, + cloudwatchLogsExports: [ + 'trace', + 'audit', + 'alert', + 'listener' + ], + cloudwatchLogsRetention: logs.RetentionDays.OneMonth, + autoMinorVersionUpgrade: false, + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + Properties: { + DBInstanceClass: 'db.t2.medium', + AllocatedStorage: '100', + AutoMinorVersionUpgrade: false, + BackupRetentionPeriod: '7', + CopyTagsToSnapshot: true, + DBName: 'ORCL', + DBSubnetGroupName: { + Ref: 'InstanceSubnetGroupF2CBA54F' + }, + DeletionProtection: true, + EnableCloudwatchLogsExports: [ + 'trace', + 'audit', + 'alert', + 'listener' + ], + EnablePerformanceInsights: true, + Engine: 'oracle-se1', + Iops: 1000, + LicenseModel: 'bring-your-own-license', + MasterUsername: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'InstanceSecret478E0A47' + }, + ':SecretString:username::}}' + ] + ] + }, + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'InstanceSecret478E0A47' + }, + ':SecretString:password::}}' + ] + ] + }, + MonitoringInterval: 60, + MonitoringRoleArn: { + 'Fn::GetAtt': [ + 'InstanceMonitoringRole3E2B4286', + 'Arn' + ] + }, + MultiAZ: true, + PerformanceInsightsRetentionPeriod: 7, + StorageEncrypted: true, + StorageType: 'io1', + VPCSecurityGroups: [ + { + 'Fn::GetAtt': [ + 'InstanceSecurityGroupB4E5FA83', + 'GroupId' + ] + } + ] + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain' + }, ResourcePart.CompleteDefinition)); + + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain' + }, ResourcePart.CompleteDefinition)); + + expect(stack).to(haveResource('AWS::RDS::DBSubnetGroup', { + DBSubnetGroupDescription: 'Subnet group for Instance database', + SubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' + }, + { + Ref: 'VPCPrivateSubnet3Subnet3EDCD457' + } + ] + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Security group for Instance database', + })); + + expect(stack).to(haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'monitoring.rds.amazonaws.com' + } + } + ], + Version: '2012-10-17' + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition' + }, + ':iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole' + ] + ] + } + ] + })); + + expect(stack).to(haveResource('AWS::SecretsManager::Secret', { + GenerateSecretString: { + ExcludeCharacters: '\"@/\\', + GenerateStringKey: 'password', + PasswordLength: 30, + SecretStringTemplate: '{"username":"syscdk"}' + } + })); + + expect(stack).to(haveResource('AWS::SecretsManager::SecretTargetAttachment', { + SecretId: { + Ref: 'InstanceSecret478E0A47' + }, + TargetId: { + Ref: 'InstanceC1063A87' + }, + TargetType: 'AWS::RDS::DBInstance' + })); + + expect(stack).to(countResources('Custom::LogRetention', 4)); + + test.done(); + }, + + 'instance with option and parameter group'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + const optionGroup = new rds.OptionGroup(stack, 'OptionGroup', { + engine: rds.DatabaseInstanceEngine.OracleSE1, + majorEngineVersion: '11.2', + configurations: [ + { + name: 'XMLDB' + } + ] + }); + + const parameterGroup = new rds.ParameterGroup(stack, 'ParameterGroup', { + family: 'hello', + description: 'desc', + parameters: { + key: 'value' + } + }); + + // WHEN + new rds.DatabaseInstance(stack, 'Database', { + engine: rds.DatabaseInstanceEngine.SqlServerEE, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + masterUsername: 'syscdk', + masterUserPassword: cdk.SecretValue.plainText('tooshort'), + vpc, + optionGroup, + parameterGroup + }); + + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + DBParameterGroupName: { + Ref: 'ParameterGroup5E32DECB' + }, + OptionGroupName: { + Ref: 'OptionGroupACA43DC1' + } + })); + + test.done(); + }, + + 'create an instance from snapshot'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { + snapshotIdentifier: 'my-snapshot', + engine: rds.DatabaseInstanceEngine.Postgres, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Large), + vpc + }); + + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + DBSnapshotIdentifier: 'my-snapshot' + })); + + test.done(); + }, + + 'throws when trying to generate a new password from snapshot without username'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // THEN + test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { + snapshotIdentifier: 'my-snapshot', + engine: rds.DatabaseInstanceEngine.Mysql, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Large), + vpc, + generateMasterUserPassword: true, + }), /`masterUsername`.*`generateMasterUserPassword`/); + + test.done(); + }, + + 'create a read replica'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const sourceInstance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.Mysql, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + masterUsername: 'admin', + vpc + }); + + // WHEN + new rds.DatabaseInstanceReadReplica(stack, 'ReadReplica', { + sourceDatabaseInstance: sourceInstance, + engine: rds.DatabaseInstanceEngine.Mysql, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Large), + vpc + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + SourceDBInstanceIdentifier: { + Ref: 'InstanceC1063A87' + } + })); + + test.done(); + }, + + 'on event'(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, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + masterUsername: 'admin', + vpc + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.inline('dummy'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 + }); + + // WHEN + instance.onEvent('InstanceEvent', { target: new targets.LambdaFunction(fn) }); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + EventPattern: { + source: [ + 'aws.rds' + ], + resources: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition' + }, + ':rds:', + { + Ref: 'AWS::Region' + }, + ':', + { + Ref: 'AWS::AccountId' + }, + ':db:', + { + Ref: 'InstanceC1063A87' + } + ] + ] + } + ] + } + })); + + test.done(); + }, + + 'can use metricCPUUtilization'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.Mysql, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + masterUsername: 'admin', + vpc + }); + + // THEN + test.deepEqual(stack.node.resolve(instance.metricCPUUtilization()), { + dimensions: { DBInstanceIdentifier: { Ref: 'InstanceC1063A87' } }, + namespace: 'AWS/RDS', + metricName: 'CPUUtilization', + periodSec: 300, + statistic: 'Average' + }); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-rds/test/test.option-group.ts b/packages/@aws-cdk/aws-rds/test/test.option-group.ts new file mode 100644 index 0000000000000..22c5f6999ccd2 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/test.option-group.ts @@ -0,0 +1,115 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import { DatabaseInstanceEngine, OptionGroup } from '../lib'; + +export = { + 'create an option group'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new OptionGroup(stack, 'Options', { + engine: DatabaseInstanceEngine.OracleSE1, + majorEngineVersion: '11.2', + configurations: [ + { + name: 'XMLDB' + } + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::OptionGroup', { + EngineName: 'oracle-se1', + MajorEngineVersion: '11.2', + OptionGroupDescription: 'Option group for oracle-se1 11.2', + OptionConfigurations: [ + { + OptionName: 'XMLDB' + } + ] + })); + + test.done(); + }, + + 'option group with security groups'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const optionGroup = new OptionGroup(stack, 'Options', { + engine: DatabaseInstanceEngine.OracleSE1, + majorEngineVersion: '11.2', + configurations: [ + { + name: 'OEM', + port: 1158, + vpc + } + ] + }); + optionGroup.optionConnections.OEM.connections.allowDefaultPortFromAnyIpv4(); + + // THEN + expect(stack).to(haveResource('AWS::RDS::OptionGroup', { + EngineName: 'oracle-se1', + MajorEngineVersion: '11.2', + OptionGroupDescription: 'Option group for oracle-se1 11.2', + OptionConfigurations: [ + { + OptionName: 'OEM', + Port: 1158, + VpcSecurityGroupMemberships: [ + { + 'Fn::GetAtt': [ + 'OptionsSecurityGroupOEM6C9FE79D', + 'GroupId' + ] + } + ] + } + ] + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Security group for OEM option', + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'from 0.0.0.0/0:1158', + FromPort: 1158, + IpProtocol: "tcp", + ToPort: 1158 + } + ], + VpcId: { + Ref: 'VPCB9E5F0B4' + } + })); + + test.done(); + }, + + 'throws when using an option with port and no vpc'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + test.throws(() => new OptionGroup(stack, 'Options', { + engine: DatabaseInstanceEngine.OracleSE1, + majorEngineVersion: '11.2', + configurations: [ + { + name: 'OEM', + port: 1158 + } + ] + }), /`port`.*`vpc`/); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-rds/test/test.parameter-group.ts b/packages/@aws-cdk/aws-rds/test/test.parameter-group.ts new file mode 100644 index 0000000000000..f48d1ea4f3891 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/test.parameter-group.ts @@ -0,0 +1,56 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import { ClusterParameterGroup, ParameterGroup } from '../lib'; + +export = { + 'create a parameter group'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ParameterGroup(stack, 'Params', { + family: 'hello', + description: 'desc', + parameters: { + key: 'value' + } + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBParameterGroup', { + Description: 'desc', + Family: 'hello', + Parameters: { + key: 'value' + } + })); + + test.done(); + }, + + 'create a cluster parameter group'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + family: 'hello', + description: 'desc', + parameters: { + key: 'value' + } + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBClusterParameterGroup', { + Description: 'desc', + Family: 'hello', + Parameters: { + key: 'value' + } + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts b/packages/@aws-cdk/aws-rds/test/test.secret-rotation.ts similarity index 55% rename from packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts rename to packages/@aws-cdk/aws-rds/test/test.secret-rotation.ts index 45c95cb7e9e69..edaf88c0af0b1 100644 --- a/packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts +++ b/packages/@aws-cdk/aws-rds/test/test.secret-rotation.ts @@ -5,6 +5,7 @@ import cdk = require('@aws-cdk/cdk'); import { SecretValue } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import rds = require('../lib'); +import { SecretRotationApplication } from '../lib'; // tslint:disable:object-literal-key-quotes @@ -93,7 +94,7 @@ export = { expect(stack).to(haveResource('AWS::Serverless::Application', { "Location": { "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMySQLRotationSingleUser", - "SemanticVersion": "1.0.74" + "SemanticVersion": "1.0.85" }, "Parameters": { "endpoint": { @@ -182,51 +183,193 @@ export = { test.done(); }, - 'throws when both application location and engine are not specified'(test: Test) { + 'throws when connections object has no default port range'(test: Test) { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); + const secret = new secretsmanager.Secret(stack, 'Secret'); const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc, }); + + // WHEN const target = new ec2.Connections({ - defaultPortRange: new ec2.TcpPort(1521), securityGroups: [securityGroup] }); - const secret = new secretsmanager.Secret(stack, 'Secret'); // THEN - test.throws(() => new rds.RotationSingleUser(stack, 'Rotation', { + test.throws(() => new rds.SecretRotation(stack, 'Rotation', { secret, + application: SecretRotationApplication.MysqlRotationSingleUser, vpc, target - }), /`serverlessApplicationLocation`.+`engine`/); + }), /`target`.+default port range/); test.done(); }, - 'throws when connections object has no default port range'(test: Test) { + 'add a rds rotation single user to an instance'(test: Test) { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); - const secret = new secretsmanager.Secret(stack, 'Secret'); - const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { - vpc, + const instance = new rds.DatabaseInstance(stack, 'Database', { + engine: rds.DatabaseInstanceEngine.MariaDb, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + masterUsername: 'syscdk', + vpc }); // WHEN - const target = new ec2.Connections({ - securityGroups: [securityGroup] + instance.addRotationSingleUser('Rotation'); + + // THEN + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + "IpProtocol": "tcp", + "Description": "from DatabaseRotationSecurityGroup1C5A8031:{IndirectPort}", + "FromPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Endpoint.Port" + ] + }, + "GroupId": { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "DatabaseRotationSecurityGroup17736B63", + "GroupId" + ] + }, + "ToPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Endpoint.Port" + ] + } + })); + + expect(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + "SecretId": { + "Ref": "DatabaseSecretAttachedSecretE6CAC445" + }, + "RotationLambdaARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":lambda:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":function:DatabaseRotation0D47EBD2" + ] + ] + }, + "RotationRules": { + "AutomaticallyAfterDays": 30 + } + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + "GroupDescription": "Database/Rotation/SecurityGroup" + })); + + expect(stack).to(haveResource('AWS::Serverless::Application', { + "Location": { + "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMariaDBRotationSingleUser", + "SemanticVersion": "1.0.57" + }, + "Parameters": { + "endpoint": { + "Fn::Join": [ + "", + [ + "https://secretsmanager.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + }, + "functionName": "DatabaseRotation0D47EBD2", + "vpcSecurityGroupIds": { + "Fn::GetAtt": [ + "DatabaseRotationSecurityGroup17736B63", + "GroupId" + ] + }, + "vpcSubnetIds": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + ",", + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + ",", + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + ] + } + } + })); + + expect(stack).to(haveResource('AWS::Lambda::Permission', { + "Action": "lambda:InvokeFunction", + "FunctionName": "DatabaseRotation0D47EBD2", + "Principal": { + "Fn::Join": [ + "", + [ + "secretsmanager.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + })); + + test.done(); + }, + + 'throws when trying to add rotation to an instance without secret'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const instance = new rds.DatabaseInstance(stack, 'Database', { + engine: rds.DatabaseInstanceEngine.SqlServerEE, + instanceClass: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + masterUsername: 'syscdk', + masterUserPassword: SecretValue.plainText('tooshort'), + vpc }); // THEN - test.throws(() => new rds.RotationSingleUser(stack, 'Rotation', { - secret, - engine: rds.DatabaseEngine.Mysql, - vpc, - target - }), /`target`.+default port range/); + test.throws(() => instance.addRotationSingleUser('Rotation'), /without secret/); test.done(); - } + }, }; diff --git a/packages/@aws-cdk/cfnspec/lib/augmentations/AWS_RDS_DBInstance.json b/packages/@aws-cdk/cfnspec/lib/augmentations/AWS_RDS_DBInstance.json new file mode 100644 index 0000000000000..d4e695ec82665 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/lib/augmentations/AWS_RDS_DBInstance.json @@ -0,0 +1,37 @@ +{ + "options": { + "classFile": "instance", + "class": "DatabaseInstanceBase", + "interface": "IDatabaseInstance" + }, + "metrics": { + "namespace": "AWS/RDS", + "dimensions": { "DBInstanceIdentifier": "this.instanceIdentifier" }, + "metrics": [ + { + "name": "CPUUtilization", + "documentation": "The percentage of CPU utilization." + }, + { + "name": "DatabaseConnections", + "documentation": "The number of database connections in use." + }, + { + "name": "FreeStorageSpace", + "documentation": "The amount of available storage space." + }, + { + "name": "FreeableMemory", + "documentation": "The amount of available random access memory." + }, + { + "name": "WriteIOPS", + "documentation": "The average number of disk read I/O operations per second." + }, + { + "name": "ReadIOPS", + "documentation": "The average number of disk write I/O operations per second." + } + ] + } +}