From ba6b525d27b593bff8c64576352c9716c2738d8d Mon Sep 17 00:00:00 2001 From: Sainath Mallidi Date: Wed, 27 Jan 2021 23:40:30 -0800 Subject: [PATCH 1/5] feat(neptune): high level constructs for db clusters and instances - This change adds higher level constructs for Neptune clusters - Adds higher-level constructs for - AWS::Neptune::DBCluster - AWS::Neptune::DBInstance - AWS::Neptune::DBClusterParameterGroup - AWS::Neptune::DBParameterGroup - AWS::Neptune::DBSubnetGroup fixes aws#12762 --- packages/@aws-cdk/aws-neptune/README.md | 37 +- .../@aws-cdk/aws-neptune/lib/cluster-ref.ts | 75 +++ packages/@aws-cdk/aws-neptune/lib/cluster.ts | 397 +++++++++++++ packages/@aws-cdk/aws-neptune/lib/endpoint.ts | 31 ++ packages/@aws-cdk/aws-neptune/lib/index.ts | 7 + .../aws-neptune/lib/parameter-group.ts | 135 +++++ packages/@aws-cdk/aws-neptune/lib/props.ts | 106 ++++ .../@aws-cdk/aws-neptune/lib/subnet-group.ts | 91 +++ packages/@aws-cdk/aws-neptune/package.json | 16 +- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 482 ++++++++++++++++ .../aws-neptune/test/endpoint.test.ts | 29 + .../test/integ.cluster.expected.json | 520 ++++++++++++++++++ .../aws-neptune/test/integ.cluster.ts | 50 ++ .../@aws-cdk/aws-neptune/test/neptune.test.ts | 6 - .../aws-neptune/test/parameter-group.test.ts | 50 ++ .../aws-neptune/test/subnet-group.test.ts | 83 +++ 16 files changed, 2107 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/aws-neptune/lib/cluster-ref.ts create mode 100644 packages/@aws-cdk/aws-neptune/lib/cluster.ts create mode 100644 packages/@aws-cdk/aws-neptune/lib/endpoint.ts create mode 100644 packages/@aws-cdk/aws-neptune/lib/parameter-group.ts create mode 100644 packages/@aws-cdk/aws-neptune/lib/props.ts create mode 100644 packages/@aws-cdk/aws-neptune/lib/subnet-group.ts create mode 100644 packages/@aws-cdk/aws-neptune/test/cluster.test.ts create mode 100644 packages/@aws-cdk/aws-neptune/test/endpoint.test.ts create mode 100644 packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json create mode 100644 packages/@aws-cdk/aws-neptune/test/integ.cluster.ts delete mode 100644 packages/@aws-cdk/aws-neptune/test/neptune.test.ts create mode 100644 packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts create mode 100644 packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index 6b2eddde67362..19c11e554f16e 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -9,10 +9,45 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- +## Starting a Neptune Database + +To set up a Neptune database, define a `DatabaseCluster`. You must always launch a database in a VPC. + +```ts +const cluster = new DatabaseCluster(this, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + } +}); +``` + +Your cluster will be empty by default. + +## Connecting + +To control who can access the cluster, use the `.connections` attribute. Neptune databases have a default port, so +you don't need to specify the port: + +```ts +cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); +``` + +The endpoints to access your database cluster will be available as the `.clusterEndpoint` and `.clusterReadEndpoint` +attributes: + ```ts -import * as neptune from '@aws-cdk/aws-neptune'; +const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster-ref.ts b/packages/@aws-cdk/aws-neptune/lib/cluster-ref.ts new file mode 100644 index 0000000000000..71b8f30f0714a --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/cluster-ref.ts @@ -0,0 +1,75 @@ +import { IConnectable, ISecurityGroup } from '@aws-cdk/aws-ec2'; +import { IResource } from '@aws-cdk/core'; +import { Endpoint } from './endpoint'; + +/** + * Create a clustered database with a given number of instances. + */ +export interface IDatabaseCluster extends IResource, IConnectable { + /** + * Identifier of the cluster + */ + readonly clusterIdentifier: string; + + /** + * Identifiers of the replicas + */ + readonly instanceIdentifiers: string[]; + + /** + * The endpoint to use for read/write operations + * @attribute Endpoint,Port + */ + readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + * @attribute ReadEndpoint + */ + readonly clusterReadEndpoint: Endpoint; + + /** + * Endpoints which address each individual replica. + */ + readonly instanceEndpoints: Endpoint[]; +} + +/** + * Properties that describe an existing cluster instance + */ +export interface DatabaseClusterAttributes { + /** + * The database port + */ + readonly port: number; + + /** + * The security group of the database cluster + */ + readonly securityGroup: ISecurityGroup; + + /** + * Identifier for the cluster + */ + readonly clusterIdentifier: string; + + /** + * Identifier for the instances + */ + readonly instanceIdentifiers: string[]; + + /** + * Cluster endpoint address + */ + readonly clusterEndpointAddress: string; + + /** + * Reader endpoint address + */ + readonly readerEndpointAddress: string; + + /** + * Endpoint addresses of individual instances + */ + readonly instanceEndpointAddresses: string[]; +} diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts new file mode 100644 index 0000000000000..25ab0cd9d6f90 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -0,0 +1,397 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import { RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; +import { Endpoint } from './endpoint'; +import { CfnDBCluster, CfnDBInstance } from './neptune.generated'; +import { IClusterParameterGroup } from './parameter-group'; +import { BackupProps, InstanceProps } from './props'; +import { ISubnetGroup, SubnetGroup } from './subnet-group'; + +/** + * Properties for a new database cluster + */ +export interface DatabaseClusterProps { + /** + * What version of the database to start + * + * @default - The default engine version. + */ + readonly engineVersion?: string; + + /** + * The port the Neptune cluster will listen on + * + * @default - The default engine port + */ + readonly port?: number; + + /** + * Backup settings + * + * @default - Backup retention period for automated backups is 1 day. + * Backup preferred window is set to 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. + */ + readonly backup?: BackupProps; + + /** + * The KMS key for storage encryption. + * + * @default - default master key. + */ + readonly kmsKey?: kms.IKey; + + /** + * Whether to enable storage encryption + * + * @default true + */ + readonly storageEncrypted?: boolean; + + /** + * Number of Neptune compute instances + * + * @default 1 + */ + readonly instances?: number; + + /** + * An optional identifier for the cluster + * + * @default - A name is automatically generated. + */ + readonly dbClusterName?: string; + + /** + * Base identifier for instances + * + * Every replica is named by appending the replica number to this string, 1-based. + * + * @default - `dbClusterName` is used with the word "Instance" appended. If `dbClusterName` is not provided, the + * identifier is automatically generated. + */ + readonly instanceIdentifierBase?: string; + + /** + * Settings for the individual instances that are launched + */ + readonly instanceProps: InstanceProps; + + /** + * A list of AWS Identity and Access Management (IAM) role that can be used by the cluster to access other AWS services. + * + * @default - No role is attached to the cluster. + */ + readonly associatedRoles?: iam.IRole[]; + + /** + * Indicates whether the DB cluster is IAM auth enabled. + * + * @default - false, connections to database don't require IAM auth + */ + readonly iamAuthEnabled?: boolean; + + /** + * Indicates whether the DB cluster should have deletion protection enabled. + * + * @default - true if ``removalPolicy`` is RETAIN, false otherwise + */ + readonly deletionProtection?: boolean; + + /** + * A weekly time range in which maintenance should preferably execute. + * + * Must be at least 30 minutes long. + * + * Example: 'tue:04:17-tue:04:47' + * + * @default - 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. + */ + readonly preferredMaintenanceWindow?: string; + + /** + * Additional parameters to pass to the database engine + * + * @default - No parameter group. + */ + readonly parameterGroup?: IClusterParameterGroup; + + /** + * Existing subnet group for the cluster. + * + * @default - a new subnet group will be created. + */ + readonly subnetGroup?: ISubnetGroup; + + /** + * What subnets to run the Neptune instances in. + * + * Must be at least 2 subnets in two different AZs. + */ + readonly vpc: ec2.IVpc; + + /** + * Where to place the instances within the VPC + * + * @default private subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * Security group. + * + * @default a new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * The removal policy to apply when the cluster and its instances are removed + * or replaced during a stack update, or when the stack is deleted. This + * removal policy also applies to the implicit security group created for the + * cluster if one is not supplied as a parameter. + * + * @default - Retain cluster. + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * A new or imported clustered database. + */ +abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster { + /** + * Identifier of the cluster + */ + public abstract readonly clusterIdentifier: string; + + /** + * The endpoint to use for read/write operations + */ + public abstract readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + */ + public abstract readonly clusterReadEndpoint: Endpoint; + + /** + * Access to the network connections + */ + public abstract readonly connections: ec2.Connections; + + /** + * Identifiers of the replicas + */ + public abstract readonly instanceIdentifiers: string[]; + + /** + * Endpoints which address each individual replica. + */ + public abstract readonly instanceEndpoints: Endpoint[]; +} + +/** + * Create a clustered database with a given number of instances. + * + * @resource AWS::Neptune::DBCluster + */ +export class DatabaseCluster extends DatabaseClusterBase { + + /** + * The default number of instances in the Neptune cluster if none are + * specified + */ + public static readonly DEFAULT_NUM_INSTANCES = 1; + + /** + * Import an existing DatabaseCluster from properties + */ + public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { + class Import extends DatabaseClusterBase implements IDatabaseCluster { + public readonly defaultPort = ec2.Port.tcp(attrs.port); + public readonly connections = new ec2.Connections({ + securityGroups: [attrs.securityGroup], + defaultPort: this.defaultPort, + }); + public readonly clusterIdentifier = attrs.clusterIdentifier; + public readonly instanceIdentifiers = attrs.instanceIdentifiers; + public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); + public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); + public readonly instanceEndpoints = attrs.instanceEndpointAddresses.map(a => new Endpoint(a, attrs.port)); + } + + return new Import(scope, id); + } + + /** + * Identifier of the cluster + */ + public readonly clusterIdentifier: string; + + /** + * The endpoint to use for read/write operations + */ + public readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + */ + public readonly clusterReadEndpoint: Endpoint; + + /** + * The resource id for the cluster; for example: cluster-ABCD1234EFGH5678IJKL90MNOP. The cluster ID uniquely + * identifies the cluster and is used in things like IAM authentication policies. + * @attribute ClusterResourceId + */ + public readonly clusterResourceIdentifier: string; + + /** + * The connections object to implement IConectable + */ + public readonly connections: ec2.Connections; + + /** + * Identifiers of the replicas + */ + public readonly instanceIdentifiers: string[] = []; + + /** + * Endpoints which address each individual replica. + */ + public readonly instanceEndpoints: Endpoint[] = []; + + /** + * The VPC where the DB subnet group is created. + */ + public readonly vpc: ec2.IVpc; + + /** + * The subnets used by the DB subnet group. + */ + public readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * The security groups used by DB cluster. + */ + protected readonly securityGroups: ec2.ISecurityGroup[]; + + /** + * Subnet group used by the DB + */ + public readonly subnetGroup: ISubnetGroup; + + constructor(scope: Construct, id: string, props: DatabaseClusterProps) { + super(scope, id); + + this.vpc = props.vpc; + this.vpcSubnets = props.vpcSubnets ? props.vpcSubnets : { + subnetType: ec2.SubnetType.PRIVATE, + }; + + // Determine the subnet(s) to deploy the Neptune cluster to + const { subnetIds, internetConnectivityEstablished } = this.vpc.selectSubnets(this.vpcSubnets); + + // Cannot test whether the subnets are in different AZs, but at least we can test the amount. + if (subnetIds.length < 2) { + throw new Error(`Cluster requires at least 2 subnets, got ${subnetIds.length}`); + } + + this.subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'Subnets', { + description: `Subnets for ${id} database`, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined, + }); + + this.securityGroups = props.securityGroups ?? [ + new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'Neptune security group', + vpc: this.vpc, + }), + ]; + + // Default to encrypted storage + const storageEncrypted = props.storageEncrypted ?? true; + + if (props.kmsKey && !storageEncrypted) { + throw new Error('KMS key supplied but storageEncrypted is false'); + } + + // Deletion protection + const deletionProtection = props.deletionProtection ?? (props.removalPolicy === RemovalPolicy.RETAIN ? true : undefined); + + // Create the Neptune cluster + const cluster = new CfnDBCluster(this, 'Resource', { + // Basic + engineVersion: props.engineVersion, + dbClusterIdentifier: props.dbClusterName, + dbSubnetGroupName: this.subnetGroup.subnetGroupName, + port: props.port, + vpcSecurityGroupIds: this.securityGroups.map(sg => sg.securityGroupId), + dbClusterParameterGroupName: props.parameterGroup?.clusterParameterGroupName, + deletionProtection: deletionProtection, + associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined, + iamAuthEnabled: props.iamAuthEnabled, + // Backup + backupRetentionPeriod: props.backup?.retention?.toDays(), + preferredBackupWindow: props.backup?.preferredWindow, + preferredMaintenanceWindow: props.preferredMaintenanceWindow, + // Encryption + kmsKeyId: props.kmsKey?.keyArn, + storageEncrypted, + }); + + cluster.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + this.clusterIdentifier = cluster.ref; + this.clusterResourceIdentifier = cluster.attrClusterResourceId; + + const port = Token.asNumber(cluster.attrPort); + this.clusterEndpoint = new Endpoint(cluster.attrEndpoint, port); + this.clusterReadEndpoint = new Endpoint(cluster.attrReadEndpoint, port); + + // Create the instances + const instanceCount = props.instances ?? DatabaseCluster.DEFAULT_NUM_INSTANCES; + if (instanceCount < 1) { + throw new Error('At least one instance is required'); + } + + 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 instance = new CfnDBInstance(this, `Instance${instanceIndex}`, { + // Link to cluster + dbClusterIdentifier: cluster.ref, + dbInstanceIdentifier: instanceIdentifier, + // Instance properties + dbInstanceClass: props.instanceProps.instanceType, + dbParameterGroupName: props.instanceProps.parameterGroup?.parameterGroupName, + }); + + instance.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + // 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.connections = new ec2.Connections({ + defaultPort: ec2.Port.tcp(port), + securityGroups: this.securityGroups, + }); + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/endpoint.ts b/packages/@aws-cdk/aws-neptune/lib/endpoint.ts new file mode 100644 index 0000000000000..fc92942416b7d --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/endpoint.ts @@ -0,0 +1,31 @@ +import { Token } from '@aws-cdk/core'; + +/** + * Connection endpoint of a neptune 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.isUnresolved(port) ? Token.asString(port) : port; + this.socketAddress = `${address}:${portDesc}`; + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/index.ts b/packages/@aws-cdk/aws-neptune/lib/index.ts index 67cdd432ee7d2..7c79249bffa51 100644 --- a/packages/@aws-cdk/aws-neptune/lib/index.ts +++ b/packages/@aws-cdk/aws-neptune/lib/index.ts @@ -1,2 +1,9 @@ +export * from './cluster'; +export * from './cluster-ref'; +export * from './endpoint'; +export * from './parameter-group'; +export * from './props'; +export * from './subnet-group'; + // AWS::Neptune CloudFormation Resources: export * from './neptune.generated'; diff --git a/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts b/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts new file mode 100644 index 0000000000000..3cfacf061f19c --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts @@ -0,0 +1,135 @@ +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDBClusterParameterGroup } from './neptune.generated'; + +/** + * Properties for a parameter group + */ +interface ParameterGroupPropsBase { + /** + * Description for this parameter group + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * The parameters in this parameter group + */ + readonly parameters: { [key: string]: string }; +} + +/** + * Marker class for cluster parameter group + */ +export interface ClusterParameterGroupProps extends ParameterGroupPropsBase { + /** + * The name of the parameter group + * + * @default A CDK generated name for the parameter group + */ + readonly clusterParameterGroupName?: string; +} + +/** + * Marker class for cluster parameter group + */ +export interface ParameterGroupProps extends ParameterGroupPropsBase { + /** + * The name of the parameter group + * + * @default A CDK generated name for the parameter group + */ + readonly parameterGroupName?: string; +} + +/** + * A parameter group + */ +export interface IClusterParameterGroup extends IResource { + /** + * The name of this parameter group + */ + readonly clusterParameterGroupName: string; +} + + +/** + * A cluster parameter group + * + * @resource AWS::Neptune::DBClusterParameterGroup + */ +export class ClusterParameterGroup extends Resource implements IClusterParameterGroup { + /** + * Imports a parameter group + */ + public static fromClusterParameterGroupName(scope: Construct, id: string, clusterParameterGroupName: string): IClusterParameterGroup { + class Import extends Resource implements IClusterParameterGroup { + public readonly clusterParameterGroupName = clusterParameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public readonly clusterParameterGroupName: string; + + constructor(scope: Construct, id: string, props: ClusterParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBClusterParameterGroup(this, 'Resource', { + name: props.clusterParameterGroupName, + description: props.description || 'Cluster parameter group for neptune db cluster', + family: 'neptune1', + parameters: props.parameters, + }); + + this.clusterParameterGroupName = resource.ref; + } +} + +/** + * A parameter group + */ +export interface IParameterGroup extends IResource { + /** + * The name of this parameter group + */ + readonly parameterGroupName: string; +} + +/** + * DB parameter group + * + * @resource AWS::Neptune::DBParameterGroup + */ +export class ParameterGroup extends Resource implements IParameterGroup { + /** + * Imports a parameter group + */ + public static fromParameterGroupName(scope: Construct, id: string, parameterGroupName: string): IParameterGroup { + class Import extends Resource implements IParameterGroup { + public readonly parameterGroupName = parameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public readonly parameterGroupName: string; + + constructor(scope: Construct, id: string, props: ParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBClusterParameterGroup(this, 'Resource', { + name: props.parameterGroupName, + description: props.description || 'Instance parameter group for neptune db instances', + family: 'neptune1', + parameters: props.parameters, + }); + + this.parameterGroupName = resource.ref; + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/props.ts b/packages/@aws-cdk/aws-neptune/lib/props.ts new file mode 100644 index 0000000000000..9466fba657059 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/props.ts @@ -0,0 +1,106 @@ +import { Duration } from '@aws-cdk/core'; +import { IParameterGroup } from './parameter-group'; + +/** + * Possible Instances Types to use in Neptune cluster + * used for defining {@link InstanceProps.instanceType}. + */ +export enum InstanceType { + /** + * db.r5.large + */ + R5_LARGE = 'db.r5.large', + /** + * db.r5.xlarge + */ + R5_XLARGE = 'db.r5.xlarge', + /** + * db.r5.2xlarge + */ + R5_2XLARGE = 'db.r5.2xlarge', + /** + * db.r5.4xlarge + */ + R5_4XLARGE = 'db.r5.4xlarge', + /** + * db.r5.8xlarge + */ + R5_8XLARGE = 'db.r5.8xlarge', + /** + * db.r5.12xlarge + */ + R5_12XLARGE = 'db.r5.12xlarge', + /** + * db.r5.24xlarge + */ + R5_24XLARGE = 'db.r5.24xlarge', + /** + * db.r4.large + */ + R4_LARGE = 'db.r4.large', + /** + * db.r4.xlarge + */ + R4_XLARGE = 'db.r4.xlarge', + /** + * db.r4.2xlarge + */ + R4_2XLARGE = 'db.r4.2xlarge', + /** + * db.r4.4xlarge + */ + R4_4XLARGE = 'db.r4.4xlarge', + /** + * db.r4.8xlarge + */ + R4_8XLARGE = 'db.r4.8xlarge', + /** + * db.t3.medium + */ + T3_MEDIUM = 'db.t3.medium' +} + +/** + * Backup configuration for Neptune databases + * + * @default - The retention period for automated backups is 1 day. + * The preferred backup window will be a 30-minute window selected at random + * from an 8-hour block of time for each AWS Region. + */ +export interface BackupProps { + + /** + * How many days to retain the backup + */ + readonly retention: Duration; + + /** + * A daily time range in 24-hours UTC format in which backups preferably execute. + * + * Must be at least 30 minutes long. + * + * Example: '01:00-02:00' + * + * @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 + */ + readonly preferredWindow?: string; +} + +/** + * Instance properties for Neptune database instances + */ +export interface InstanceProps { + /** + * What type of instance to start for the replicas + */ + readonly instanceType: InstanceType; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; +} + diff --git a/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts b/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts new file mode 100644 index 0000000000000..383435b7a0b38 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts @@ -0,0 +1,91 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import { IResource, RemovalPolicy, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDBSubnetGroup } from './neptune.generated'; + +/** + * Interface for a subnet group. + */ +export interface ISubnetGroup extends IResource { + /** + * The name of the subnet group. + * @attribute + */ + readonly subnetGroupName: string; +} + +/** + * Properties for creating a SubnetGroup. + */ +export interface SubnetGroupProps { + /** + * Description of the subnet group. + * + * @default - a name is generated + */ + readonly description?: string; + + /** + * The VPC to place the subnet group in. + */ + readonly vpc: ec2.IVpc; + + /** + * The name of the subnet group. + * + * @default - a name is generated + */ + readonly subnetGroupName?: string; + + /** + * Which subnets within the VPC to associate with this group. + * + * @default - private subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * The removal policy to apply when the subnet group are removed + * from the stack or replaced during an update. + * + * @default RemovalPolicy.DESTROY + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * Class for creating a RDS DB subnet group + * + * @resource AWS::Neptune::DBSubnetGroup + */ +export class SubnetGroup extends Resource implements ISubnetGroup { + + /** + * Imports an existing subnet group by name. + */ + public static fromSubnetGroupName(scope: Construct, id: string, subnetGroupName: string): ISubnetGroup { + return new class extends Resource implements ISubnetGroup { + public readonly subnetGroupName = subnetGroupName; + }(scope, id); + } + + public readonly subnetGroupName: string; + + constructor(scope: Construct, id: string, props: SubnetGroupProps) { + super(scope, id); + + const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE }); + + const subnetGroup = new CfnDBSubnetGroup(this, 'Resource', { + dbSubnetGroupDescription: props.description || 'Subnet group for Neptune', + dbSubnetGroupName: props.subnetGroupName, + subnetIds, + }); + + if (props.removalPolicy) { + subnetGroup.applyRemovalPolicy(props.removalPolicy); + } + + this.subnetGroupName = subnetGroup.ref; + } +} diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index b0ff4727beb8a..9f2f0e89d6615 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -74,22 +74,36 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-neptune.ParameterGroupProps", + "props-physical-name:@aws-cdk/aws-neptune.ClusterParameterGroupProps", + "props-physical-name:@aws-cdk/aws-neptune.SubnetGroupProps" + ] + }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts new file mode 100644 index 0000000000000..abca6af4d4f99 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -0,0 +1,482 @@ +import { expect as expectCDK, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; + +import { ClusterParameterGroup, DatabaseCluster, InstanceType } from '../lib'; + +describe('DatabaseCluster', () => { + + test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + Properties: { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + StorageEncrypted: true, + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + expectCDK(stack).to(haveResource('AWS::Neptune::DBSubnetGroup', { + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, + ], + })); + }); + + test('can create a cluster with a single instance', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + })); + }); + + test('errors when less than one instance is specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + expect(() => { + new DatabaseCluster(stack, 'Database', { + instances: 0, + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + }).toThrowError('At least one instance is required'); + }); + + test('errors when only one subnet is specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC', { + maxAzs: 1, + }); + + // WHEN + expect(() => { + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + vpcSubnets: { + subnetType: ec2.SubnetType.PRIVATE, + }, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + }).toThrowError('Cluster requires at least 2 subnets, got 1'); + }); + + test('can create a cluster with imported vpc and security group', () => { + // GIVEN + const stack = testStack(); + const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { + vpcId: 'VPC12345', + }); + const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'SecurityGroupId12345'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + securityGroups: [sg], + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: ['SecurityGroupId12345'], + })); + }); + + test('cluster with parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = new ClusterParameterGroup(stack, 'Params', { + description: 'bye', + parameters: { + param: 'value', + }, + }); + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + parameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + }); + + test('cluster with associated role', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + }); + role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess')); + + new DatabaseCluster(stack, 'Database', { + vpc, + associatedRoles: [role], + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + AssociatedRoles: [ + { + RoleArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }, + ], + })); + }); + + test('cluster with iam auth', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + iamAuthEnabled: true, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + IamAuthEnabled: true, + })); + }); + + test('cluster with imported parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = ClusterParameterGroup.fromClusterParameterGroupName(stack, 'Params', 'ParamGroupName'); + + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + parameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBClusterParameterGroupName: 'ParamGroupName', + })); + }); + + test('create an encrypted cluster with custom KMS key', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + kmsKey: new kms.Key(stack, 'Key'), + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + KmsKeyId: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + StorageEncrypted: true, + })); + }); + + test('creating a cluster defaults to using encryption', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + StorageEncrypted: true, + })); + }); + + test('supplying a KMS key with storageEncryption false throws an error', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + function action() { + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + kmsKey: new kms.Key(stack, 'Key'), + storageEncrypted: false, + }); + } + + // THEN + expect(action).toThrow(); + }); + + test('cluster exposes different read and write endpoints', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + }); + + // THEN + expect(stack.resolve(cluster.clusterEndpoint)).not.toBe(stack.resolve(cluster.clusterReadEndpoint)); + }); + + test('instance identifier used when present', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const instanceIdentifierBase = 'instanceidentifierbase-'; + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + instanceIdentifierBase, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBInstanceIdentifier: `${instanceIdentifierBase}1`, + })); + }); + + test('cluster identifier used', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const clusterIdentifier = 'clusteridentifier-'; + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + dbClusterName: clusterIdentifier, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBInstanceIdentifier: `${clusterIdentifier}instance1`, + })); + }); + + test('imported cluster has supplied attributes', () => { + // GIVEN + const stack = testStack(); + + // WHEN + const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterIdentifier: 'identifier', + instanceEndpointAddresses: ['addr'], + instanceIdentifiers: ['identifier'], + port: 3306, + readerEndpointAddress: 'reader-address', + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + + // THEN + expect(cluster.clusterEndpoint.hostname).toEqual('addr'); + expect(cluster.clusterEndpoint.port).toEqual(3306); + expect(cluster.clusterIdentifier).toEqual('identifier'); + expect(cluster.instanceIdentifiers).toEqual(['identifier']); + expect(cluster.clusterReadEndpoint.hostname).toEqual('reader-address'); + }); + + test('imported cluster with imported security group honors allowAllOutbound', () => { + // GIVEN + const stack = testStack(); + + const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterIdentifier: 'identifier', + instanceEndpointAddresses: ['addr'], + instanceIdentifiers: ['identifier'], + port: 3306, + readerEndpointAddress: 'reader-address', + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + + // WHEN + cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); + + // THEN + expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: 'sg-123456789', + })); + }); + + test('backup retention period respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + backup: { + retention: cdk.Duration.days(20), + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + BackupRetentionPeriod: 20, + })); + }); + + test('backup maintenance window respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + backup: { + retention: cdk.Duration.days(20), + preferredWindow: '07:34-08:04', + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + BackupRetentionPeriod: 20, + PreferredBackupWindow: '07:34-08:04', + })); + }); + + test('regular maintenance window respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + preferredMaintenanceWindow: '07:34-08:04', + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + PreferredMaintenanceWindow: '07:34-08:04', + })); + }); +}); + +function testStack() { + const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + return stack; +} diff --git a/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts b/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts new file mode 100644 index 0000000000000..acf22d45f385a --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts @@ -0,0 +1,29 @@ +import { Endpoint } from '../lib'; + +// A numeric CDK token (see: https://docs.aws.amazon.com/cdk/latest/guide/tokens.html#tokens_number) +const CDK_NUMERIC_TOKEN = -1.8881545897087626e+289; + +describe('Endpoint', () => { + test('accepts tokens for the port value', () => { + // GIVEN + const token = CDK_NUMERIC_TOKEN; + + // WHEN + const endpoint = new Endpoint('127.0.0.1', token); + + // THEN + expect(endpoint.port).toBe(token); + }); + + test('accepts valid port string numbers', () => { + // GIVEN + for (const port of [1, 50, 65535]) { + // WHEN + const endpoint = new Endpoint('127.0.0.1', port); + + // THEN + expect(endpoint.port).toBe(port); + } + }); + +}); diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json new file mode 100644 index 0000000000000..13dbc845318e0 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json @@ -0,0 +1,520 @@ +{ + "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-neptune-integ/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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-neptune-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "ParamsA8366201": { + "Type": "AWS::Neptune::DBClusterParameterGroup", + "Properties": { + "Description": "A nice parameter group", + "Family": "neptune1", + "Parameters": { + "neptune_enable_audit_log": "1", + "neptune_query_timeout": "100000" + } + } + }, + "DbSecurity381C2C15": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseSubnets3C9252C9": { + "Type": "AWS::Neptune::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnets for Database database", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + } + }, + "DatabaseSecurityGroup5C91FDCB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Neptune security group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "DatabaseSecurityGroupfrom00000IndirectPortF24F2E03": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "CidrIp": "0.0.0.0/0", + "Description": "Open to the world", + "FromPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Port" + ] + }, + "GroupId": { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + }, + "ToPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Port" + ] + } + } + }, + "DatabaseB269D8BB": { + "Type": "AWS::Neptune::DBCluster", + "Properties": { + "DBClusterParameterGroupName": { + "Ref": "ParamsA8366201" + }, + "DBSubnetGroupName": { + "Ref": "DatabaseSubnets3C9252C9" + }, + "KmsKeyId": { + "Fn::GetAtt": [ + "DbSecurity381C2C15", + "Arn" + ] + }, + "StorageEncrypted": true, + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseInstance1844F58FD": { + "Type": "AWS::Neptune::DBInstance", + "Properties": { + "DBInstanceClass": "db.r5.large", + "DBClusterIdentifier": { + "Ref": "DatabaseB269D8BB" + } + }, + "DependsOn": [ + "VPCPrivateSubnet1DefaultRouteAE1D6490", + "VPCPrivateSubnet2DefaultRouteF4F5CFD2" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts new file mode 100644 index 0000000000000..4017023fbcfa5 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts @@ -0,0 +1,50 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; +import { DatabaseCluster, InstanceType } from '../lib'; +import { ClusterParameterGroup } from '../lib/parameter-group'; + +/* + * Stack verification steps: + * * aws docdb describe-db-clusters --db-cluster-identifier + */ + +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 }); + + const params = new ClusterParameterGroup(this, 'Params', { + description: 'A nice parameter group', + parameters: { + neptune_enable_audit_log: '1', + neptune_query_timeout: '100000', + }, + }); + + const kmsKey = new kms.Key(this, 'DbSecurity', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const cluster = new DatabaseCluster(this, 'Database', { + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, + instanceProps: { + instanceType: InstanceType.R5_LARGE, + }, + parameterGroup: params, + kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'aws-cdk-neptune-integ'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-neptune/test/neptune.test.ts b/packages/@aws-cdk/aws-neptune/test/neptune.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-neptune/test/neptune.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts new file mode 100644 index 0000000000000..6ba6ff6562d76 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts @@ -0,0 +1,50 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { ClusterParameterGroup, ParameterGroup } from '../lib'; + +describe('ClusterParameterGroup', () => { + + test('create a cluster parameter group', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + key: 'value', + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::Neptune::DBClusterParameterGroup', { + Description: 'desc', + Parameters: { + key: 'value', + }, + })); + + }); + + test('create a instance/db parameter group', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new ParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + key: 'value', + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::Neptune::DBClusterParameterGroup', { + Description: 'desc', + Parameters: { + key: 'value', + }, + })); + + }); +}); diff --git a/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts new file mode 100644 index 0000000000000..e6c75013716c8 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts @@ -0,0 +1,83 @@ +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import { Stack } from '@aws-cdk/core'; +import { SubnetGroup } from '../lib'; + +let stack: Stack; +let vpc: ec2.IVpc; + +beforeEach(() => { + stack = new Stack(); + vpc = new ec2.Vpc(stack, 'VPC'); +}); + +test('creates a subnet group from minimal properties', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); +}); + +test('creates a subnet group from all properties', () => { + new SubnetGroup(stack, 'Group', { + description: 'My Shared Group', + subnetGroupName: 'SharedGroup', + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'My Shared Group', + DBSubnetGroupName: 'SharedGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); +}); + +describe('subnet selection', () => { + test('defaults to private subnets', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); + }); + + test('can specify subnet type', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, + { Ref: 'VPCPublicSubnet2Subnet74179F39' }, + ], + }); + }); +}); + +test('import group by name', () => { + const subnetGroup = SubnetGroup.fromSubnetGroupName(stack, 'Group', 'my-subnet-group'); + expect(subnetGroup.subnetGroupName).toBe('my-subnet-group'); +}); From 4421394c1d78d8d1cd45c17ac932f9d3de45227f Mon Sep 17 00:00:00 2001 From: Sainath Mallidi Date: Mon, 1 Feb 2021 18:28:29 -0800 Subject: [PATCH 2/5] Add L2 construct for instance and slight refactor to properties --- .../@aws-cdk/aws-neptune/lib/cluster-ref.ts | 75 ------ packages/@aws-cdk/aws-neptune/lib/cluster.ts | 146 +++++++--- packages/@aws-cdk/aws-neptune/lib/index.ts | 3 +- packages/@aws-cdk/aws-neptune/lib/instance.ts | 254 ++++++++++++++++++ packages/@aws-cdk/aws-neptune/lib/props.ts | 106 -------- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 81 ++---- .../aws-neptune/test/instance.test.ts | 131 +++++++++ .../aws-neptune/test/integ.cluster.ts | 6 +- 8 files changed, 518 insertions(+), 284 deletions(-) delete mode 100644 packages/@aws-cdk/aws-neptune/lib/cluster-ref.ts create mode 100644 packages/@aws-cdk/aws-neptune/lib/instance.ts delete mode 100644 packages/@aws-cdk/aws-neptune/lib/props.ts create mode 100644 packages/@aws-cdk/aws-neptune/test/instance.test.ts diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster-ref.ts b/packages/@aws-cdk/aws-neptune/lib/cluster-ref.ts deleted file mode 100644 index 71b8f30f0714a..0000000000000 --- a/packages/@aws-cdk/aws-neptune/lib/cluster-ref.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { IConnectable, ISecurityGroup } from '@aws-cdk/aws-ec2'; -import { IResource } from '@aws-cdk/core'; -import { Endpoint } from './endpoint'; - -/** - * Create a clustered database with a given number of instances. - */ -export interface IDatabaseCluster extends IResource, IConnectable { - /** - * Identifier of the cluster - */ - readonly clusterIdentifier: string; - - /** - * Identifiers of the replicas - */ - readonly instanceIdentifiers: string[]; - - /** - * The endpoint to use for read/write operations - * @attribute Endpoint,Port - */ - readonly clusterEndpoint: Endpoint; - - /** - * Endpoint to use for load-balanced read-only operations. - * @attribute ReadEndpoint - */ - readonly clusterReadEndpoint: Endpoint; - - /** - * Endpoints which address each individual replica. - */ - readonly instanceEndpoints: Endpoint[]; -} - -/** - * Properties that describe an existing cluster instance - */ -export interface DatabaseClusterAttributes { - /** - * The database port - */ - readonly port: number; - - /** - * The security group of the database cluster - */ - readonly securityGroup: ISecurityGroup; - - /** - * Identifier for the cluster - */ - readonly clusterIdentifier: string; - - /** - * Identifier for the instances - */ - readonly instanceIdentifiers: string[]; - - /** - * Cluster endpoint address - */ - readonly clusterEndpointAddress: string; - - /** - * Reader endpoint address - */ - readonly readerEndpointAddress: string; - - /** - * Endpoint addresses of individual instances - */ - readonly instanceEndpointAddresses: string[]; -} diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts index 25ab0cd9d6f90..ded3a2a9dad1a 100644 --- a/packages/@aws-cdk/aws-neptune/lib/cluster.ts +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -1,15 +1,41 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { Duration, IResource, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; import { Endpoint } from './endpoint'; +import { InstanceType } from './instance'; import { CfnDBCluster, CfnDBInstance } from './neptune.generated'; -import { IClusterParameterGroup } from './parameter-group'; -import { BackupProps, InstanceProps } from './props'; +import { IClusterParameterGroup, IParameterGroup } from './parameter-group'; import { ISubnetGroup, SubnetGroup } from './subnet-group'; +/** + * Backup configuration for Neptune databases + * + * @default - The retention period for automated backups is 1 day. + * The preferred backup window will be a 30-minute window selected at random + * from an 8-hour block of time for each AWS Region. + */ +export interface BackupProps { + + /** + * How many days to retain the backup + */ + readonly retention: Duration; + + /** + * A daily time range in 24-hours UTC format in which backups preferably execute. + * + * Must be at least 30 minutes long. + * + * Example: '01:00-02:00' + * + * @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 + */ + readonly preferredWindow?: string; +} + /** * Properties for a new database cluster */ @@ -76,9 +102,9 @@ export interface DatabaseClusterProps { readonly instanceIdentifierBase?: string; /** - * Settings for the individual instances that are launched + * What type of instance to start for the replicas */ - readonly instanceProps: InstanceProps; + readonly instanceType: InstanceType; /** * A list of AWS Identity and Access Management (IAM) role that can be used by the cluster to access other AWS services. @@ -118,7 +144,14 @@ export interface DatabaseClusterProps { * * @default - No parameter group. */ - readonly parameterGroup?: IClusterParameterGroup; + readonly clusterParameterGroup?: IClusterParameterGroup; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; /** * Existing subnet group for the cluster. @@ -159,6 +192,59 @@ export interface DatabaseClusterProps { readonly removalPolicy?: RemovalPolicy } +/** + * Create a clustered database with a given number of instances. + */ +export interface IDatabaseCluster extends IResource, ec2.IConnectable { + /** + * Identifier of the cluster + */ + readonly clusterIdentifier: string; + + /** + * The endpoint to use for read/write operations + * @attribute Endpoint,Port + */ + readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + * @attribute ReadEndpoint + */ + readonly clusterReadEndpoint: Endpoint; +} + +/** + * Properties that describe an existing cluster instance + */ +export interface DatabaseClusterAttributes { + /** + * The database port + */ + readonly port: number; + + /** + * The security group of the database cluster + */ + readonly securityGroup: ec2.ISecurityGroup; + + /** + * Identifier for the cluster + */ + readonly clusterIdentifier: string; + + /** + * Cluster endpoint address + */ + readonly clusterEndpointAddress: string; + + /** + * Reader endpoint address + */ + readonly readerEndpointAddress: string; +} + + /** * A new or imported clustered database. */ @@ -182,16 +268,6 @@ abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster * Access to the network connections */ public abstract readonly connections: ec2.Connections; - - /** - * Identifiers of the replicas - */ - public abstract readonly instanceIdentifiers: string[]; - - /** - * Endpoints which address each individual replica. - */ - public abstract readonly instanceEndpoints: Endpoint[]; } /** @@ -218,10 +294,8 @@ export class DatabaseCluster extends DatabaseClusterBase { defaultPort: this.defaultPort, }); public readonly clusterIdentifier = attrs.clusterIdentifier; - public readonly instanceIdentifiers = attrs.instanceIdentifiers; public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); - public readonly instanceEndpoints = attrs.instanceEndpointAddresses.map(a => new Endpoint(a, attrs.port)); } return new Import(scope, id); @@ -254,16 +328,6 @@ export class DatabaseCluster extends DatabaseClusterBase { */ public readonly connections: ec2.Connections; - /** - * Identifiers of the replicas - */ - public readonly instanceIdentifiers: string[] = []; - - /** - * Endpoints which address each individual replica. - */ - public readonly instanceEndpoints: Endpoint[] = []; - /** * The VPC where the DB subnet group is created. */ @@ -284,6 +348,16 @@ export class DatabaseCluster extends DatabaseClusterBase { */ public readonly subnetGroup: ISubnetGroup; + /** + * Identifiers of the instance + */ + public readonly instanceIdentifiers: string[] = []; + + /** + * Endpoints which address each individual instance. + */ + public readonly instanceEndpoints: Endpoint[] = []; + constructor(scope: Construct, id: string, props: DatabaseClusterProps) { super(scope, id); @@ -332,7 +406,7 @@ export class DatabaseCluster extends DatabaseClusterBase { dbSubnetGroupName: this.subnetGroup.subnetGroupName, port: props.port, vpcSecurityGroupIds: this.securityGroups.map(sg => sg.securityGroupId), - dbClusterParameterGroupName: props.parameterGroup?.clusterParameterGroupName, + dbClusterParameterGroupName: props.clusterParameterGroup?.clusterParameterGroupName, deletionProtection: deletionProtection, associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined, iamAuthEnabled: props.iamAuthEnabled, @@ -373,18 +447,18 @@ export class DatabaseCluster extends DatabaseClusterBase { dbClusterIdentifier: cluster.ref, dbInstanceIdentifier: instanceIdentifier, // Instance properties - dbInstanceClass: props.instanceProps.instanceType, - dbParameterGroupName: props.instanceProps.parameterGroup?.parameterGroupName, - }); - - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, + dbInstanceClass: props.instanceType, + dbParameterGroupName: props.parameterGroup?.parameterGroupName, }); // We must have a dependency on the NAT gateway provider here to create // things in the right order. instance.node.addDependency(internetConnectivityEstablished); + instance.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + this.instanceIdentifiers.push(instance.ref); this.instanceEndpoints.push(new Endpoint(instance.attrEndpoint, port)); } diff --git a/packages/@aws-cdk/aws-neptune/lib/index.ts b/packages/@aws-cdk/aws-neptune/lib/index.ts index 7c79249bffa51..35257958b20aa 100644 --- a/packages/@aws-cdk/aws-neptune/lib/index.ts +++ b/packages/@aws-cdk/aws-neptune/lib/index.ts @@ -1,8 +1,7 @@ export * from './cluster'; -export * from './cluster-ref'; +export * from './instance'; export * from './endpoint'; export * from './parameter-group'; -export * from './props'; export * from './subnet-group'; // AWS::Neptune CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-neptune/lib/instance.ts b/packages/@aws-cdk/aws-neptune/lib/instance.ts new file mode 100644 index 0000000000000..0ac394548ad43 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/instance.ts @@ -0,0 +1,254 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IDatabaseCluster } from './cluster'; +import { Endpoint } from './endpoint'; +import { CfnDBInstance } from './neptune.generated'; +import { IParameterGroup } from './parameter-group'; + +/** + * Possible Instances Types to use in Neptune cluster + * used for defining {@link InstanceProps.instanceType}. + */ +export enum InstanceType { + /** + * db.r5.large + */ + R5_LARGE = 'db.r5.large', + /** + * db.r5.xlarge + */ + R5_XLARGE = 'db.r5.xlarge', + /** + * db.r5.2xlarge + */ + R5_2XLARGE = 'db.r5.2xlarge', + /** + * db.r5.4xlarge + */ + R5_4XLARGE = 'db.r5.4xlarge', + /** + * db.r5.8xlarge + */ + R5_8XLARGE = 'db.r5.8xlarge', + /** + * db.r5.12xlarge + */ + R5_12XLARGE = 'db.r5.12xlarge', + /** + * db.r5.24xlarge + */ + R5_24XLARGE = 'db.r5.24xlarge', + /** + * db.r4.large + */ + R4_LARGE = 'db.r4.large', + /** + * db.r4.xlarge + */ + R4_XLARGE = 'db.r4.xlarge', + /** + * db.r4.2xlarge + */ + R4_2XLARGE = 'db.r4.2xlarge', + /** + * db.r4.4xlarge + */ + R4_4XLARGE = 'db.r4.4xlarge', + /** + * db.r4.8xlarge + */ + R4_8XLARGE = 'db.r4.8xlarge', + /** + * db.t3.medium + */ + T3_MEDIUM = 'db.t3.medium' +} + +/** + * A database instance + */ +export interface IDatabaseInstance extends cdk.IResource { + /** + * The instance identifier. + */ + readonly instanceIdentifier: string; + + /** + * The instance endpoint. + */ + readonly instanceEndpoint: Endpoint; + + /** + * The instance endpoint address. + * + * @attribute Endpoint + */ + readonly dbInstanceEndpointAddress: string; + + /** + * The instance endpoint port. + * + * @attribute Port + */ + readonly dbInstanceEndpointPort: string; +} + +/** + * 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; +} + +/** + * A new or imported database instance. + */ +abstract class DatabaseInstanceBase extends cdk.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 defaultPort = ec2.Port.tcp(attrs.port); + 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); + } + + return new Import(scope, id); + } + + /** + * @inheritdoc + */ + public abstract readonly instanceIdentifier: string; + /** + * @inheritdoc + */ + public abstract readonly instanceEndpoint: Endpoint; + /** + * @inheritdoc + */ + public abstract readonly dbInstanceEndpointAddress: string; + /** + * @inheritdoc + */ + public abstract readonly dbInstanceEndpointPort: string; +} + +/** + * Construction properties for a DatabaseInstanceNew + */ +export interface DatabaseInstanceProps { + /** + * The Neptune database cluster the instance should launch into. + */ + readonly cluster: IDatabaseCluster; + + /** + * What type of instance to start for the replicas + */ + readonly instanceType: InstanceType; + + /** + * The name of the Availability Zone where the DB instance will be located. + * + * @default - no preference + */ + readonly availabilityZone?: string; + + /** + * A name for the DB instance. If you specify a name, AWS CloudFormation + * converts it to lowercase. + * + * @default - a CloudFormation generated name + */ + readonly dbInstanceName?: string; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; + + /** + * The CloudFormation policy to apply when the instance is removed from the + * stack or replaced during an update. + * + * @default RemovalPolicy.Retain + */ + readonly removalPolicy?: cdk.RemovalPolicy +} + +/** + * A database instance + * + * @resource AWS::Neptune::DBInstance + */ +export class DatabaseInstance extends DatabaseInstanceBase implements IDatabaseInstance { + /** + * The instance's database cluster + */ + public readonly cluster: IDatabaseCluster; + + /** + * @inheritdoc + */ + public readonly instanceIdentifier: string; + + /** + * @inheritdoc + */ + public readonly instanceEndpoint: Endpoint; + + /** + * @inheritdoc + */ + public readonly dbInstanceEndpointAddress: string; + + /** + * @inheritdoc + */ + public readonly dbInstanceEndpointPort: string; + + constructor(scope: Construct, id: string, props: DatabaseInstanceProps) { + super(scope, id); + + const instance = new CfnDBInstance(this, 'Resource', { + dbClusterIdentifier: props.cluster.clusterIdentifier, + dbInstanceClass: props.instanceType, + availabilityZone: props.availabilityZone, + dbInstanceIdentifier: props.dbInstanceName, + dbParameterGroupName: props.parameterGroup?.parameterGroupName, + }); + + this.cluster = props.cluster; + this.instanceIdentifier = instance.ref; + this.dbInstanceEndpointAddress = instance.attrEndpoint; + this.dbInstanceEndpointPort = instance.attrPort; + + // create a number token that represents the port of the instance + const portAttribute = cdk.Token.asNumber(instance.attrPort); + this.instanceEndpoint = new Endpoint(instance.attrEndpoint, portAttribute); + + instance.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/props.ts b/packages/@aws-cdk/aws-neptune/lib/props.ts deleted file mode 100644 index 9466fba657059..0000000000000 --- a/packages/@aws-cdk/aws-neptune/lib/props.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Duration } from '@aws-cdk/core'; -import { IParameterGroup } from './parameter-group'; - -/** - * Possible Instances Types to use in Neptune cluster - * used for defining {@link InstanceProps.instanceType}. - */ -export enum InstanceType { - /** - * db.r5.large - */ - R5_LARGE = 'db.r5.large', - /** - * db.r5.xlarge - */ - R5_XLARGE = 'db.r5.xlarge', - /** - * db.r5.2xlarge - */ - R5_2XLARGE = 'db.r5.2xlarge', - /** - * db.r5.4xlarge - */ - R5_4XLARGE = 'db.r5.4xlarge', - /** - * db.r5.8xlarge - */ - R5_8XLARGE = 'db.r5.8xlarge', - /** - * db.r5.12xlarge - */ - R5_12XLARGE = 'db.r5.12xlarge', - /** - * db.r5.24xlarge - */ - R5_24XLARGE = 'db.r5.24xlarge', - /** - * db.r4.large - */ - R4_LARGE = 'db.r4.large', - /** - * db.r4.xlarge - */ - R4_XLARGE = 'db.r4.xlarge', - /** - * db.r4.2xlarge - */ - R4_2XLARGE = 'db.r4.2xlarge', - /** - * db.r4.4xlarge - */ - R4_4XLARGE = 'db.r4.4xlarge', - /** - * db.r4.8xlarge - */ - R4_8XLARGE = 'db.r4.8xlarge', - /** - * db.t3.medium - */ - T3_MEDIUM = 'db.t3.medium' -} - -/** - * Backup configuration for Neptune databases - * - * @default - The retention period for automated backups is 1 day. - * The preferred backup window will be a 30-minute window selected at random - * from an 8-hour block of time for each AWS Region. - */ -export interface BackupProps { - - /** - * How many days to retain the backup - */ - readonly retention: Duration; - - /** - * A daily time range in 24-hours UTC format in which backups preferably execute. - * - * Must be at least 30 minutes long. - * - * Example: '01:00-02:00' - * - * @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 - */ - readonly preferredWindow?: string; -} - -/** - * Instance properties for Neptune database instances - */ -export interface InstanceProps { - /** - * What type of instance to start for the replicas - */ - readonly instanceType: InstanceType; - - /** - * The DB parameter group to associate with the instance. - * - * @default no parameter group - */ - readonly parameterGroup?: IParameterGroup; -} - diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index abca6af4d4f99..3d374b6d2ef73 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -16,9 +16,7 @@ describe('DatabaseCluster', () => { // WHEN new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); // THEN @@ -55,9 +53,7 @@ describe('DatabaseCluster', () => { new DatabaseCluster(stack, 'Database', { instances: 1, vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); // THEN @@ -77,9 +73,7 @@ describe('DatabaseCluster', () => { new DatabaseCluster(stack, 'Database', { instances: 0, vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); }).toThrowError('At least one instance is required'); }); @@ -99,9 +93,7 @@ describe('DatabaseCluster', () => { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE, }, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); }).toThrowError('Cluster requires at least 2 subnets, got 1'); }); @@ -119,9 +111,7 @@ describe('DatabaseCluster', () => { instances: 1, vpc, securityGroups: [sg], - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); // THEN @@ -145,10 +135,8 @@ describe('DatabaseCluster', () => { }); new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, - parameterGroup: group, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: group, }); // THEN @@ -171,9 +159,7 @@ describe('DatabaseCluster', () => { new DatabaseCluster(stack, 'Database', { vpc, associatedRoles: [role], - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); // THEN @@ -200,9 +186,7 @@ describe('DatabaseCluster', () => { new DatabaseCluster(stack, 'Database', { vpc, iamAuthEnabled: true, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); // THEN @@ -221,10 +205,8 @@ describe('DatabaseCluster', () => { new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, - parameterGroup: group, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: group, }); // THEN @@ -241,9 +223,7 @@ describe('DatabaseCluster', () => { // WHEN new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, kmsKey: new kms.Key(stack, 'Key'), }); @@ -267,9 +247,7 @@ describe('DatabaseCluster', () => { // WHEN new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); // THEN @@ -287,9 +265,7 @@ describe('DatabaseCluster', () => { function action() { new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, kmsKey: new kms.Key(stack, 'Key'), storageEncrypted: false, }); @@ -307,9 +283,7 @@ describe('DatabaseCluster', () => { // WHEN const cluster = new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, }); // THEN @@ -325,9 +299,7 @@ describe('DatabaseCluster', () => { const instanceIdentifierBase = 'instanceidentifierbase-'; new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, instanceIdentifierBase, }); @@ -346,9 +318,7 @@ describe('DatabaseCluster', () => { const clusterIdentifier = 'clusteridentifier-'; new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, dbClusterName: clusterIdentifier, }); @@ -366,8 +336,6 @@ describe('DatabaseCluster', () => { const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { clusterEndpointAddress: 'addr', clusterIdentifier: 'identifier', - instanceEndpointAddresses: ['addr'], - instanceIdentifiers: ['identifier'], port: 3306, readerEndpointAddress: 'reader-address', securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { @@ -379,7 +347,6 @@ describe('DatabaseCluster', () => { expect(cluster.clusterEndpoint.hostname).toEqual('addr'); expect(cluster.clusterEndpoint.port).toEqual(3306); expect(cluster.clusterIdentifier).toEqual('identifier'); - expect(cluster.instanceIdentifiers).toEqual(['identifier']); expect(cluster.clusterReadEndpoint.hostname).toEqual('reader-address'); }); @@ -390,8 +357,6 @@ describe('DatabaseCluster', () => { const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { clusterEndpointAddress: 'addr', clusterIdentifier: 'identifier', - instanceEndpointAddresses: ['addr'], - instanceIdentifiers: ['identifier'], port: 3306, readerEndpointAddress: 'reader-address', securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { @@ -416,9 +381,7 @@ describe('DatabaseCluster', () => { // WHEN new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, backup: { retention: cdk.Duration.days(20), }, @@ -438,9 +401,7 @@ describe('DatabaseCluster', () => { // WHEN new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, backup: { retention: cdk.Duration.days(20), preferredWindow: '07:34-08:04', @@ -462,9 +423,7 @@ describe('DatabaseCluster', () => { // WHEN new DatabaseCluster(stack, 'Database', { vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, + instanceType: InstanceType.R5_LARGE, preferredMaintenanceWindow: '07:34-08:04', }); diff --git a/packages/@aws-cdk/aws-neptune/test/instance.test.ts b/packages/@aws-cdk/aws-neptune/test/instance.test.ts new file mode 100644 index 0000000000000..4dcfc75e243ac --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/instance.test.ts @@ -0,0 +1,131 @@ +import { expect as expectCDK, haveOutput, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; + +import { DatabaseCluster, DatabaseInstance, InstanceType, ParameterGroup } from '../lib'; + +describe('DatabaseInstance', () => { + test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + + // WHEN + new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Properties: { + DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, + DBInstanceClass: 'db.r5.large', + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + }); + + test('check that the endpoint works', () => { + // GIVEN + const stack = testStack(); + const instance = new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + }); + const exportName = 'DbInstanceEndpoint'; + + // WHEN + new cdk.CfnOutput(stack, exportName, { + exportName, + value: instance.instanceEndpoint.socketAddress, + }); + + // THEN + expectCDK(stack).to(haveOutput({ + exportName, + outputValue: { + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': ['InstanceC1063A87', 'Endpoint'] }, + ':', + { 'Fn::GetAtt': ['InstanceC1063A87', 'Port'] }, + ], + ], + }, + })); + }); + + test('check importing works as expected', () => { + // GIVEN + const stack = testStack(); + const endpointExportName = 'DbInstanceEndpoint'; + const instanceEndpointAddress = '127.0.0.1'; + const instanceIdentifier = 'InstanceID'; + const port = 8888; + + // WHEN + const instance = DatabaseInstance.fromDatabaseInstanceAttributes(stack, 'Instance', { + instanceEndpointAddress, + instanceIdentifier, + port, + }); + new cdk.CfnOutput(stack, 'EndpointOutput', { + exportName: endpointExportName, + value: instance.instanceEndpoint.socketAddress, + }); + + // THEN + expectCDK(stack).to(haveOutput({ + exportName: endpointExportName, + outputValue: `${instanceEndpointAddress}:${port}`, + })); + }); + + test('instance with parameter group', () => { + // GIVEN + const stack = testStack(); + + // WHEN + const group = new ParameterGroup(stack, 'Params', { + description: 'bye', + parameters: { + param: 'value', + }, + }); + new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + parameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + }); +}); + +class TestStack extends cdk.Stack { + public readonly vpc: ec2.Vpc; + public readonly cluster: DatabaseCluster; + + constructor(scope?: constructs.Construct, id?: string, props: cdk.StackProps = {}) { + super(scope, id, props); + + this.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + + this.vpc = new ec2.Vpc(this, 'VPC'); + this.cluster = new DatabaseCluster(this, 'Database', { + instanceType: InstanceType.R5_LARGE, + vpc: this.vpc, + }); + } +} + +function testStack() { + const stack = new TestStack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + return stack; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts index 4017023fbcfa5..b62c0d054a624 100644 --- a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts @@ -31,10 +31,8 @@ class TestStack extends cdk.Stack { const cluster = new DatabaseCluster(this, 'Database', { vpc, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - }, - parameterGroup: params, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: params, kmsKey, removalPolicy: cdk.RemovalPolicy.DESTROY, }); From e4ce1ce05ba0b7449e7272d93d7cc463236fecb0 Mon Sep 17 00:00:00 2001 From: Sainath Mallidi Date: Mon, 1 Feb 2021 19:42:39 -0800 Subject: [PATCH 3/5] Add more instructions to README and remove IAM auth related details --- packages/@aws-cdk/aws-neptune/README.md | 58 +++++++++++++++++-- packages/@aws-cdk/aws-neptune/lib/cluster.ts | 10 +--- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 18 ------ 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index 19c11e554f16e..401219a99e1db 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -27,14 +27,12 @@ To set up a Neptune database, define a `DatabaseCluster`. You must always launch ```ts const cluster = new DatabaseCluster(this, 'Database', { - vpc, - instanceProps: { - instanceType: InstanceType.R5_LARGE, - } + vpc, + instanceType: InstanceType.R5_LARGE }); ``` -Your cluster will be empty by default. +By default only writer instance is provisioned with this construct. ## Connecting @@ -51,3 +49,53 @@ attributes: ```ts const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` + +## Customizing parameters + +Neptune allows configuring database behavior by supplying custom parameter groups. For more details, refer to the +following link: + +```ts +const clusterParams = new ClusterParameterGroup(this, 'ClusterParams', { + description: 'Cluster parameter group', + parameters: { + neptune_enable_audit_log: '1' + }, +}); + +const dbParams = new ParameterGroup(this, 'DbParams', { + description: 'Db parameter group', + parameters: { + neptune_query_timeout: '120000' + }, +}); + +const cluster = new DatabaseCluster(this, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: clusterParams, + parameterGroup: dbParams, +}); +``` + +## Adding replicas + +`DatabaseCluster` allows launching replicas along with the writer instance. This can be specified using the `instanceCount` +attribute. + +```ts +const cluster = new DatabaseCluster(this, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + instances: 2 +}); +``` + +Additionally it is also possible to add replicas using `DatabaseInstance` for an existing cluster. + +```ts +const replica1 = new DatabaseInstance(this, 'Instance', { + cluster, + instanceType: InstanceType.R5_LARGE +}); +``` diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts index ded3a2a9dad1a..ca991bfd858e2 100644 --- a/packages/@aws-cdk/aws-neptune/lib/cluster.ts +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -113,13 +113,6 @@ export interface DatabaseClusterProps { */ readonly associatedRoles?: iam.IRole[]; - /** - * Indicates whether the DB cluster is IAM auth enabled. - * - * @default - false, connections to database don't require IAM auth - */ - readonly iamAuthEnabled?: boolean; - /** * Indicates whether the DB cluster should have deletion protection enabled. * @@ -336,7 +329,7 @@ export class DatabaseCluster extends DatabaseClusterBase { /** * The subnets used by the DB subnet group. */ - public readonly vpcSubnets?: ec2.SubnetSelection; + public readonly vpcSubnets: ec2.SubnetSelection; /** * The security groups used by DB cluster. @@ -409,7 +402,6 @@ export class DatabaseCluster extends DatabaseClusterBase { dbClusterParameterGroupName: props.clusterParameterGroup?.clusterParameterGroupName, deletionProtection: deletionProtection, associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined, - iamAuthEnabled: props.iamAuthEnabled, // Backup backupRetentionPeriod: props.backup?.retention?.toDays(), preferredBackupWindow: props.backup?.preferredWindow, diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index 3d374b6d2ef73..e116c1d3b5195 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -177,24 +177,6 @@ describe('DatabaseCluster', () => { })); }); - test('cluster with iam auth', () => { - // GIVEN - const stack = testStack(); - const vpc = new ec2.Vpc(stack, 'VPC'); - - // WHEN - new DatabaseCluster(stack, 'Database', { - vpc, - iamAuthEnabled: true, - instanceType: InstanceType.R5_LARGE, - }); - - // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { - IamAuthEnabled: true, - })); - }); - test('cluster with imported parameter group', () => { // GIVEN const stack = testStack(); From e57cdb2292c540e00d07a50b2f79e4fd9bbad24e Mon Sep 17 00:00:00 2001 From: Sainath Mallidi Date: Tue, 23 Feb 2021 16:42:29 -0800 Subject: [PATCH 4/5] Address code review feedback --- packages/@aws-cdk/aws-neptune/README.md | 33 ++++--- packages/@aws-cdk/aws-neptune/lib/cluster.ts | 97 +++++-------------- packages/@aws-cdk/aws-neptune/lib/instance.ts | 58 ++++------- .../aws-neptune/rosetta/default.ts-fixture | 14 +++ .../rosetta/with-cluster.ts-fixture | 19 ++++ .../@aws-cdk/aws-neptune/test/cluster.test.ts | 10 +- .../aws-neptune/test/endpoint.test.ts | 14 --- 7 files changed, 98 insertions(+), 147 deletions(-) create mode 100644 packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture create mode 100644 packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index 401219a99e1db..bc0bca012969c 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -21,14 +21,21 @@ +Amazon Neptune is a fast, reliable, fully managed graph database service that makes it easy to build and run applications that work with highly connected datasets. The core of Neptune is a purpose-built, high-performance graph database engine. This engine is optimized for storing billions of relationships and querying the graph with milliseconds latency. Neptune supports the popular graph query languages Apache TinkerPop Gremlin and W3C’s SPARQL, enabling you to build queries that efficiently navigate highly connected datasets. + +The `@aws-cdk/aws-neptune` package contains primitives for setting up Neptune database clusters and instances. + +```ts nofixture +import * as neptune from '@aws-cdk/aws-neptune'; + ## Starting a Neptune Database To set up a Neptune database, define a `DatabaseCluster`. You must always launch a database in a VPC. ```ts -const cluster = new DatabaseCluster(this, 'Database', { +const cluster = new neptune.DatabaseCluster(this, 'Database', { vpc, - instanceType: InstanceType.R5_LARGE + instanceType: neptune.InstanceType.R5_LARGE }); ``` @@ -39,14 +46,14 @@ By default only writer instance is provisioned with this construct. To control who can access the cluster, use the `.connections` attribute. Neptune databases have a default port, so you don't need to specify the port: -```ts +```ts fixture=with-cluster cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); ``` The endpoints to access your database cluster will be available as the `.clusterEndpoint` and `.clusterReadEndpoint` attributes: -```ts +```ts fixture=with-cluster const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` @@ -56,23 +63,23 @@ Neptune allows configuring database behavior by supplying custom parameter group following link: ```ts -const clusterParams = new ClusterParameterGroup(this, 'ClusterParams', { +const clusterParams = new neptune.ClusterParameterGroup(this, 'ClusterParams', { description: 'Cluster parameter group', parameters: { neptune_enable_audit_log: '1' }, }); -const dbParams = new ParameterGroup(this, 'DbParams', { +const dbParams = new neptune.ParameterGroup(this, 'DbParams', { description: 'Db parameter group', parameters: { neptune_query_timeout: '120000' }, }); -const cluster = new DatabaseCluster(this, 'Database', { +const cluster = new neptune.DatabaseCluster(this, 'Database', { vpc, - instanceType: InstanceType.R5_LARGE, + instanceType: neptune.InstanceType.R5_LARGE, clusterParameterGroup: clusterParams, parameterGroup: dbParams, }); @@ -84,18 +91,18 @@ const cluster = new DatabaseCluster(this, 'Database', { attribute. ```ts -const cluster = new DatabaseCluster(this, 'Database', { +const cluster = new neptune.DatabaseCluster(this, 'Database', { vpc, - instanceType: InstanceType.R5_LARGE, + instanceType: neptune.InstanceType.R5_LARGE, instances: 2 }); ``` Additionally it is also possible to add replicas using `DatabaseInstance` for an existing cluster. -```ts -const replica1 = new DatabaseInstance(this, 'Instance', { +```ts fixture=with-cluster +const replica1 = new neptune.DatabaseInstance(this, 'Instance', { cluster, - instanceType: InstanceType.R5_LARGE + instanceType: neptune.InstanceType.R5_LARGE }); ``` diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts index ca991bfd858e2..879ffb03128fa 100644 --- a/packages/@aws-cdk/aws-neptune/lib/cluster.ts +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -9,33 +9,6 @@ import { CfnDBCluster, CfnDBInstance } from './neptune.generated'; import { IClusterParameterGroup, IParameterGroup } from './parameter-group'; import { ISubnetGroup, SubnetGroup } from './subnet-group'; -/** - * Backup configuration for Neptune databases - * - * @default - The retention period for automated backups is 1 day. - * The preferred backup window will be a 30-minute window selected at random - * from an 8-hour block of time for each AWS Region. - */ -export interface BackupProps { - - /** - * How many days to retain the backup - */ - readonly retention: Duration; - - /** - * A daily time range in 24-hours UTC format in which backups preferably execute. - * - * Must be at least 30 minutes long. - * - * Example: '01:00-02:00' - * - * @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 - */ - readonly preferredWindow?: string; -} - /** * Properties for a new database cluster */ @@ -55,13 +28,23 @@ export interface DatabaseClusterProps { readonly port?: number; /** - * Backup settings + * How many days to retain the backup + * + * @default - Backup retention period for automated backups is 1 day + */ + readonly backupRetention?: Duration; + + /** + * A daily time range in 24-hours UTC format in which backups preferably execute. + * + * Must be at least 30 minutes long. + * + * Example: '01:00-02:00' * - * @default - Backup retention period for automated backups is 1 day. - * Backup preferred window is set to 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. + * @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 */ - readonly backup?: BackupProps; + readonly preferredBackupWindow?: string; /** * The KMS key for storage encryption. @@ -237,38 +220,12 @@ export interface DatabaseClusterAttributes { readonly readerEndpointAddress: string; } - -/** - * A new or imported clustered database. - */ -abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster { - /** - * Identifier of the cluster - */ - public abstract readonly clusterIdentifier: string; - - /** - * The endpoint to use for read/write operations - */ - public abstract readonly clusterEndpoint: Endpoint; - - /** - * Endpoint to use for load-balanced read-only operations. - */ - public abstract readonly clusterReadEndpoint: Endpoint; - - /** - * Access to the network connections - */ - public abstract readonly connections: ec2.Connections; -} - /** * Create a clustered database with a given number of instances. * * @resource AWS::Neptune::DBCluster */ -export class DatabaseCluster extends DatabaseClusterBase { +export class DatabaseCluster extends Resource implements IDatabaseCluster { /** * The default number of instances in the Neptune cluster if none are @@ -280,7 +237,7 @@ export class DatabaseCluster extends DatabaseClusterBase { * Import an existing DatabaseCluster from properties */ public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { - class Import extends DatabaseClusterBase implements IDatabaseCluster { + class Import extends Resource implements IDatabaseCluster { public readonly defaultPort = ec2.Port.tcp(attrs.port); public readonly connections = new ec2.Connections({ securityGroups: [attrs.securityGroup], @@ -331,11 +288,6 @@ export class DatabaseCluster extends DatabaseClusterBase { */ public readonly vpcSubnets: ec2.SubnetSelection; - /** - * The security groups used by DB cluster. - */ - protected readonly securityGroups: ec2.ISecurityGroup[]; - /** * Subnet group used by the DB */ @@ -355,9 +307,7 @@ export class DatabaseCluster extends DatabaseClusterBase { super(scope, id); this.vpc = props.vpc; - this.vpcSubnets = props.vpcSubnets ? props.vpcSubnets : { - subnetType: ec2.SubnetType.PRIVATE, - }; + this.vpcSubnets = props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE }; // Determine the subnet(s) to deploy the Neptune cluster to const { subnetIds, internetConnectivityEstablished } = this.vpc.selectSubnets(this.vpcSubnets); @@ -374,7 +324,7 @@ export class DatabaseCluster extends DatabaseClusterBase { removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined, }); - this.securityGroups = props.securityGroups ?? [ + const securityGroups = props.securityGroups ?? [ new ec2.SecurityGroup(this, 'SecurityGroup', { description: 'Neptune security group', vpc: this.vpc, @@ -388,7 +338,6 @@ export class DatabaseCluster extends DatabaseClusterBase { throw new Error('KMS key supplied but storageEncrypted is false'); } - // Deletion protection const deletionProtection = props.deletionProtection ?? (props.removalPolicy === RemovalPolicy.RETAIN ? true : undefined); // Create the Neptune cluster @@ -398,13 +347,13 @@ export class DatabaseCluster extends DatabaseClusterBase { dbClusterIdentifier: props.dbClusterName, dbSubnetGroupName: this.subnetGroup.subnetGroupName, port: props.port, - vpcSecurityGroupIds: this.securityGroups.map(sg => sg.securityGroupId), + vpcSecurityGroupIds: securityGroups.map(sg => sg.securityGroupId), dbClusterParameterGroupName: props.clusterParameterGroup?.clusterParameterGroupName, deletionProtection: deletionProtection, associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined, // Backup - backupRetentionPeriod: props.backup?.retention?.toDays(), - preferredBackupWindow: props.backup?.preferredWindow, + backupRetentionPeriod: props.backupRetention?.toDays(), + preferredBackupWindow: props.preferredBackupWindow, preferredMaintenanceWindow: props.preferredMaintenanceWindow, // Encryption kmsKeyId: props.kmsKey?.keyArn, @@ -457,7 +406,7 @@ export class DatabaseCluster extends DatabaseClusterBase { this.connections = new ec2.Connections({ defaultPort: ec2.Port.tcp(port), - securityGroups: this.securityGroups, + securityGroups: securityGroups, }); } } diff --git a/packages/@aws-cdk/aws-neptune/lib/instance.ts b/packages/@aws-cdk/aws-neptune/lib/instance.ts index 0ac394548ad43..8459c710577c8 100644 --- a/packages/@aws-cdk/aws-neptune/lib/instance.ts +++ b/packages/@aws-cdk/aws-neptune/lib/instance.ts @@ -8,7 +8,7 @@ import { IParameterGroup } from './parameter-group'; /** * Possible Instances Types to use in Neptune cluster - * used for defining {@link InstanceProps.instanceType}. + * used for defining {@link DatabaseInstanceProps.instanceType}. */ export enum InstanceType { /** @@ -114,43 +114,6 @@ export interface DatabaseInstanceAttributes { readonly port: number; } -/** - * A new or imported database instance. - */ -abstract class DatabaseInstanceBase extends cdk.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 defaultPort = ec2.Port.tcp(attrs.port); - 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); - } - - return new Import(scope, id); - } - - /** - * @inheritdoc - */ - public abstract readonly instanceIdentifier: string; - /** - * @inheritdoc - */ - public abstract readonly instanceEndpoint: Endpoint; - /** - * @inheritdoc - */ - public abstract readonly dbInstanceEndpointAddress: string; - /** - * @inheritdoc - */ - public abstract readonly dbInstanceEndpointPort: string; -} - /** * Construction properties for a DatabaseInstanceNew */ @@ -201,7 +164,24 @@ export interface DatabaseInstanceProps { * * @resource AWS::Neptune::DBInstance */ -export class DatabaseInstance extends DatabaseInstanceBase implements IDatabaseInstance { +export class DatabaseInstance extends cdk.Resource implements IDatabaseInstance { + + /** + * Import an existing database instance. + */ + public static fromDatabaseInstanceAttributes(scope: Construct, id: string, attrs: DatabaseInstanceAttributes): IDatabaseInstance { + class Import extends cdk.Resource implements IDatabaseInstance { + public readonly defaultPort = ec2.Port.tcp(attrs.port); + 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); + } + + return new Import(scope, id); + } + + /** * The instance's database cluster */ diff --git a/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..2e687290371fa --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as neptune from '@aws-cdk/aws-neptune'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 }); + + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture b/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture new file mode 100644 index 0000000000000..c638d8b4d04fa --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture @@ -0,0 +1,19 @@ +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as neptune from '@aws-cdk/aws-neptune'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 }); + + const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + }); + + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index e116c1d3b5195..acb26a335c4ab 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -364,9 +364,7 @@ describe('DatabaseCluster', () => { new DatabaseCluster(stack, 'Database', { vpc, instanceType: InstanceType.R5_LARGE, - backup: { - retention: cdk.Duration.days(20), - }, + backupRetention: cdk.Duration.days(20), }); // THEN @@ -384,10 +382,8 @@ describe('DatabaseCluster', () => { new DatabaseCluster(stack, 'Database', { vpc, instanceType: InstanceType.R5_LARGE, - backup: { - retention: cdk.Duration.days(20), - preferredWindow: '07:34-08:04', - }, + backupRetention: cdk.Duration.days(20), + preferredBackupWindow: '07:34-08:04', }); // THEN diff --git a/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts b/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts index acf22d45f385a..cd5bd17bd3af2 100644 --- a/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts @@ -1,20 +1,6 @@ import { Endpoint } from '../lib'; -// A numeric CDK token (see: https://docs.aws.amazon.com/cdk/latest/guide/tokens.html#tokens_number) -const CDK_NUMERIC_TOKEN = -1.8881545897087626e+289; - describe('Endpoint', () => { - test('accepts tokens for the port value', () => { - // GIVEN - const token = CDK_NUMERIC_TOKEN; - - // WHEN - const endpoint = new Endpoint('127.0.0.1', token); - - // THEN - expect(endpoint.port).toBe(token); - }); - test('accepts valid port string numbers', () => { // GIVEN for (const port of [1, 50, 65535]) { From 060be26467b1f38f16fa01554c28dc1412414077 Mon Sep 17 00:00:00 2001 From: Sainath Mallidi Date: Wed, 24 Feb 2021 23:37:35 -0800 Subject: [PATCH 5/5] Add enum-like class for engine version --- packages/@aws-cdk/aws-neptune/README.md | 1 + packages/@aws-cdk/aws-neptune/lib/cluster.ts | 51 +++++++++++++++++-- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 22 +++++++- .../test/integ.cluster.expected.json | 18 +------ 4 files changed, 71 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index bc0bca012969c..fc542acf1b3da 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -27,6 +27,7 @@ The `@aws-cdk/aws-neptune` package contains primitives for setting up Neptune da ```ts nofixture import * as neptune from '@aws-cdk/aws-neptune'; +``` ## Starting a Neptune Database diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts index 879ffb03128fa..4adbe2ce8ea04 100644 --- a/packages/@aws-cdk/aws-neptune/lib/cluster.ts +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -9,6 +9,51 @@ import { CfnDBCluster, CfnDBInstance } from './neptune.generated'; import { IClusterParameterGroup, IParameterGroup } from './parameter-group'; import { ISubnetGroup, SubnetGroup } from './subnet-group'; +/** + * Possible Instances Types to use in Neptune cluster + * used for defining {@link DatabaseClusterProps.engineVersion}. + */ +export class EngineVersion { + /** + * Neptune engine version 1.0.1.0 + */ + public static readonly V1_0_1_0 = new EngineVersion('1.0.1.0'); + /** + * Neptune engine version 1.0.1.1 + */ + public static readonly V1_0_1_1 = new EngineVersion('1.0.1.1'); + /** + * Neptune engine version 1.0.1.2 + */ + public static readonly V1_0_1_2 = new EngineVersion('1.0.1.2'); + /** + * Neptune engine version 1.0.2.1 + */ + public static readonly V1_0_2_1 = new EngineVersion('1.0.2.1'); + /** + * Neptune engine version 1.0.2.2 + */ + public static readonly V1_0_2_2 = new EngineVersion('1.0.2.2'); + /** + * Neptune engine version 1.0.3.0 + */ + public static readonly V1_0_3_0 = new EngineVersion('1.0.3.0'); + /** + * Neptune engine version 1.0.4.0 + */ + public static readonly V1_0_4_0 = new EngineVersion('1.0.4.0'); + /** + * Neptune engine version 1.0.4.1 + */ + public static readonly V1_0_4_1 = new EngineVersion('1.0.4.1'); + + /** + * Constructor for specifying a custom engine version + * @param version the engine version of Neptune + */ + public constructor(public readonly version: string) {} +} + /** * Properties for a new database cluster */ @@ -18,7 +63,7 @@ export interface DatabaseClusterProps { * * @default - The default engine version. */ - readonly engineVersion?: string; + readonly engineVersion?: EngineVersion; /** * The port the Neptune cluster will listen on @@ -30,7 +75,7 @@ export interface DatabaseClusterProps { /** * How many days to retain the backup * - * @default - Backup retention period for automated backups is 1 day + * @default - cdk.Duration.days(1) */ readonly backupRetention?: Duration; @@ -343,7 +388,7 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { // Create the Neptune cluster const cluster = new CfnDBCluster(this, 'Resource', { // Basic - engineVersion: props.engineVersion, + engineVersion: props.engineVersion?.version, dbClusterIdentifier: props.dbClusterName, dbSubnetGroupName: this.subnetGroup.subnetGroupName, port: props.port, diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index acb26a335c4ab..d2c5ff4b6c1ef 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -4,7 +4,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; -import { ClusterParameterGroup, DatabaseCluster, InstanceType } from '../lib'; +import { ClusterParameterGroup, DatabaseCluster, EngineVersion, InstanceType } from '../lib'; describe('DatabaseCluster', () => { @@ -98,6 +98,26 @@ describe('DatabaseCluster', () => { }).toThrowError('Cluster requires at least 2 subnets, got 1'); }); + test('can create a cluster with custom engine version', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + engineVersion: EngineVersion.V1_0_4_1, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + EngineVersion: '1.0.4.1', + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + })); + }); + test('can create a cluster with imported vpc and security group', () => { // GIVEN const stack = testStack(); diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json index 13dbc845318e0..823f7af2a5b45 100644 --- a/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json @@ -372,23 +372,7 @@ "KeyPolicy": { "Statement": [ { - "Action": [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion", - "kms:GenerateDataKey", - "kms:TagResource", - "kms:UntagResource" - ], + "Action": "kms:*", "Effect": "Allow", "Principal": { "AWS": {