From 47df704bdb57373176272f806bb6f677c4d8bba1 Mon Sep 17 00:00:00 2001 From: Steven Tan Date: Wed, 23 Aug 2023 00:44:53 +1000 Subject: [PATCH] feat(opensearchservice): configuring gp3 throughput (#26172) The interface EbsOptions for the opensearchservices CDK construct is missing a provisioned throughput option for eg gp3 instance types. iops is there, but not throughput Closes #26137. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- ...efaultTestDeployAssert4E6713E1.assets.json | 19 ++ ...aultTestDeployAssert4E6713E1.template.json | 36 ++++ .../cdk-integ-opensearch-gp3.assets.json | 19 ++ .../cdk-integ-opensearch-gp3.template.json | 71 +++++++ .../integ.opensearch.gp3.js.snapshot/cdk.out | 1 + .../integ.json | 12 ++ .../manifest.json | 111 +++++++++++ .../tree.json | 150 +++++++++++++++ .../test/integ.opensearch.gp3.ts | 31 +++ .../aws-opensearchservice/README.md | 15 ++ .../aws-opensearchservice/lib/domain.ts | 93 ++++++++- .../aws-opensearchservice/test/domain.test.ts | 176 +++++++++++++++++- 12 files changed, 732 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/IntegDefaultTestDeployAssert4E6713E1.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/IntegDefaultTestDeployAssert4E6713E1.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk-integ-opensearch-gp3.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk-integ-opensearch-gp3.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/IntegDefaultTestDeployAssert4E6713E1.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/IntegDefaultTestDeployAssert4E6713E1.assets.json new file mode 100644 index 0000000000000..cb12f699bfe4a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/IntegDefaultTestDeployAssert4E6713E1.assets.json @@ -0,0 +1,19 @@ +{ + "version": "33.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "IntegDefaultTestDeployAssert4E6713E1.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/IntegDefaultTestDeployAssert4E6713E1.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/IntegDefaultTestDeployAssert4E6713E1.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/IntegDefaultTestDeployAssert4E6713E1.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk-integ-opensearch-gp3.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk-integ-opensearch-gp3.assets.json new file mode 100644 index 0000000000000..fd4931cecfbb1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk-integ-opensearch-gp3.assets.json @@ -0,0 +1,19 @@ +{ + "version": "33.0.0", + "files": { + "213b01de3bb16c293563a528b8be2db8d685bf9228b484f6c369e8ce3f9f3958": { + "source": { + "path": "cdk-integ-opensearch-gp3.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "213b01de3bb16c293563a528b8be2db8d685bf9228b484f6c369e8ce3f9f3958.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk-integ-opensearch-gp3.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk-integ-opensearch-gp3.template.json new file mode 100644 index 0000000000000..c07127843390a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk-integ-opensearch-gp3.template.json @@ -0,0 +1,71 @@ +{ + "Resources": { + "Domain66AC69E0": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "ClusterConfig": { + "DedicatedMasterEnabled": false, + "InstanceCount": 1, + "InstanceType": "r5.large.search", + "MultiAZWithStandbyEnabled": true, + "ZoneAwarenessEnabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": false, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "Iops": 3000, + "Throughput": 125, + "VolumeSize": 30, + "VolumeType": "gp3" + }, + "EncryptionAtRestOptions": { + "Enabled": false + }, + "EngineVersion": "OpenSearch_2.5", + "LogPublishingOptions": {}, + "NodeToNodeEncryptionOptions": { + "Enabled": false + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk.out new file mode 100644 index 0000000000000..560dae10d018f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"33.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/integ.json new file mode 100644 index 0000000000000..5a21b6845b50a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "33.0.0", + "testCases": { + "Integ/DefaultTest": { + "stacks": [ + "cdk-integ-opensearch-gp3" + ], + "assertionStack": "Integ/DefaultTest/DeployAssert", + "assertionStackName": "IntegDefaultTestDeployAssert4E6713E1" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/manifest.json new file mode 100644 index 0000000000000..6ab669045facc --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/manifest.json @@ -0,0 +1,111 @@ +{ + "version": "33.0.0", + "artifacts": { + "cdk-integ-opensearch-gp3.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "cdk-integ-opensearch-gp3.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "cdk-integ-opensearch-gp3": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "cdk-integ-opensearch-gp3.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/213b01de3bb16c293563a528b8be2db8d685bf9228b484f6c369e8ce3f9f3958.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "cdk-integ-opensearch-gp3.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "cdk-integ-opensearch-gp3.assets" + ], + "metadata": { + "/cdk-integ-opensearch-gp3/Domain/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Domain66AC69E0" + } + ], + "/cdk-integ-opensearch-gp3/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/cdk-integ-opensearch-gp3/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "cdk-integ-opensearch-gp3" + }, + "IntegDefaultTestDeployAssert4E6713E1.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "IntegDefaultTestDeployAssert4E6713E1.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "IntegDefaultTestDeployAssert4E6713E1": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "IntegDefaultTestDeployAssert4E6713E1.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "IntegDefaultTestDeployAssert4E6713E1.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "IntegDefaultTestDeployAssert4E6713E1.assets" + ], + "metadata": { + "/Integ/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/Integ/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "Integ/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/tree.json new file mode 100644 index 0000000000000..ca1e108b0f69a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.js.snapshot/tree.json @@ -0,0 +1,150 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "cdk-integ-opensearch-gp3": { + "id": "cdk-integ-opensearch-gp3", + "path": "cdk-integ-opensearch-gp3", + "children": { + "Domain": { + "id": "Domain", + "path": "cdk-integ-opensearch-gp3/Domain", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-integ-opensearch-gp3/Domain/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::OpenSearchService::Domain", + "aws:cdk:cloudformation:props": { + "clusterConfig": { + "dedicatedMasterEnabled": false, + "instanceCount": 1, + "instanceType": "r5.large.search", + "multiAzWithStandbyEnabled": true, + "zoneAwarenessEnabled": false + }, + "domainEndpointOptions": { + "enforceHttps": false, + "tlsSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "ebsOptions": { + "ebsEnabled": true, + "volumeSize": 30, + "volumeType": "gp3", + "iops": 3000, + "throughput": 125 + }, + "encryptionAtRestOptions": { + "enabled": false + }, + "engineVersion": "OpenSearch_2.5", + "logPublishingOptions": {}, + "nodeToNodeEncryptionOptions": { + "enabled": false + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_opensearchservice.CfnDomain", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_opensearchservice.Domain", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "cdk-integ-opensearch-gp3/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "cdk-integ-opensearch-gp3/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "Integ": { + "id": "Integ", + "path": "Integ", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "Integ/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "Integ/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.69" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "Integ/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "Integ/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "Integ/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.69" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.ts new file mode 100644 index 0000000000000..300930ab62604 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-opensearchservice/test/integ.opensearch.gp3.ts @@ -0,0 +1,31 @@ +import { App, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as opensearch from 'aws-cdk-lib/aws-opensearchservice'; +import { EbsDeviceVolumeType } from 'aws-cdk-lib/aws-ec2'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + // deploy the latest opensearch domain with minimal configuration + // required for gp3 volumes + const domainProps: opensearch.DomainProps = { + removalPolicy: RemovalPolicy.DESTROY, + version: opensearch.EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.GP3, + throughput: 125, + iops: 3000, + }, + }; + + new opensearch.Domain(this, 'Domain', domainProps); + } +} + +const app = new App(); +const stack = new TestStack(app, 'cdk-integ-opensearch-gp3'); + +new IntegTest(app, 'Integ', { testCases: [stack] }); diff --git a/packages/aws-cdk-lib/aws-opensearchservice/README.md b/packages/aws-cdk-lib/aws-opensearchservice/README.md index 23db698c2b8f7..bb5cf3f7f3bc6 100644 --- a/packages/aws-cdk-lib/aws-opensearchservice/README.md +++ b/packages/aws-cdk-lib/aws-opensearchservice/README.md @@ -22,6 +22,21 @@ const devDomain = new Domain(this, 'Domain', { }); ``` +Create a cluster with GP3 volumes: + +```ts +const gp3Domain = new Domain(this, 'Domain', { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: ec2.EbsDeviceVolumeType.GP3, + throughput: 125, + iops: 3000, + }, +}); +``` + + Create a production grade cluster by also specifying things like capacity and az distribution ```ts diff --git a/packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts b/packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts index 7c698d27b331b..8971cfc25e65c 100644 --- a/packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts +++ b/packages/aws-cdk-lib/aws-opensearchservice/lib/domain.ts @@ -130,13 +130,21 @@ export interface EbsOptions { /** * The number of I/O operations per second (IOPS) that the volume - * supports. This property applies only to the Provisioned IOPS (SSD) EBS + * supports. This property applies only to the gp3 and Provisioned IOPS (SSD) EBS * volume type. * * @default - iops are not set. */ readonly iops?: number; + /** + * The throughput (in MiB/s) of the EBS volumes attached to data nodes. + * This property applies only to the gp3 volume type. + * + * @default - throughput is not set. + */ + readonly throughput?: number; + /** * The size (in GiB) of the EBS volume for each data node. The minimum and * maximum size of an EBS volume depends on the EBS volume type and the @@ -1539,6 +1547,88 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { throw new Error('EBS volumes are required when using instance types other than r3, i3 or r6gd.'); } + // Only for a valid ebs volume configuration, per + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-opensearchservice-domain-ebsoptions.html + if (ebsEnabled) { + // Check if iops or throughput if general purpose is configured + if (volumeType == ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD || volumeType == ec2.EbsDeviceVolumeType.STANDARD) { + if (props.ebs?.iops !== undefined || props.ebs?.throughput !== undefined) { + throw new Error('General Purpose EBS volumes can not be used with Iops or Throughput configuration'); + } + } + + if ( + volumeType && + [ + ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + ].includes(volumeType) && + !props.ebs?.iops + ) { + throw new Error( + '`iops` must be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`.', + ); + } + if (props.ebs?.iops) { + if ( + ![ + ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + ].includes(volumeType) + ) { + throw new Error( + '`iops` may only be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`, `PROVISIONED_IOPS_SSD_IO2` or `GENERAL_PURPOSE_SSD_GP3`.', + ); + } + // Enforce minimum & maximum IOPS: + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html + const iopsRanges: { [key: string]: { Min: number, Max: number } } = {}; + iopsRanges[ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = { Min: 3000, Max: 16000 }; + iopsRanges[ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = { Min: 100, Max: 64000 }; + iopsRanges[ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = { Min: 100, Max: 64000 }; + const { Min, Max } = iopsRanges[volumeType]; + if (props.ebs?.iops < Min || props.ebs?.iops > Max) { + throw new Error(`\`${volumeType}\` volumes iops must be between ${Min} and ${Max}.`); + } + + // Enforce maximum ratio of IOPS/GiB: + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html + const maximumRatios: { [key: string]: number } = {}; + maximumRatios[ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = 500; + maximumRatios[ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = 50; + maximumRatios[ec2.EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = 500; + const maximumRatio = maximumRatios[volumeType]; + if (props.ebs?.volumeSize && (props.ebs?.iops > maximumRatio * props.ebs?.volumeSize)) { + throw new Error(`\`${volumeType}\` volumes iops has a maximum ratio of ${maximumRatio} IOPS/GiB.`); + } + + const maximumThroughputRatios: { [key: string]: number } = {}; + maximumThroughputRatios[ec2.EbsDeviceVolumeType.GP3] = 0.25; + const maximumThroughputRatio = maximumThroughputRatios[volumeType]; + if (props.ebs?.throughput && props.ebs?.iops) { + const iopsRatio = (props.ebs?.throughput / props.ebs?.iops); + if (iopsRatio > maximumThroughputRatio) { + throw new Error(`Throughput (MiBps) to iops ratio of ${iopsRatio} is too high; maximum is ${maximumThroughputRatio} MiBps per iops.`); + } + } + } + + if (props.ebs?.throughput) { + const throughputRange = { Min: 125, Max: 1000 }; + const { Min, Max } = throughputRange; + if (volumeType != ec2.EbsDeviceVolumeType.GP3) { + throw new Error( + '`throughput` property requires volumeType: `EbsDeviceVolumeType.GP3`', + ); + } + if (props.ebs?.throughput < Min || props.ebs?.throughput > Max) { + throw new Error( + `throughput property takes a minimum of ${Min} and a maximum of ${Max}.`, + ); + } + } + } + // Fine-grained access control requires node-to-node encryption, encryption at rest, // and enforced HTTPS. if (advancedSecurityEnabled) { @@ -1728,6 +1818,7 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable { volumeSize: ebsEnabled ? volumeSize : undefined, volumeType: ebsEnabled ? volumeType : undefined, iops: ebsEnabled ? props.ebs?.iops : undefined, + throughput: ebsEnabled ? props.ebs?.throughput : undefined, }, encryptionAtRestOptions: { enabled: encryptionAtRestEnabled, diff --git a/packages/aws-cdk-lib/aws-opensearchservice/test/domain.test.ts b/packages/aws-cdk-lib/aws-opensearchservice/test/domain.test.ts index b7afa0fb833a8..fb9057fed85f0 100644 --- a/packages/aws-cdk-lib/aws-opensearchservice/test/domain.test.ts +++ b/packages/aws-cdk-lib/aws-opensearchservice/test/domain.test.ts @@ -10,7 +10,7 @@ import * as logs from '../../aws-logs'; import * as route53 from '../../aws-route53'; import { App, Stack, Duration, SecretValue, CfnParameter, Token } from '../../core'; import * as cxapi from '../../cx-api'; -import { Domain, EngineVersion } from '../lib'; +import { Domain, DomainProps, EngineVersion } from '../lib'; let app: App; let stack: Stack; @@ -2278,6 +2278,180 @@ each(testedOpenSearchVersions).describe('offPeakWindow and softwareUpdateOptions }); }); +describe('EBS Options Configurations', () => { + + test('iops', () => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + iops: 500, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }, + }; + new Domain(stack, 'Domain', domainProps); + + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { + EBSOptions: { + VolumeSize: 30, + Iops: 500, + VolumeType: 'io1', + }, + }); + }); + + test('throughput', () => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + throughput: 125, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + }, + }; + new Domain(stack, 'Domain', domainProps); + + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { + EBSOptions: { + VolumeSize: 30, + Throughput: 125, + VolumeType: 'gp3', + }, + }); + }); + + test('throughput and iops', () => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + iops: 3000, + throughput: 125, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + }, + }; + new Domain(stack, 'Domain', domainProps); + + Template.fromStack(stack).hasResourceProperties('AWS::OpenSearchService::Domain', { + EBSOptions: { + VolumeSize: 30, + Iops: 3000, + Throughput: 125, + VolumeType: 'gp3', + }, + }); + }); + + test('validation required props', () => { + let idx: number = 0; + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('`iops` must be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`.'); + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + iops: 125, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('General Purpose EBS volumes can not be used with Iops or Throughput configuration'); + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + iops: 125, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('General Purpose EBS volumes can not be used with Iops or Throughput configuration'); + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + iops: 99, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('`io1` volumes iops must be between 100 and 64000.'); + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + iops: 64001, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('`io1` volumes iops must be between 100 and 64000.'); + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + iops: 16001, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('`gp3` volumes iops must be between 3000 and 16000.'); + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + iops: 2999, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('`gp3` volumes iops must be between 3000 and 16000.'); + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + throughput: 1024, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('throughput property takes a minimum of 125 and a maximum of 1000.'); + + expect(() => { + const domainProps: DomainProps = { + version: EngineVersion.OPENSEARCH_2_5, + ebs: { + volumeSize: 30, + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + throughput: 100, + }, + }; + new Domain(stack, `Domain${idx++}`, domainProps); + }).toThrow('throughput property takes a minimum of 125 and a maximum of 1000.'); + }); +}); + function testGrant( expectedActions: string[], invocation: (user: iam.IPrincipal, domain: Domain) => void,