packages/aws-cdk-lib/aws-rds/lib/instance.ts (807 lines of code) (raw):

import { Construct } from 'constructs'; import { CaCertificate } from './ca-certificate'; import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; import { IInstanceEngine } from './instance-engine'; import { IOptionGroup } from './option-group'; import { IParameterGroup, ParameterGroup } from './parameter-group'; import { applyDefaultRotationOptions, defaultDeletionProtection, engineDescription, renderCredentials, setupS3ImportExport, helperRemovalPolicy, renderUnless } from './private/util'; import { Credentials, PerformanceInsightRetention, RotationMultiUserOptions, RotationSingleUserOptions, SnapshotCredentials } from './props'; import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy'; import { CfnDBInstance, CfnDBInstanceProps } from './rds.generated'; import { ISubnetGroup, SubnetGroup } from './subnet-group'; import * as ec2 from '../../aws-ec2'; import * as events from '../../aws-events'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; import * as logs from '../../aws-logs'; import * as s3 from '../../aws-s3'; import * as secretsmanager from '../../aws-secretsmanager'; import * as cxschema from '../../cloud-assembly-schema'; import { ArnComponents, ArnFormat, ContextProvider, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token, Tokenization } from '../../core'; import { ValidationError } from '../../core/lib/errors'; import { addConstructMetadata } from '../../core/lib/metadata-resource'; import * as cxapi from '../../cx-api'; /** * A database instance */ 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 EndpointAddress */ readonly dbInstanceEndpointAddress: string; /** * The instance endpoint port. * * @attribute EndpointPort */ readonly dbInstanceEndpointPort: string; /** * The AWS Region-unique, immutable identifier for the DB instance. * This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB instance is accessed. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#aws-resource-rds-dbinstance-return-values */ readonly instanceResourceId?: string; /** * The instance endpoint. */ readonly instanceEndpoint: Endpoint; /** * The engine of this database Instance. * May be not known for imported Instances if it wasn't provided explicitly, * or for read replicas. */ readonly engine?: IInstanceEngine; /** * Add a new db proxy to this instance. */ addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy; /** * Grant the given identity connection access to the database. * * @param grantee the Principal to grant the permissions to * @param dbUser the name of the database user to allow connecting as to the db instance */ grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant; /** * 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 AWS Region-unique, immutable identifier for the DB instance. * This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB instance is accessed. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#aws-resource-rds-dbinstance-return-values */ readonly instanceResourceId?: string; /** * The security groups of the instance. */ readonly securityGroups: ec2.ISecurityGroup[]; /** * The engine of the existing database Instance. * * @default - the imported Instance's engine is unknown */ readonly engine?: IInstanceEngine; } /** * A new or imported database instance. */ export abstract class DatabaseInstanceBase extends Resource implements IDatabaseInstance { /** * Lookup an existing DatabaseInstance using instanceIdentifier. */ public static fromLookup(scope: Construct, id: string, options: DatabaseInstanceLookupOptions): IDatabaseInstance { const response: {[key: string]: any}[] = ContextProvider.getValue(scope, { provider: cxschema.ContextProvider.CC_API_PROVIDER, props: { typeName: 'AWS::RDS::DBInstance', exactIdentifier: options.instanceIdentifier, propertiesToReturn: [ 'DBInstanceArn', 'Endpoint.Address', 'Endpoint.Port', 'DbiResourceId', 'DBSecurityGroups', ], } as cxschema.CcApiContextQuery, dummyValue: [ { 'Identifier': 'TEST', 'DBInstanceArn': 'TESTARN', 'Endpoint.Address': 'TESTADDRESS', 'Endpoint.Port': '5432', 'DbiResourceId': 'TESTID', 'DBSecurityGroups': [], }, ], }).value; // getValue returns a list of result objects. We are expecting 1 result or Error. const instance = response[0]; // Get ISecurityGroup from securityGroupId let securityGroups: ec2.ISecurityGroup[] = []; const dbsg: string[] = instance.DBSecurityGroups; if (dbsg) { securityGroups = dbsg.map(securityGroupId => { return ec2.SecurityGroup.fromSecurityGroupId( scope, `LSG-${securityGroupId}`, securityGroupId, ); }); } return this.fromDatabaseInstanceAttributes(scope, id, { instanceEndpointAddress: instance['Endpoint.Address'], port: instance['Endpoint.Port'], instanceIdentifier: options.instanceIdentifier, securityGroups: securityGroups, instanceResourceId: instance.DbiResourceId, }); } /** * Import an existing database instance. */ public static fromDatabaseInstanceAttributes(scope: Construct, id: string, attrs: DatabaseInstanceAttributes): IDatabaseInstance { class Import extends DatabaseInstanceBase implements IDatabaseInstance { public readonly defaultPort = ec2.Port.tcp(attrs.port); public readonly connections = new ec2.Connections({ securityGroups: attrs.securityGroups, defaultPort: this.defaultPort, }); public readonly instanceIdentifier = attrs.instanceIdentifier; public readonly dbInstanceEndpointAddress = attrs.instanceEndpointAddress; public readonly dbInstanceEndpointPort = Tokenization.stringifyNumber(attrs.port); public readonly instanceEndpoint = new Endpoint(attrs.instanceEndpointAddress, attrs.port); public readonly engine = attrs.engine; protected enableIamAuthentication = true; public readonly instanceResourceId = attrs.instanceResourceId; } return new Import(scope, id); } public abstract readonly instanceIdentifier: string; public abstract readonly dbInstanceEndpointAddress: string; public abstract readonly dbInstanceEndpointPort: string; public abstract readonly instanceResourceId?: string; public abstract readonly instanceEndpoint: Endpoint; // only required because of JSII bug: https://github.com/aws/jsii/issues/2040 public abstract readonly engine?: IInstanceEngine; protected abstract enableIamAuthentication?: boolean; /** * Access to network connections. */ public abstract readonly connections: ec2.Connections; /** * Add a new db proxy to this instance. */ public addProxy(id: string, options: DatabaseProxyOptions): DatabaseProxy { return new DatabaseProxy(this, id, { proxyTarget: ProxyTarget.fromInstance(this), ...options, }); } public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant { if (this.enableIamAuthentication === false) { throw new ValidationError('Cannot grant connect when IAM authentication is disabled', this); } if (!this.instanceResourceId) { throw new ValidationError('For imported Database Instances, instanceResourceId is required to grantConnect()', this); } if (!dbUser) { throw new ValidationError('For imported Database Instances, the dbUser is required to grantConnect()', this); } this.enableIamAuthentication = true; return iam.Grant.addToPrincipal({ grantee, actions: ['rds-db:connect'], resourceArns: [ // The ARN of an IAM policy for IAM database access is not the same as the instance ARN, so we cannot use `this.instanceArn`. // See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html Stack.of(this).formatArn({ arnFormat: ArnFormat.COLON_RESOURCE_NAME, service: 'rds-db', resource: 'dbuser', resourceName: [this.instanceResourceId, dbUser].join('/'), }), ], }); } /** * 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 { const commonAnComponents: ArnComponents = { service: 'rds', resource: 'db', arnFormat: ArnFormat.COLON_RESOURCE_NAME, }; const localArn = Stack.of(this).formatArn({ ...commonAnComponents, resourceName: this.instanceIdentifier, }); return this.getResourceArnAttribute(localArn, { ...commonAnComponents, resourceName: this.physicalName, }); } /** * Renders the secret attachment target specifications. */ public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps { return { targetId: this.instanceIdentifier, targetType: secretsmanager.AttachmentTargetType.RDS_DB_INSTANCE, }; } } /** * The license model. */ export enum LicenseModel { /** * License included. */ LICENSE_INCLUDED = 'license-included', /** * Bring your own license. */ BRING_YOUR_OWN_LICENSE = 'bring-your-own-license', /** * General public license. */ GENERAL_PUBLIC_LICENSE = 'general-public-license', } /** * The processor features. */ export interface ProcessorFeatures { /** * The number of CPU core. * * @default - the default number of CPU cores for the chosen instance class. */ readonly coreCount?: number; /** * The number of threads per core. * * @default - the default number of threads per core for the chosen instance class. */ readonly threadsPerCore?: number; } /** * The type of storage. * * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html */ export enum StorageType { /** * Standard. * * Amazon RDS supports magnetic storage for backward compatibility. It is recommended to use * General Purpose SSD or Provisioned IOPS SSD for any new storage needs. * * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#CHAP_Storage.Magnetic */ STANDARD = 'standard', /** * General purpose SSD (gp2). * * Baseline performance determined by volume size * * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#Concepts.Storage.GeneralSSD */ GP2 = 'gp2', /** * General purpose SSD (gp3). * * Performance scales independently from storage * * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#Concepts.Storage.GeneralSSD */ GP3 = 'gp3', /** * Provisioned IOPS SSD (io1). * * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#USER_PIOPS */ IO1 = 'io1', /** * Provisioned IOPS SSD (io2). * * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#USER_PIOPS */ IO2 = 'io2', } /** * The network type of the DB instance. */ export enum NetworkType { /** * IPv4 only network type. */ IPV4 = 'IPV4', /** * Dual-stack network type. */ DUAL = 'DUAL', } /** * Construction properties for a DatabaseInstanceNew */ export interface DatabaseInstanceNewProps { /** * 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. Storage types supported are gp2, io1, standard. * * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#Concepts.Storage.GeneralSSD * * @default GP2 */ readonly storageType?: StorageType; /** * The storage throughput, specified in mebibytes per second (MiBps). * * Only applicable for GP3. * * @see https://docs.aws.amazon.com//AmazonRDS/latest/UserGuide/CHAP_Storage.html#gp3-storage * * @default - 125 MiBps if allocated storage is less than 400 GiB for MariaDB, MySQL, and PostgreSQL, * less than 200 GiB for Oracle and less than 20 GiB for SQL Server. 500 MiBps otherwise (except for * SQL Server where the default is always 125 MiBps). */ readonly storageThroughput?: number; /** * 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 if storage type is not specified. For GP3: 3,000 IOPS if allocated * storage is less than 400 GiB for MariaDB, MySQL, and PostgreSQL, less than 200 GiB for Oracle and * less than 20 GiB for SQL Server. 12,000 IOPS otherwise (except for SQL Server where the default is * always 3,000 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. * * @deprecated use `vpcSubnets` * @default - private subnets */ readonly vpcPlacement?: ec2.SubnetSelection; /** * The type of subnets to add to the created DB subnet group. * * @default - private subnets */ readonly vpcSubnets?: ec2.SubnetSelection; /** * The security groups to assign to the DB instance. * * @default - a new security group is created */ readonly securityGroups?: ec2.ISecurityGroup[]; /** * The port for the instance. * * @default - the default port for the chosen engine. */ readonly port?: number; /** * The DB parameter group to associate with the instance. * * @default - no parameter group */ readonly parameterGroup?: IParameterGroup; /** * 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. * When creating a read replica, you must enable automatic backups on the source * database instance by setting the backup retention to a value other than zero. * * @default - Duration.days(1) for source instances, disabled for read replicas */ readonly backupRetention?: Duration; /** * 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_WorkingWithAutomatedBackups.html#USER_WorkingWithAutomatedBackups.BackupWindow */ 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 true */ 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?: Duration; /** * Role that will be used to manage DB instance monitoring. * * @default - A role is automatically created for you */ readonly monitoringRole?: iam.IRole; /** * Whether to enable Performance Insights for the DB instance. * * @default - false, unless ``performanceInsightRetention`` or ``performanceInsightEncryptionKey`` is set. */ readonly enablePerformanceInsights?: boolean; /** * The amount of time, in days, to retain Performance Insights data. * * @default 7 this is the free tier */ readonly performanceInsightRetention?: PerformanceInsightRetention; /** * The AWS KMS key for encryption of Performance Insights data. * * @default - default master key */ readonly performanceInsightEncryptionKey?: 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; /** * The IAM role for the Lambda function associated with the custom resource * that sets the retention policy. * * @default - a new role is created. */ readonly cloudwatchLogsRetentionRole?: iam.IRole; /** * Indicates that minor engine upgrades are applied automatically to the * DB instance during the maintenance window. * * @default true */ readonly autoMinorVersionUpgrade?: boolean; /** * 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#Concepts.DBMaintenance */ readonly preferredMaintenanceWindow?: string; /** * Indicates whether the DB instance should have deletion protection enabled. * * @default - true if ``removalPolicy`` is RETAIN, false otherwise */ readonly deletionProtection?: boolean; /** * The CloudFormation policy to apply when the instance is removed from the * stack or replaced during an update. * * @default - RemovalPolicy.SNAPSHOT (remove the resource, but retain a snapshot of the data) */ readonly removalPolicy?: RemovalPolicy; /** * Upper limit to which RDS can scale the storage in GiB(Gibibyte). * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PIOPS.StorageTypes.html#USER_PIOPS.Autoscaling * @default - No autoscaling of RDS instance */ readonly maxAllocatedStorage?: number; /** * The Active Directory directory ID to create the DB instance in. * * @default - Do not join domain */ readonly domain?: string; /** * The IAM role to be used when making API calls to the Directory Service. The role needs the AWS-managed policy * AmazonRDSDirectoryServiceAccess or equivalent. * * @default - The role will be created for you if `DatabaseInstanceNewProps#domain` is specified */ readonly domainRole?: iam.IRole; /** * Existing subnet group for the instance. * * @default - a new subnet group will be created. */ readonly subnetGroup?: ISubnetGroup; /** * Role that will be associated with this DB instance to enable S3 import. * This feature is only supported by the Microsoft SQL Server, Oracle, and PostgreSQL engines. * * This property must not be used if `s3ImportBuckets` is used. * * For Microsoft SQL Server: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html * For Oracle: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html * For PostgreSQL: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Procedural.Importing.html * * @default - New role is created if `s3ImportBuckets` is set, no role is defined otherwise */ readonly s3ImportRole?: iam.IRole; /** * S3 buckets that you want to load data from. * This feature is only supported by the Microsoft SQL Server, Oracle, and PostgreSQL engines. * * This property must not be used if `s3ImportRole` is used. * * For Microsoft SQL Server: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html * For Oracle: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html * For PostgreSQL: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Procedural.Importing.html * * @default - None */ readonly s3ImportBuckets?: s3.IBucket[]; /** * Role that will be associated with this DB instance to enable S3 export. * * This property must not be used if `s3ExportBuckets` is used. * * For Microsoft SQL Server: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html * For Oracle: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html * * @default - New role is created if `s3ExportBuckets` is set, no role is defined otherwise */ readonly s3ExportRole?: iam.IRole; /** * S3 buckets that you want to load data into. * * This property must not be used if `s3ExportRole` is used. * * For Microsoft SQL Server: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html * For Oracle: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html * * @default - None */ readonly s3ExportBuckets?: s3.IBucket[]; /** * Indicates whether the DB instance is an internet-facing instance. If not specified, * the instance's vpcSubnets will be used to determine if the instance is internet-facing * or not. * * @default - `true` if the instance's `vpcSubnets` is `subnetType: SubnetType.PUBLIC`, `false` otherwise */ readonly publiclyAccessible?: boolean; /** * The network type of the DB instance. * * @default - IPV4 */ readonly networkType?: NetworkType; /** * The identifier of the CA certificate for this DB instance. * * Specifying or updating this property triggers a reboot. * * For RDS DB engines: * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL-certificate-rotation.html * For Aurora DB engines: * @see https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.SSL-certificate-rotation.html * * @default - RDS will choose a certificate authority */ readonly caCertificate?: CaCertificate; /** * Specifies whether changes to the DB instance and any pending modifications are applied immediately, regardless of the `preferredMaintenanceWindow` setting. * If set to `false`, changes are applied during the next maintenance window. * * Until RDS applies the changes, the DB instance remains in a drift state. * As a result, the configuration doesn't fully reflect the requested modifications and temporarily diverges from the intended state. * * This property also determines whether the DB instance reboots when a static parameter is modified in the associated DB parameter group. * @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html * * @default - Changes will be applied immediately */ readonly applyImmediately?: boolean; } /** * A new database instance. */ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IDatabaseInstance { /** * The VPC where this database instance is deployed. */ public readonly vpc: ec2.IVpc; public readonly connections: ec2.Connections; /** * The log group is created when `cloudwatchLogsExports` is set. * * Each export value will create a separate log group. */ public readonly cloudwatchLogGroups: {[engine: string]: logs.ILogGroup}; protected abstract readonly instanceType: ec2.InstanceType; protected readonly vpcPlacement?: ec2.SubnetSelection; protected readonly newCfnProps: CfnDBInstanceProps; private readonly cloudwatchLogsExports?: string[]; private readonly cloudwatchLogsRetention?: logs.RetentionDays; private readonly cloudwatchLogsRetentionRole?: iam.IRole; private readonly domainId?: string; private readonly domainRole?: iam.IRole; protected enableIamAuthentication?: boolean; constructor(scope: Construct, id: string, props: DatabaseInstanceNewProps) { // RDS always lower-cases the ID of the database, so use that for the physical name // (which is the name used for cross-environment access, so it needs to be correct, // regardless of the feature flag that changes it in the template for the L1) const instancePhysicalName = Token.isUnresolved(props.instanceIdentifier) ? props.instanceIdentifier : props.instanceIdentifier?.toLowerCase(); super(scope, id, { physicalName: instancePhysicalName, }); this.vpc = props.vpc; if (props.vpcSubnets && props.vpcPlacement) { throw new ValidationError('Only one of `vpcSubnets` or `vpcPlacement` can be specified', this); } this.vpcPlacement = props.vpcSubnets ?? props.vpcPlacement; if (props.multiAz === true && props.availabilityZone) { throw new ValidationError('Requesting a specific availability zone is not valid for Multi-AZ instances', this); } const subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'SubnetGroup', { description: `Subnet group for ${this.node.id} database`, vpc: this.vpc, vpcSubnets: this.vpcPlacement, removalPolicy: renderUnless(helperRemovalPolicy(props.removalPolicy), RemovalPolicy.DESTROY), }); const securityGroups = props.securityGroups || [new ec2.SecurityGroup(this, 'SecurityGroup', { description: `Security group for ${this.node.id} database`, vpc: props.vpc, })]; this.connections = new ec2.Connections({ securityGroups, defaultPort: ec2.Port.tcp(Lazy.number({ produce: () => this.instanceEndpoint.port })), }); let monitoringRole; if (props.monitoringInterval && props.monitoringInterval.toSeconds()) { monitoringRole = props.monitoringRole || new iam.Role(this, 'MonitoringRole', { assumedBy: new iam.ServicePrincipal('monitoring.rds.amazonaws.com'), managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSEnhancedMonitoringRole')], }); } const storageType = props.storageType ?? StorageType.GP2; const iops = defaultIops(storageType, props.iops); if (props.storageThroughput && storageType !== StorageType.GP3) { throw new ValidationError(`The storage throughput can only be specified with GP3 storage type. Got ${storageType}.`, this); } if (storageType === StorageType.GP3 && props.storageThroughput && iops && !Token.isUnresolved(props.storageThroughput) && !Token.isUnresolved(iops) && props.storageThroughput/iops > 0.25) { throw new ValidationError(`The maximum ratio of storage throughput to IOPS is 0.25. Got ${props.storageThroughput/iops}.`, this); } this.cloudwatchLogGroups = {}; this.cloudwatchLogsExports = props.cloudwatchLogsExports; this.cloudwatchLogsRetention = props.cloudwatchLogsRetention; this.cloudwatchLogsRetentionRole = props.cloudwatchLogsRetentionRole; this.enableIamAuthentication = props.iamAuthentication; const enablePerformanceInsights = props.enablePerformanceInsights || props.performanceInsightRetention !== undefined || props.performanceInsightEncryptionKey !== undefined; if (enablePerformanceInsights && props.enablePerformanceInsights === false) { throw new ValidationError('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set', this); } if (props.domain) { this.domainId = props.domain; this.domainRole = props.domainRole || new iam.Role(this, 'RDSDirectoryServiceRole', { assumedBy: new iam.CompositePrincipal( new iam.ServicePrincipal('rds.amazonaws.com'), new iam.ServicePrincipal('directoryservice.rds.amazonaws.com'), ), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSDirectoryServiceAccess'), ], }); } const maybeLowercasedInstanceId = FeatureFlags.of(this).isEnabled(cxapi.RDS_LOWERCASE_DB_IDENTIFIER) && !Token.isUnresolved(props.instanceIdentifier) ? props.instanceIdentifier?.toLowerCase() : props.instanceIdentifier; const instanceParameterGroupConfig = props.parameterGroup?.bindToInstance({}); const isInPublicSubnet = this.vpcPlacement && this.vpcPlacement.subnetType === ec2.SubnetType.PUBLIC; this.newCfnProps = { autoMinorVersionUpgrade: props.autoMinorVersionUpgrade, availabilityZone: props.multiAz ? undefined : props.availabilityZone, backupRetentionPeriod: props.backupRetention?.toDays(), copyTagsToSnapshot: props.copyTagsToSnapshot ?? true, dbInstanceClass: Lazy.string({ produce: () => `db.${this.instanceType}` }), dbInstanceIdentifier: Token.isUnresolved(props.instanceIdentifier) // if the passed identifier is a Token, // we need to use the physicalName of the database // (we cannot change its case anyway), // as it might be used in a cross-environment fashion ? this.physicalName : maybeLowercasedInstanceId, dbSubnetGroupName: subnetGroup.subnetGroupName, deleteAutomatedBackups: props.deleteAutomatedBackups, deletionProtection: defaultDeletionProtection(props.deletionProtection, props.removalPolicy), enableCloudwatchLogsExports: this.cloudwatchLogsExports, enableIamDatabaseAuthentication: Lazy.any({ produce: () => this.enableIamAuthentication }), enablePerformanceInsights: enablePerformanceInsights || props.enablePerformanceInsights, // fall back to undefined if not set, iops, monitoringInterval: props.monitoringInterval?.toSeconds(), monitoringRoleArn: monitoringRole?.roleArn, multiAz: props.multiAz, dbParameterGroupName: instanceParameterGroupConfig?.parameterGroupName, optionGroupName: props.optionGroup?.optionGroupName, performanceInsightsKmsKeyId: props.performanceInsightEncryptionKey?.keyArn, performanceInsightsRetentionPeriod: enablePerformanceInsights ? (props.performanceInsightRetention || PerformanceInsightRetention.DEFAULT) : undefined, port: props.port !== undefined ? Tokenization.stringifyNumber(props.port) : undefined, preferredBackupWindow: props.preferredBackupWindow, preferredMaintenanceWindow: props.preferredMaintenanceWindow, processorFeatures: props.processorFeatures && renderProcessorFeatures(props.processorFeatures), publiclyAccessible: props.publiclyAccessible ?? isInPublicSubnet, storageType, storageThroughput: props.storageThroughput, vpcSecurityGroups: securityGroups.map(s => s.securityGroupId), maxAllocatedStorage: props.maxAllocatedStorage, domain: this.domainId, domainIamRoleName: this.domainRole?.roleName, networkType: props.networkType, caCertificateIdentifier: props.caCertificate ? props.caCertificate.toString() : undefined, applyImmediately: props.applyImmediately, }; } protected setLogRetention() { if (this.cloudwatchLogsExports && this.cloudwatchLogsRetention) { for (const log of this.cloudwatchLogsExports) { const logGroupName = `/aws/rds/instance/${this.instanceIdentifier}/${log}`; new logs.LogRetention(this, `LogRetention${log}`, { logGroupName, retention: this.cloudwatchLogsRetention, role: this.cloudwatchLogsRetentionRole, }); this.cloudwatchLogGroups[log] = logs.LogGroup.fromLogGroupName(this, `LogGroup${this.instanceIdentifier}${log}`, logGroupName); } } } } /** * Construction properties for a DatabaseInstanceSource */ export interface DatabaseInstanceSourceProps extends DatabaseInstanceNewProps { /** * The database engine. */ readonly engine: IInstanceEngine; /** * The name of the compute and memory capacity for the instance. * * @default - m5.large (or, more specifically, db.m5.large) */ readonly instanceType?: ec2.InstanceType; /** * The license model. * * @default - RDS default license model */ readonly licenseModel?: LicenseModel; /** * Whether to allow major version upgrades. * * @default false */ readonly allowMajorVersionUpgrade?: boolean; /** * The time zone of the instance. This is currently supported only by Microsoft Sql Server. * * @default - RDS default timezone */ readonly timezone?: string; /** * The allocated storage size, specified in gibibytes (GiB). * * @default 100 */ readonly allocatedStorage?: number; /** * The name of the database. * * @default - no name */ readonly databaseName?: string; /** * The parameters in the DBParameterGroup to create automatically * * You can only specify parameterGroup or parameters but not both. * You need to use a versioned engine to auto-generate a DBParameterGroup. * * @default - None */ readonly parameters?: { [key: string]: string }; } /** * A new source database instance (not a read replica) */ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDatabaseInstance { public readonly engine?: IInstanceEngine; /** * The AWS Secrets Manager secret attached to the instance. */ public abstract readonly secret?: secretsmanager.ISecret; protected readonly sourceCfnProps: CfnDBInstanceProps; protected readonly instanceType: ec2.InstanceType; private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication; private readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication; constructor(scope: Construct, id: string, props: DatabaseInstanceSourceProps) { super(scope, id, props); this.singleUserRotationApplication = props.engine.singleUserRotationApplication; this.multiUserRotationApplication = props.engine.multiUserRotationApplication; this.engine = props.engine; const engineType = props.engine.engineType; // only Oracle and SQL Server require the import and export Roles to be the same const combineRoles = engineType.startsWith('oracle-') || engineType.startsWith('sqlserver-'); let { s3ImportRole, s3ExportRole } = setupS3ImportExport(this, props, combineRoles); const engineConfig = props.engine.bindToInstance(this, { ...props, s3ImportRole, s3ExportRole, }); const instanceAssociatedRoles: CfnDBInstance.DBInstanceRoleProperty[] = []; const engineFeatures = engineConfig.features; if (s3ImportRole) { if (!engineFeatures?.s3Import) { throw new ValidationError(`Engine '${engineDescription(props.engine)}' does not support S3 import`, this); } instanceAssociatedRoles.push({ roleArn: s3ImportRole.roleArn, featureName: engineFeatures?.s3Import }); } if (s3ExportRole) { if (!engineFeatures?.s3Export) { throw new ValidationError(`Engine '${engineDescription(props.engine)}' does not support S3 export`, this); } // only add the export feature if it's different from the import feature if (engineFeatures.s3Import !== engineFeatures?.s3Export) { instanceAssociatedRoles.push({ roleArn: s3ExportRole.roleArn, featureName: engineFeatures?.s3Export }); } } this.instanceType = props.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE); if (props.parameterGroup && props.parameters) { throw new ValidationError('You cannot specify both parameterGroup and parameters', this); } const dbParameterGroupName = props.parameters ? new ParameterGroup(this, 'ParameterGroup', { engine: props.engine, parameters: props.parameters, }).bindToInstance({}).parameterGroupName : this.newCfnProps.dbParameterGroupName; this.sourceCfnProps = { ...this.newCfnProps, associatedRoles: instanceAssociatedRoles.length > 0 ? instanceAssociatedRoles : undefined, optionGroupName: engineConfig.optionGroup?.optionGroupName, allocatedStorage: props.allocatedStorage?.toString() ?? '100', allowMajorVersionUpgrade: props.allowMajorVersionUpgrade, dbName: props.databaseName, engine: engineType, engineVersion: props.engine.engineVersion?.fullVersion, licenseModel: props.licenseModel, timezone: props.timezone, dbParameterGroupName, }; } /** * Adds the single user rotation of the master password to this instance. * * @param options the options for the rotation, * if you want to override the defaults */ public addRotationSingleUser(options: RotationSingleUserOptions = {}): secretsmanager.SecretRotation { if (!this.secret) { throw new ValidationError('Cannot add single user rotation for an instance without secret.', this); } const id = 'RotationSingleUser'; const existing = this.node.tryFindChild(id); if (existing) { throw new ValidationError('A single user rotation was already added to this instance.', this); } return new secretsmanager.SecretRotation(this, id, { ...applyDefaultRotationOptions(options, this.vpcPlacement), secret: this.secret, application: this.singleUserRotationApplication, vpc: this.vpc, target: this, }); } /** * Adds the multi user rotation to this instance. */ public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation { if (!this.secret) { throw new ValidationError('Cannot add multi user rotation for an instance without secret.', this); } return new secretsmanager.SecretRotation(this, id, { ...applyDefaultRotationOptions(options, this.vpcPlacement), secret: options.secret, masterSecret: this.secret, application: this.multiUserRotationApplication, vpc: this.vpc, target: this, }); } /** * Grant the given identity connection access to the database. * * @param grantee the Principal to grant the permissions to * @param dbUser the name of the database user to allow connecting as to the db instance, * or the default database user, obtained from the Secret, if not specified */ public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant { if (!dbUser) { if (!this.secret) { throw new ValidationError('A secret or dbUser is required to grantConnect()', this); } dbUser = this.secret.secretValueFromJson('username').unsafeUnwrap(); } return super.grantConnect(grantee, dbUser); } } /** * Properties for looking up an existing DatabaseInstance. */ export interface DatabaseInstanceLookupOptions { /** * The instance identifier of the DatabaseInstance */ readonly instanceIdentifier: string; } /** * Construction properties for a DatabaseInstance. */ export interface DatabaseInstanceProps extends DatabaseInstanceSourceProps { /** * Credentials for the administrative user * * @default - A username of 'admin' (or 'postgres' for PostgreSQL) and SecretsManager-generated password */ readonly credentials?: Credentials; /** * 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 - true if storageEncryptionKey has been provided, false otherwise */ readonly storageEncrypted?: boolean; /** * The KMS key that's used to encrypt the DB instance. * * @default - default master key if storageEncrypted is true, no key otherwise */ readonly storageEncryptionKey?: 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 instanceResourceId?: string; public readonly instanceEndpoint: Endpoint; public readonly secret?: secretsmanager.ISecret; constructor(scope: Construct, id: string, props: DatabaseInstanceProps) { super(scope, id, props); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); const credentials = renderCredentials(this, props.engine, props.credentials); const secret = credentials.secret; const instance = new CfnDBInstance(this, 'Resource', { ...this.sourceCfnProps, characterSetName: props.characterSetName, kmsKeyId: props.storageEncryptionKey && props.storageEncryptionKey.keyArn, masterUsername: credentials.username, masterUserPassword: credentials.password?.unsafeUnwrap(), storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, }); this.instanceIdentifier = this.getResourceNameAttribute(instance.ref); this.dbInstanceEndpointAddress = instance.attrEndpointAddress; this.dbInstanceEndpointPort = instance.attrEndpointPort; this.instanceResourceId = instance.attrDbiResourceId; // create a number token that represents the port of the instance const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); instance.applyRemovalPolicy(props.removalPolicy ?? RemovalPolicy.SNAPSHOT); if (secret) { this.secret = secret.attach(this); } 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; /** * Master user credentials. * * Note - It is not possible to change the master username for a snapshot; * however, it is possible to provide (or generate) a new password. * * @default - The existing username and password from the snapshot will be used. */ readonly credentials?: SnapshotCredentials; } /** * 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 instanceResourceId?: string; public readonly instanceEndpoint: Endpoint; public readonly secret?: secretsmanager.ISecret; constructor(scope: Construct, id: string, props: DatabaseInstanceFromSnapshotProps) { super(scope, id, props); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); let credentials = props.credentials; let secret = credentials?.secret; if (!secret && credentials?.generatePassword) { if (!credentials.username) { throw new ValidationError('`credentials` `username` must be specified when `generatePassword` is set to true', this); } secret = new DatabaseSecret(this, 'Secret', { username: credentials.username, encryptionKey: credentials.encryptionKey, excludeCharacters: credentials.excludeCharacters, replaceOnPasswordCriteriaChanges: credentials.replaceOnPasswordCriteriaChanges, replicaRegions: credentials.replicaRegions, }); } const instance = new CfnDBInstance(this, 'Resource', { ...this.sourceCfnProps, dbSnapshotIdentifier: props.snapshotIdentifier, masterUserPassword: secret?.secretValueFromJson('password')?.unsafeUnwrap() ?? credentials?.password?.unsafeUnwrap(), // Safe usage }); this.instanceIdentifier = instance.ref; this.dbInstanceEndpointAddress = instance.attrEndpointAddress; this.dbInstanceEndpointPort = instance.attrEndpointPort; this.instanceResourceId = instance.attrDbiResourceId; // create a number token that represents the port of the instance const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); instance.applyRemovalPolicy(props.removalPolicy ?? RemovalPolicy.SNAPSHOT); if (secret) { this.secret = secret.attach(this); } this.setLogRetention(); } } /** * Construction properties for a DatabaseInstanceReadReplica. */ export interface DatabaseInstanceReadReplicaProps extends DatabaseInstanceNewProps { /** * The name of the compute and memory capacity classes. */ readonly instanceType: ec2.InstanceType; /** * 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 - true if storageEncryptionKey has been provided, false otherwise */ readonly storageEncrypted?: boolean; /** * The KMS key that's used to encrypt the DB instance. * * @default - default master key if storageEncrypted is true, no key otherwise */ readonly storageEncryptionKey?: kms.IKey; /** * The allocated storage size, specified in gibibytes (GiB). * * @default - The replica will inherit the allocated storage of the source database instance */ readonly allocatedStorage?: number; } /** * 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; /** * The AWS Region-unique, immutable identifier for the DB instance. * This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB instance is accessed. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#aws-resource-rds-dbinstance-return-values */ public readonly instanceResourceId?: string; public readonly instanceEndpoint: Endpoint; public readonly engine?: IInstanceEngine = undefined; protected readonly instanceType: ec2.InstanceType; constructor(scope: Construct, id: string, props: DatabaseInstanceReadReplicaProps) { super(scope, id, props); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); if (props.sourceDatabaseInstance.engine && !props.sourceDatabaseInstance.engine.supportsReadReplicaBackups && props.backupRetention) { throw new ValidationError(`Cannot set 'backupRetention', as engine '${engineDescription(props.sourceDatabaseInstance.engine)}' does not support automatic backups for read replicas`, this); } // The read replica instance always uses the same engine as the source instance // but some CF validations require the engine to be explicitly passed when some // properties are specified. const shouldPassEngine = props.domain != null; const instance = new CfnDBInstance(this, 'Resource', { ...this.newCfnProps, // this must be ARN, not ID, because of https://github.com/terraform-providers/terraform-provider-aws/issues/528#issuecomment-391169012 sourceDbInstanceIdentifier: props.sourceDatabaseInstance.instanceArn, kmsKeyId: props.storageEncryptionKey?.keyArn, storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, engine: shouldPassEngine ? props.sourceDatabaseInstance.engine?.engineType : undefined, allocatedStorage: props.allocatedStorage?.toString(), }); this.instanceType = props.instanceType; this.instanceIdentifier = instance.ref; this.dbInstanceEndpointAddress = instance.attrEndpointAddress; this.dbInstanceEndpointPort = instance.attrEndpointPort; this.instanceResourceId = FeatureFlags.of(this).isEnabled(cxapi.USE_CORRECT_VALUE_FOR_INSTANCE_RESOURCE_ID_PROPERTY) ? instance.attrDbiResourceId : instance.attrDbInstanceArn; // create a number token that represents the port of the instance const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); instance.applyRemovalPolicy(props.removalPolicy ?? RemovalPolicy.SNAPSHOT); 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; } function defaultIops(storageType: StorageType, iops?: number): number | undefined { switch (storageType) { case StorageType.STANDARD: case StorageType.GP2: return undefined; case StorageType.GP3: return iops; case StorageType.IO1: case StorageType.IO2: return iops ?? 1000; } }