From d20baad01965ce02eeb59580b0e1afc884ad22f9 Mon Sep 17 00:00:00 2001 From: Sainath Mallidi Date: Wed, 27 Jan 2021 23:40:30 -0800 Subject: [PATCH] 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 | 389 +++++++++++++ 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 | 462 ++++++++++++++++ .../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, 2079 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..5d33837d2a653 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -0,0 +1,389 @@ +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 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, + // 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..13b8319ad6a8e --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -0,0 +1,462 @@ +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 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'); +});