From c0eeb83b2524cc43b50f6fb148f0c82c0c2fda03 Mon Sep 17 00:00:00 2001 From: Ian Kerins Date: Wed, 22 May 2024 22:02:53 -0400 Subject: [PATCH] feat(ec2): support throughput on LaunchTemplate EBS volumes This support was simply not included in ec2 when it was added to autoscaling in #22441. I have copied that PR's implementation implementation to ec2 and similarly adapted its tests. Fixes #24341. --- .../aws-cdk-ec2-lt-metadata-1.assets.json | 4 +- .../aws-cdk-ec2-lt-metadata-1.template.json | 10 ++++ .../manifest.json | 2 +- .../tree.json | 10 ++++ .../aws-ec2/test/integ.launch-template.ts | 7 +++ packages/aws-cdk-lib/aws-ec2/README.md | 9 ++- .../aws-ec2/lib/private/ebs-util.ts | 26 ++++++++- packages/aws-cdk-lib/aws-ec2/lib/volume.ts | 8 +++ .../aws-ec2/test/launch-template.test.ts | 56 +++++++++++++++++++ 9 files changed, 125 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/aws-cdk-ec2-lt-metadata-1.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/aws-cdk-ec2-lt-metadata-1.assets.json index 8001fbc9aa735..41a7d96d57705 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/aws-cdk-ec2-lt-metadata-1.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/aws-cdk-ec2-lt-metadata-1.assets.json @@ -14,7 +14,7 @@ } } }, - "264474739ebdfe766ec5c3a9e9b351a2f4647da318e4f5848a0ad0aa00e295ed": { + "cc17875d03d1b5e7a15825f9c30066a372268b7c7d1870d971e91d6c25c8fd02": { "source": { "path": "aws-cdk-ec2-lt-metadata-1.template.json", "packaging": "file" @@ -22,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "264474739ebdfe766ec5c3a9e9b351a2f4647da318e4f5848a0ad0aa00e295ed.json", + "objectKey": "cc17875d03d1b5e7a15825f9c30066a372268b7c7d1870d971e91d6c25c8fd02.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/aws-cdk-ec2-lt-metadata-1.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/aws-cdk-ec2-lt-metadata-1.template.json index 6cf4c834cc5a3..b9dea70d2c2cb 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/aws-cdk-ec2-lt-metadata-1.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/aws-cdk-ec2-lt-metadata-1.template.json @@ -159,6 +159,16 @@ "Type": "AWS::EC2::LaunchTemplate", "Properties": { "LaunchTemplateData": { + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "Throughput": 250, + "VolumeSize": 15, + "VolumeType": "gp3" + } + } + ], "MetadataOptions": { "HttpEndpoint": "enabled", "HttpProtocolIpv6": "enabled", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/manifest.json index 454dad9938fab..4f1c638c36073 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/manifest.json @@ -18,7 +18,7 @@ "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}/264474739ebdfe766ec5c3a9e9b351a2f4647da318e4f5848a0ad0aa00e295ed.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/cc17875d03d1b5e7a15825f9c30066a372268b7c7d1870d971e91d6c25c8fd02.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/tree.json index 0475a6ce3a9fe..c47b3b78078ea 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.js.snapshot/tree.json @@ -146,6 +146,16 @@ "aws:cdk:cloudformation:type": "AWS::EC2::LaunchTemplate", "aws:cdk:cloudformation:props": { "launchTemplateData": { + "blockDeviceMappings": [ + { + "deviceName": "/dev/xvda", + "ebs": { + "volumeSize": 15, + "throughput": 250, + "volumeType": "gp3" + } + } + ], "securityGroupIds": [ { "Fn::GetAtt": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.ts index 39d71b394b897..3a293d11772e1 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ec2/test/integ.launch-template.ts @@ -22,6 +22,13 @@ const lt = new ec2.LaunchTemplate(stack, 'LT', { httpTokens: ec2.LaunchTemplateHttpTokens.REQUIRED, instanceMetadataTags: true, securityGroup: sg1, + blockDevices: [{ + deviceName: '/dev/xvda', + volume: ec2.BlockDeviceVolume.ebs(15, { + volumeType: ec2.EbsDeviceVolumeType.GP3, + throughput: 250, + }), + }], }); const sg2 = new ec2.SecurityGroup(stack, 'sg2', { diff --git a/packages/aws-cdk-lib/aws-ec2/README.md b/packages/aws-cdk-lib/aws-ec2/README.md index 5d10fd95d7e5f..5b16541cbeeba 100644 --- a/packages/aws-cdk-lib/aws-ec2/README.md +++ b/packages/aws-cdk-lib/aws-ec2/README.md @@ -1543,8 +1543,8 @@ new ec2.Instance(this, 'Instance', { ### Block Devices To add EBS block device mappings, specify the `blockDevices` property. The following example sets the EBS-backed -root device (`/dev/sda1`) size to 50 GiB, and adds another EBS-backed device mapped to `/dev/sdm` that is 100 GiB in -size: +root device (`/dev/sda1`) size to 50 GiB, and adds another EBS-backed GP3 device mapped to `/dev/sdm` that is 100 GiB in +size, with the GP3-specific `throughput` option set to 250 MiB/s: ```ts declare const vpc: ec2.Vpc; @@ -1565,7 +1565,10 @@ new ec2.Instance(this, 'Instance', { }, { deviceName: '/dev/sdm', - volume: ec2.BlockDeviceVolume.ebs(100), + volume: ec2.BlockDeviceVolume.ebs(100, { + volumeType: ec2.EbsDeviceVolumeType.GP3, + throughput: 250, + }), }, ], }); diff --git a/packages/aws-cdk-lib/aws-ec2/lib/private/ebs-util.ts b/packages/aws-cdk-lib/aws-ec2/lib/private/ebs-util.ts index 51a020fa1036f..6586f657d83c8 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/private/ebs-util.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/private/ebs-util.ts @@ -25,7 +25,30 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevic if (ebs) { - const { iops, volumeType, kmsKey, ...rest } = ebs; + const { iops, throughput, volumeType, kmsKey, ...rest } = ebs; + + if (throughput) { + const throughputRange = { Min: 125, Max: 1000 }; + const { Min, Max } = throughputRange; + + if (volumeType != EbsDeviceVolumeType.GP3) { + throw new Error('throughput property requires volumeType: EbsDeviceVolumeType.GP3'); + } + + if (throughput < Min || throughput > Max) { + throw new Error( + `throughput property takes a minimum of ${Min} and a maximum of ${Max}`, + ); + } + + const maximumThroughputRatio = 0.25; + if (iops) { + const iopsRatio = (throughput / iops); + if (iopsRatio > maximumThroughputRatio) { + throw new Error(`Throughput (MiBps) to iops ratio of ${iopsRatio} is too high; maximum is ${maximumThroughputRatio} MiBps per iops`); + } + } + } if (!iops) { if (volumeType === EbsDeviceVolumeType.IO1 || volumeType === EbsDeviceVolumeType.IO2) { @@ -43,6 +66,7 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevic finalEbs = { ...rest, iops, + throughput, volumeType, kmsKeyId: kmsKey?.keyArn, }; diff --git a/packages/aws-cdk-lib/aws-ec2/lib/volume.ts b/packages/aws-cdk-lib/aws-ec2/lib/volume.ts index 70d19b119f717..73ffed0d5b1f8 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/volume.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/volume.ts @@ -70,6 +70,14 @@ export interface EbsDeviceOptionsBase { * `@aws-cdk/aws-ec2:ebsDefaultGp3Volume` is enabled. */ readonly volumeType?: EbsDeviceVolumeType; + + /** + * The throughput that the volume supports, in MiB/s + * Takes a minimum of 125 and maximum of 1000. + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html + * @default - 125 MiB/s. Only valid on gp3 volumes. + */ + readonly throughput?: number; } /** diff --git a/packages/aws-cdk-lib/aws-ec2/test/launch-template.test.ts b/packages/aws-cdk-lib/aws-ec2/test/launch-template.test.ts index 227eb1c1b0d84..932c0fa08fc4c 100644 --- a/packages/aws-cdk-lib/aws-ec2/test/launch-template.test.ts +++ b/packages/aws-cdk-lib/aws-ec2/test/launch-template.test.ts @@ -314,6 +314,12 @@ describe('LaunchTemplate', () => { }, { deviceName: 'ephemeral', volume: BlockDeviceVolume.ephemeral(0), + }, { + deviceName: 'gp3-with-throughput', + volume: BlockDeviceVolume.ebs(15, { + volumeType: EbsDeviceVolumeType.GP3, + throughput: 350, + }), }, ]; @@ -366,10 +372,60 @@ describe('LaunchTemplate', () => { DeviceName: 'ephemeral', VirtualName: 'ephemeral0', }, + { + DeviceName: 'gp3-with-throughput', + Ebs: { + VolumeSize: 15, + VolumeType: 'gp3', + Throughput: 350, + }, + }, ], }, }); }); + test.each([124, 1001])('throws if throughput is set less than 125 or more than 1000', (throughput) => { + expect(() => { + new LaunchTemplate(stack, 'LaunchTemplate', { + blockDevices: [{ + deviceName: 'ebs', + volume: BlockDeviceVolume.ebs(15, { + volumeType: EbsDeviceVolumeType.GP3, + throughput, + }), + }], + }); + }).toThrow(/throughput property takes a minimum of 125 and a maximum of 1000/); + }); + test.each([ + ...Object.values(EbsDeviceVolumeType).filter((v) => v !== 'gp3'), + ])('throws if throughput is set on any volume type other than GP3', (volumeType) => { + expect(() => { + new LaunchTemplate(stack, 'LaunchTemplate', { + blockDevices: [{ + deviceName: 'ebs', + volume: BlockDeviceVolume.ebs(15, { + volumeType: volumeType, + throughput: 150, + }), + }], + }); + }).toThrow(/throughput property requires volumeType: EbsDeviceVolumeType.GP3/); + }); + test('throws if throughput / iops ratio is greater than 0.25', () => { + expect(() => { + new LaunchTemplate(stack, 'LaunchTemplate', { + blockDevices: [{ + deviceName: 'ebs', + volume: BlockDeviceVolume.ebs(15, { + volumeType: EbsDeviceVolumeType.GP3, + throughput: 751, + iops: 3000, + }), + }], + }); + }).toThrow('Throughput (MiBps) to iops ratio of 0.25033333333333335 is too high; maximum is 0.25 MiBps per iops'); + }); test('Given instance profile', () => { // GIVEN