Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cdk from 'aws-cdk-lib';
import * as constructs from 'constructs';
import { DatabaseCluster } from 'aws-cdk-lib/aws-docdb';
import { IntegTest } from '@aws-cdk/integ-tests-alpha';

class TestStack extends cdk.Stack {
constructor(scope: constructs.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2, restrictDefaultSecurityGroup: false });

new DatabaseCluster(this, 'Database', {
masterUser: {
username: 'docdb',
},
vpc,
serverlessV2ScalingConfiguration: {
minCapacity: 0.5,
maxCapacity: 2,
},
engineVersion: '5.0.0',
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}

const app = new cdk.App();

const stack = new TestStack(app, 'aws-cdk-docdb-cluster-serverless');

new IntegTest(app, 'aws-cdk-docdb-cluster-serverless-integ', {
testCases: [stack],
});
119 changes: 82 additions & 37 deletions packages/aws-cdk-lib/aws-docdb/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-re
import { propertyInjectable } from '../../core/lib/prop-injectable';

const MIN_ENGINE_VERSION_FOR_IO_OPTIMIZED_STORAGE = 5;
const MIN_ENGINE_VERSION_FOR_SERVERLESS = 5;

/**
* ServerlessV2 scaling configuration for DocumentDB clusters
*/
export interface ServerlessV2ScalingConfiguration {
/**
* The minimum number of DocumentDB capacity units (DCUs) for a DocumentDB instance in a DocumentDB Serverless cluster.
*/
readonly minCapacity: number;

/**
* The maximum number of DocumentDB capacity units (DCUs) for a DocumentDB instance in a DocumentDB Serverless cluster.
*/
readonly maxCapacity: number;
}

/**
* The storage type of the DocDB cluster
Expand Down Expand Up @@ -79,13 +95,6 @@ export interface DatabaseClusterProps {
*/
readonly storageEncrypted?: boolean;

/**
* Number of DocDB compute instances
*
* @default 1
*/
readonly instances?: number;

/**
* An optional identifier for the cluster
*
Expand All @@ -97,16 +106,34 @@ export interface DatabaseClusterProps {
* Base identifier for instances
*
* Every replica is named by appending the replica number to this string, 1-based.
* Only applicable for provisioned clusters.
*
* @default - `dbClusterName` is used with the word "Instance" appended. If `dbClusterName` is not provided, the
* identifier is automatically generated.
*/
readonly instanceIdentifierBase?: string;

/**
* What type of instance to start for the replicas
* What type of instance to start for the replicas.
* Required for provisioned clusters, not applicable for serverless clusters.
*
* @default None
*/
readonly instanceType?: ec2.InstanceType;

/**
* Number of DocDB compute instances
* @default 1
*/
readonly instances?: number;

/**
* ServerlessV2 scaling configuration.
* When specified, the cluster will be created as a serverless cluster.
*
* @default None
*/
readonly instanceType: ec2.InstanceType;
readonly serverlessV2ScalingConfiguration?: ServerlessV2ScalingConfiguration;

/**
* The identifier of the CA certificate used for the instances.
Expand Down Expand Up @@ -476,6 +503,15 @@ export class DatabaseCluster extends DatabaseClusterBase {
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);

// Determine if this is a serverless cluster
const isServerless = !!props.serverlessV2ScalingConfiguration;
if (isServerless && props.instanceType) {
throw new ValidationError('Cannot specify instanceType for serverless clusters', this);
}
if (!isServerless && !props.instanceType) {
throw new ValidationError('instanceType is required for provisioned clusters', this);
}

this.vpc = props.vpc;
this.vpcSubnets = props.vpcSubnets;

Expand Down Expand Up @@ -552,6 +588,11 @@ export class DatabaseCluster extends DatabaseClusterBase {
throw new ValidationError(`I/O-optimized storage is supported starting with engine version 5.0.0, got '${props.engineVersion}'`, this);
}

// Validate engine version for serverless clusters: https://docs.aws.amazon.com/documentdb/latest/developerguide/docdb-serverless-limitations.html
if (isServerless && props.engineVersion !== undefined && Number(props.engineVersion.split('.')[0]) < MIN_ENGINE_VERSION_FOR_SERVERLESS) {
throw new ValidationError(`DocumentDB serverless requires engine version 5.0.0 or higher, got '${props.engineVersion}'`, this);
}

// Create the DocDB cluster
this.cluster = new CfnDBCluster(this, 'Resource', {
// Basic
Expand Down Expand Up @@ -579,6 +620,8 @@ export class DatabaseCluster extends DatabaseClusterBase {
// Tags
copyTagsToSnapshot: props.copyTagsToSnapshot,
storageType: props.storageType,
// Serverless configuration
serverlessV2ScalingConfiguration: props.serverlessV2ScalingConfiguration,
});

this.cluster.applyRemovalPolicy(props.removalPolicy, {
Expand All @@ -598,41 +641,43 @@ export class DatabaseCluster extends DatabaseClusterBase {
this.secret = secret.attach(this);
}

// Create the instances
const instanceCount = props.instances ?? DatabaseCluster.DEFAULT_NUM_INSTANCES;
if (instanceCount < 1) {
throw new ValidationError('At least one instance is required', this);
}
// Create instances only for provisioned clusters
if (!isServerless) {
const instanceCount = props.instances ?? DatabaseCluster.DEFAULT_NUM_INSTANCES;
if (instanceCount < 1) {
throw new ValidationError('At least one instance is required for provisioned clusters', this);
}

const instanceRemovalPolicy = this.getInstanceRemovalPolicy(props);
const caCertificateIdentifier = props.caCertificate ? props.caCertificate.toString() : undefined;
const instanceRemovalPolicy = this.getInstanceRemovalPolicy(props);
const caCertificateIdentifier = props.caCertificate ? props.caCertificate.toString() : undefined;

for (let i = 0; i < instanceCount; i++) {
const instanceIndex = i + 1;
for (let i = 0; i < instanceCount; i++) {
const instanceIndex = i + 1;

const instanceIdentifier = props.instanceIdentifierBase != null ? `${props.instanceIdentifierBase}${instanceIndex}`
: props.dbClusterName != null ? `${props.dbClusterName}instance${instanceIndex}` : undefined;
const instanceIdentifier = props.instanceIdentifierBase != null ? `${props.instanceIdentifierBase}${instanceIndex}`
: props.dbClusterName != null ? `${props.dbClusterName}instance${instanceIndex}` : undefined;

const instance = new CfnDBInstance(this, `Instance${instanceIndex}`, {
// Link to cluster
dbClusterIdentifier: this.cluster.ref,
dbInstanceIdentifier: instanceIdentifier,
// Instance properties
dbInstanceClass: databaseInstanceType(props.instanceType),
enablePerformanceInsights: props.enablePerformanceInsights,
caCertificateIdentifier: caCertificateIdentifier,
});
const instance = new CfnDBInstance(this, `Instance${instanceIndex}`, {
// Link to cluster
dbClusterIdentifier: this.cluster.ref,
dbInstanceIdentifier: instanceIdentifier,
// Instance properties
dbInstanceClass: databaseInstanceType(props.instanceType!),
enablePerformanceInsights: props.enablePerformanceInsights,
caCertificateIdentifier: caCertificateIdentifier,
});

instance.applyRemovalPolicy(instanceRemovalPolicy, {
applyToUpdateReplacePolicy: true,
});
instance.applyRemovalPolicy(instanceRemovalPolicy, {
applyToUpdateReplacePolicy: true,
});

// We must have a dependency on the NAT gateway provider here to create
// things in the right order.
instance.node.addDependency(internetConnectivityEstablished);
// We must have a dependency on the NAT gateway provider here to create
// things in the right order.
instance.node.addDependency(internetConnectivityEstablished);

this.instanceIdentifiers.push(instance.ref);
this.instanceEndpoints.push(new Endpoint(instance.attrEndpoint, port));
this.instanceIdentifiers.push(instance.ref);
this.instanceEndpoints.push(new Endpoint(instance.attrEndpoint, port));
}
}

this.connections = new ec2.Connections({
Expand Down
171 changes: 171 additions & 0 deletions packages/aws-cdk-lib/aws-docdb/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,177 @@ describe('DatabaseCluster', () => {
}).toThrow("I/O-optimized storage is supported starting with engine version 5.0.0, got '3.6.0'");
});
});

describe('serverless clusters', () => {
test('can create a serverless cluster', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
new DatabaseCluster(stack, 'Database', {
masterUser: {
username: 'admin',
password: cdk.SecretValue.unsafePlainText('tooshort'),
},
vpc,
serverlessV2ScalingConfiguration: {
minCapacity: 0.5,
maxCapacity: 1,
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', {
ServerlessV2ScalingConfiguration: {
MinCapacity: 0.5,
MaxCapacity: 1,
},
});
// Should not create any instances
Template.fromStack(stack).resourceCountIs('AWS::DocDB::DBInstance', 0);
});

test('serverless cluster has empty instance arrays', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
const cluster = new DatabaseCluster(stack, 'Database', {
masterUser: {
username: 'admin',
},
vpc,
serverlessV2ScalingConfiguration: {
minCapacity: 0.5,
maxCapacity: 2,
},
});

// THEN
expect(cluster.instanceIdentifiers).toEqual([]);
expect(cluster.instanceEndpoints).toEqual([]);
});

test('cannot specify instanceType with serverless configuration', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN/THEN
expect(() => {
new DatabaseCluster(stack, 'Database', {
masterUser: {
username: 'admin',
},
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE),
serverlessV2ScalingConfiguration: {
minCapacity: 0.5,
maxCapacity: 1,
},
});
}).toThrow('Cannot specify instanceType for serverless clusters');
});

test('provisioned cluster requires instanceType', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN/THEN
expect(() => {
new DatabaseCluster(stack, 'Database', {
masterUser: {
username: 'admin',
},
vpc,
});
}).toThrow('instanceType is required for provisioned clusters');
});

test('serverless cluster with all configuration options', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
new DatabaseCluster(stack, 'Database', {
masterUser: {
username: 'admin',
},
vpc,
serverlessV2ScalingConfiguration: {
minCapacity: 1,
maxCapacity: 4,
},
engineVersion: '5.0.0',
deletionProtection: true,
exportAuditLogsToCloudWatch: true,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', {
ServerlessV2ScalingConfiguration: {
MinCapacity: 1,
MaxCapacity: 4,
},
EngineVersion: '5.0.0',
DeletionProtection: true,
EnableCloudwatchLogsExports: ['audit'],
});
});

test('serverless cluster requires engine version 5.0.0 or higher', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN/THEN
expect(() => {
new DatabaseCluster(stack, 'Database', {
masterUser: {
username: 'admin',
},
vpc,
serverlessV2ScalingConfiguration: {
minCapacity: 0.5,
maxCapacity: 1,
},
engineVersion: '4.0.0',
});
}).toThrow("DocumentDB serverless requires engine version 5.0.0 or higher, got '4.0.0'");
});

test('serverless cluster allows engine version 5.0.0', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
new DatabaseCluster(stack, 'Database', {
masterUser: {
username: 'admin',
},
vpc,
serverlessV2ScalingConfiguration: {
minCapacity: 0.5,
maxCapacity: 1,
},
engineVersion: '5.0.0',
});

// THEN - should not throw
Template.fromStack(stack).hasResourceProperties('AWS::DocDB::DBCluster', {
EngineVersion: '5.0.0',
ServerlessV2ScalingConfiguration: {
MinCapacity: 0.5,
MaxCapacity: 1,
},
});
});
});
});

function testStack() {
Expand Down
Loading