From 440fe0e1e00da98ec8c7e42281165ef09ba60358 Mon Sep 17 00:00:00 2001 From: Sorabh Date: Tue, 1 Aug 2023 10:03:05 -0700 Subject: [PATCH] Provide support to configure new instance types like r5.4xl/8xl, r6g.4xl/8xl and i3 instances (#47) Signed-off-by: Sorabh Hamirwasia Co-authored-by: Sorabh Hamirwasia --- README.md | 1 + lib/infra/infra-stack.ts | 17 +++++------ lib/opensearch-config/node-config.ts | 41 +++++++++++++++++++++++++++ lib/os-cluster-entrypoint.ts | 17 ++++++++++- test/os-cluster.test.ts | 42 ++++++++++++++++++++++++++-- 5 files changed, 106 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 236aa1c9345..f635fc1b259 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ In order to deploy both the stacks the user needs to provide a set of required a | use50PercentHeap (Optional) | boolean | Boolean flag to use 50% of physical memory as heap. Default is 1GB. e.g., `--context use50PercentHeap=true` | | isInternal (Optional) | boolean | Boolean flag to make network load balancer internal. Default is internet-facing e.g., `--context isInternal=true` | | enableRemoteStore (Optional) | boolean | Boolean flag to enable Remote Store feature e.g., `--context enableRemoteStore=true`. See [Enable Remote Store Feature](#enable-remote-store-feature) for more details. | +| storageVolumeType (Optional) | string | EBS volume type for all the nodes (data, ml, cluster manager), defaults to gp2. See `lib/opensearch-config/node-config.ts` for available options. E.g., `-c storageVolumeType=gp3`. For SSD based instance (i.e. i3 family), it is used for root volume configuration. | diff --git a/lib/infra/infra-stack.ts b/lib/infra/infra-stack.ts index 4d2f693b27d..f9ee5a92866 100644 --- a/lib/infra/infra-stack.ts +++ b/lib/infra/infra-stack.ts @@ -22,7 +22,7 @@ import { SubnetType, } from 'aws-cdk-lib/aws-ec2'; import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; -import { AutoScalingGroup, BlockDeviceVolume, Signals } from 'aws-cdk-lib/aws-autoscaling'; +import { AutoScalingGroup, BlockDeviceVolume, EbsDeviceVolumeType, Signals } from 'aws-cdk-lib/aws-autoscaling'; import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { CfnOutput, RemovalPolicy, Stack, StackProps, Tags, @@ -60,7 +60,8 @@ export interface infraProps extends StackProps{ readonly mlEc2InstanceType: InstanceType, readonly use50PercentHeap: boolean, readonly isInternal: boolean, - readonly enableRemoteStore: boolean + readonly enableRemoteStore: boolean, + readonly storageVolumeType: EbsDeviceVolumeType } export class InfraStack extends Stack { @@ -144,7 +145,7 @@ export class InfraStack extends Stack { securityGroup: props.securityGroup, blockDevices: [{ deviceName: '/dev/xvda', - volume: BlockDeviceVolume.ebs(props.dataNodeStorage, { deleteOnTermination: true }), + volume: BlockDeviceVolume.ebs(props.dataNodeStorage, { deleteOnTermination: true, volumeType: props.storageVolumeType }), }], init: CloudFormationInit.fromElements(...InfraStack.getCfnInitElement(this, clusterLogGroup, props)), initOptions: { @@ -195,7 +196,7 @@ export class InfraStack extends Stack { securityGroup: props.securityGroup, blockDevices: [{ deviceName: '/dev/xvda', - volume: BlockDeviceVolume.ebs(50, { deleteOnTermination: true }), + volume: BlockDeviceVolume.ebs(50, { deleteOnTermination: true, volumeType: props.storageVolumeType }), }], init: CloudFormationInit.fromElements(...InfraStack.getCfnInitElement(this, clusterLogGroup, props, 'manager')), initOptions: { @@ -228,7 +229,7 @@ export class InfraStack extends Stack { blockDevices: [{ deviceName: '/dev/xvda', // eslint-disable-next-line max-len - volume: (seedConfig === 'seed-manager') ? BlockDeviceVolume.ebs(50, { deleteOnTermination: true }) : BlockDeviceVolume.ebs(props.dataNodeStorage, { deleteOnTermination: true }), + volume: (seedConfig === 'seed-manager') ? BlockDeviceVolume.ebs(50, { deleteOnTermination: true, volumeType: props.storageVolumeType }) : BlockDeviceVolume.ebs(props.dataNodeStorage, { deleteOnTermination: true, volumeType: props.storageVolumeType }), }], init: CloudFormationInit.fromElements(...InfraStack.getCfnInitElement(this, clusterLogGroup, props, seedConfig)), initOptions: { @@ -255,7 +256,7 @@ export class InfraStack extends Stack { securityGroup: props.securityGroup, blockDevices: [{ deviceName: '/dev/xvda', - volume: BlockDeviceVolume.ebs(props.dataNodeStorage, { deleteOnTermination: true }), + volume: BlockDeviceVolume.ebs(props.dataNodeStorage, { deleteOnTermination: true, volumeType: props.storageVolumeType }), }], init: CloudFormationInit.fromElements(...InfraStack.getCfnInitElement(this, clusterLogGroup, props, 'data')), initOptions: { @@ -285,7 +286,7 @@ export class InfraStack extends Stack { securityGroup: props.securityGroup, blockDevices: [{ deviceName: '/dev/xvda', - volume: BlockDeviceVolume.ebs(50, { deleteOnTermination: true }), + volume: BlockDeviceVolume.ebs(50, { deleteOnTermination: true, volumeType: props.storageVolumeType }), }], init: CloudFormationInit.fromElements(...InfraStack.getCfnInitElement(this, clusterLogGroup, props, 'client')), initOptions: { @@ -316,7 +317,7 @@ export class InfraStack extends Stack { securityGroup: props.securityGroup, blockDevices: [{ deviceName: '/dev/xvda', - volume: BlockDeviceVolume.ebs(props.mlNodeStorage, { deleteOnTermination: true }), + volume: BlockDeviceVolume.ebs(props.mlNodeStorage, { deleteOnTermination: true, volumeType: props.storageVolumeType }), }], init: CloudFormationInit.fromElements(...InfraStack.getCfnInitElement(this, clusterLogGroup, props, 'ml')), initOptions: { diff --git a/lib/opensearch-config/node-config.ts b/lib/opensearch-config/node-config.ts index cfc7c73980c..d192945eb74 100644 --- a/lib/opensearch-config/node-config.ts +++ b/lib/opensearch-config/node-config.ts @@ -6,6 +6,7 @@ this file be licensed under the Apache-2.0 license or a compatible open source license. */ import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2'; +import { EbsDeviceVolumeType } from 'aws-cdk-lib/aws-autoscaling'; export const nodeConfig = new Map(); @@ -45,8 +46,15 @@ export enum x64Ec2InstanceType { R5_LARGE = 'r5.large', R5_XLARGE = 'r5.xlarge', R5_2XLARGE = 'r5.2xlarge', + R5_4XLARGE = 'r5.4xlarge', + R5_8XLARGE = 'r5.8xlarge', G5_LARGE = 'g5.large', G5_XLARGE = 'g5.xlarge', + I3_LARGE = 'i3.large', + I3_XLARGE = 'i3.xlarge', + I3_2XLARGE = 'i3.2xlarge', + I3_4XLARGE = 'i3.4xlarge', + I3_8XLARGE = 'i3.8xlarge', INF1_XLARGE = 'inf1.xlarge', INF1_2XLARGE = 'inf1.2xlarge' } @@ -59,6 +67,8 @@ export enum arm64Ec2InstanceType { R6G_LARGE = 'r6g.large', R6G_XLARGE = 'r6g.xlarge', R6G_2XLARGE = 'r6g.2xlarge', + R6G_4XLARGE = 'r6g.4xlarge', + R6G_8XLARGE = 'r6g.8xlarge', G5G_LARGE = 'g5g.large', G5G_XLARGE = 'g5g.xlarge' } @@ -79,10 +89,24 @@ export const getX64InstanceTypes = (instanceType: string) => { return InstanceType.of(InstanceClass.R5, InstanceSize.XLARGE); case x64Ec2InstanceType.R5_2XLARGE: return InstanceType.of(InstanceClass.R5, InstanceSize.XLARGE2); + case x64Ec2InstanceType.R5_4XLARGE: + return InstanceType.of(InstanceClass.R5, InstanceSize.XLARGE4); + case x64Ec2InstanceType.R5_8XLARGE: + return InstanceType.of(InstanceClass.R5, InstanceSize.XLARGE8); case x64Ec2InstanceType.G5_LARGE: return InstanceType.of(InstanceClass.G5, InstanceSize.LARGE); case x64Ec2InstanceType.G5_XLARGE: return InstanceType.of(InstanceClass.G5, InstanceSize.XLARGE); + case x64Ec2InstanceType.I3_LARGE: + return InstanceType.of(InstanceClass.I3, InstanceSize.LARGE); + case x64Ec2InstanceType.I3_XLARGE: + return InstanceType.of(InstanceClass.I3, InstanceSize.XLARGE); + case x64Ec2InstanceType.I3_2XLARGE: + return InstanceType.of(InstanceClass.I3, InstanceSize.XLARGE2); + case x64Ec2InstanceType.I3_4XLARGE: + return InstanceType.of(InstanceClass.I3, InstanceSize.XLARGE4); + case x64Ec2InstanceType.I3_8XLARGE: + return InstanceType.of(InstanceClass.I3, InstanceSize.XLARGE8); case x64Ec2InstanceType.INF1_XLARGE: return InstanceType.of(InstanceClass.INF1, InstanceSize.XLARGE); case x64Ec2InstanceType.INF1_2XLARGE: @@ -108,6 +132,10 @@ export const getArm64InstanceTypes = (instanceType: string) => { return InstanceType.of(InstanceClass.R6G, InstanceSize.XLARGE); case arm64Ec2InstanceType.R6G_2XLARGE: return InstanceType.of(InstanceClass.R6G, InstanceSize.XLARGE2); + case arm64Ec2InstanceType.R6G_4XLARGE: + return InstanceType.of(InstanceClass.R6G, InstanceSize.XLARGE4); + case arm64Ec2InstanceType.R6G_8XLARGE: + return InstanceType.of(InstanceClass.R6G, InstanceSize.XLARGE8); case arm64Ec2InstanceType.G5G_LARGE: return InstanceType.of(InstanceClass.G5G, InstanceSize.LARGE); case arm64Ec2InstanceType.G5G_XLARGE: @@ -116,3 +144,16 @@ export const getArm64InstanceTypes = (instanceType: string) => { throw new Error(`Invalid instance type provided, please provide any one the following: ${Object.values(arm64Ec2InstanceType)}`); } }; + +export const getVolumeType = (volumeType: string) => { + switch (volumeType) { + case EbsDeviceVolumeType.STANDARD.valueOf(): + return EbsDeviceVolumeType.STANDARD; + case EbsDeviceVolumeType.GP2.valueOf(): + return EbsDeviceVolumeType.GP2; + case EbsDeviceVolumeType.GP3.valueOf(): + return EbsDeviceVolumeType.GP3; + default: + throw new Error('Invalid volume type provided, please provide any one of the following: standard, gp2, gp3'); + } +}; diff --git a/lib/os-cluster-entrypoint.ts b/lib/os-cluster-entrypoint.ts index 7ff541fd019..dc622cd37f3 100644 --- a/lib/os-cluster-entrypoint.ts +++ b/lib/os-cluster-entrypoint.ts @@ -11,10 +11,15 @@ import { AmazonLinuxCpuType, InstanceType, IVpc, SecurityGroup, Vpc, } from 'aws-cdk-lib/aws-ec2'; import { dump } from 'js-yaml'; +import { EbsDeviceVolumeType } from 'aws-cdk-lib/aws-autoscaling'; import { NetworkStack } from './networking/vpc-stack'; import { InfraStack } from './infra/infra-stack'; import { - x64Ec2InstanceType, arm64Ec2InstanceType, getX64InstanceTypes, getArm64InstanceTypes, + arm64Ec2InstanceType, + getArm64InstanceTypes, + getVolumeType, + getX64InstanceTypes, + x64Ec2InstanceType, } from './opensearch-config/node-config'; enum cpuArchEnum{ @@ -55,6 +60,7 @@ export class OsClusterEntrypoint { let ymlConfig: string = 'undefined'; let dataEc2InstanceType: InstanceType; let mlEc2InstanceType: InstanceType; + let volumeType: EbsDeviceVolumeType; const x64InstanceTypes: string[] = Object.keys(x64Ec2InstanceType); const arm64InstanceTypes: string[] = Object.keys(arm64Ec2InstanceType); @@ -155,6 +161,14 @@ export class OsClusterEntrypoint { dataNodeStorage = parseInt(dataSize, 10); } + const inputVolumeType = `${scope.node.tryGetContext('storageVolumeType')}`; + if (inputVolumeType.toString() === 'undefined') { + // use gp2 volume by default + volumeType = getVolumeType('gp2'); + } else { + volumeType = getVolumeType(inputVolumeType); + } + const mlSize = `${scope.node.tryGetContext('mlNodeStorage')}`; if (mlSize === 'undefined') { mlNodeStorage = 100; @@ -234,6 +248,7 @@ export class OsClusterEntrypoint { use50PercentHeap, isInternal, enableRemoteStore, + storageVolumeType: volumeType, ...props, }); diff --git a/test/os-cluster.test.ts b/test/os-cluster.test.ts index f4b5110014d..0b81625b88f 100644 --- a/test/os-cluster.test.ts +++ b/test/os-cluster.test.ts @@ -160,6 +160,7 @@ test('Test Resources with security enabled single-node cluster', () => { restrictServerAccessTo: 'pl-12345', dataNodeStorage: 200, isInternal: true, + storageVolumeType: 'gp3', }, }); @@ -188,6 +189,7 @@ test('Test Resources with security enabled single-node cluster', () => { { Ebs: { VolumeSize: 200, + VolumeType: 'gp3', }, }, ], @@ -227,7 +229,7 @@ test('Throw error on wrong cpu arch to instance mapping', () => { } catch (error) { expect(error).toBeInstanceOf(Error); // eslint-disable-next-line max-len - expect(error.message).toEqual('Invalid instance type provided, please provide any one the following: m6g.xlarge,m6g.2xlarge,c6g.large,c6g.xlarge,r6g.large,r6g.xlarge,r6g.2xlarge,g5g.large,g5g.xlarge'); + expect(error.message).toEqual('Invalid instance type provided, please provide any one the following: m6g.xlarge,m6g.2xlarge,c6g.large,c6g.xlarge,r6g.large,r6g.xlarge,r6g.2xlarge,r6g.4xlarge,r6g.8xlarge,g5g.large,g5g.xlarge'); } }); @@ -245,7 +247,7 @@ test('Throw error on ec2 instance outside of enum list', () => { restrictServerAccessTo: 'pl-12345', dataNodeStorage: 200, isInternal: true, - dataInstanceType: 'r5.4xlarge', + dataInstanceType: 'r5.16xlarge', }, }); // WHEN @@ -259,7 +261,7 @@ test('Throw error on ec2 instance outside of enum list', () => { } catch (error) { expect(error).toBeInstanceOf(Error); // eslint-disable-next-line max-len - expect(error.message).toEqual('Invalid instance type provided, please provide any one the following: m5.xlarge,m5.2xlarge,c5.large,c5.xlarge,r5.large,r5.xlarge,r5.2xlarge,g5.large,g5.xlarge,inf1.xlarge,inf1.2xlarge'); + expect(error.message).toEqual('Invalid instance type provided, please provide any one the following: m5.xlarge,m5.2xlarge,c5.large,c5.xlarge,r5.large,r5.xlarge,r5.2xlarge,r5.4xlarge,r5.8xlarge,g5.large,g5.xlarge,i3.large,i3.xlarge,i3.2xlarge,i3.4xlarge,i3.8xlarge,inf1.xlarge,inf1.2xlarge'); } }); @@ -300,6 +302,7 @@ test('Test multi-node cluster with only data-nodes', () => { { Ebs: { VolumeSize: 200, + VolumeType: 'gp2', }, }, ], @@ -394,3 +397,36 @@ test('Test multi-node cluster with remote-store enabled', () => { }, }); }); + +test('Throw error on unsupported ebs volume type', () => { + const app = new App({ + context: { + securityDisabled: false, + minDistribution: false, + distributionUrl: 'www.example.com', + cpuArch: 'x64', + singleNodeCluster: false, + dashboardsUrl: 'www.example.com', + distVersion: '1.0.0', + serverAccessType: 'prefixList', + restrictServerAccessTo: 'pl-12345', + dataNodeStorage: 200, + isInternal: true, + dataInstanceType: 'r5.4xlarge', + storageVolumeType: 'io1', + }, + }); + // WHEN + try { + const testStack = new OsClusterEntrypoint(app, { + env: { account: 'test-account', region: 'us-east-1' }, + }); + + // eslint-disable-next-line no-undef + fail('Expected an error to be thrown'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // eslint-disable-next-line max-len + expect(error.message).toEqual('Invalid volume type provided, please provide any one of the following: standard, gp2, gp3'); + } +});