Skip to content

Commit

Permalink
feat(autoscaling): support for throughput on GP3 volumes (#22441)
Browse files Browse the repository at this point in the history
Adds support for the `throughput` property on GP3 volumes in both autoscaling and EC2 packages since their volume implementations are separate per the comment https://github.com/aws/aws-cdk/blob/master/packages/@aws-cdk/aws-autoscaling/lib/volume.ts#L1.

Change includes unit test coverage, integration test coverage on the autoscaling changes, and validation of the inputq to throughput. I was on the fence about whether to include the validation for synth time checking since as far as I can tell validation is not performed consistently across the CDK at synth time. Happy to modify that behavior pending reviewer feedback.

It was not obvious to me at first pass where similar integration test coverage that was added to the autoscaling package could be added to the EC2 package, so if the reviewer would like to see integration tests for GP3 volumes on in the EC2 package, would you mind providing a hint as to where that test might fit in the existing test paradigm?

Happy to break this change into two PRs -- one for EC2 and one for autoscaling.

closes: #16213

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
csumpter committed Nov 25, 2022
1 parent 47943d2 commit d13b64a
Show file tree
Hide file tree
Showing 15 changed files with 654 additions and 30 deletions.
30 changes: 30 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/README.md
Expand Up @@ -278,6 +278,36 @@ autoScalingGroup.scaleOnSchedule('AllowDownscalingAtNight', {
});
```

### Block Devices

This type specifies how block devices are exposed to the instance. You can specify virtual devices and EBS volumes.

#### GP3 Volumes

You can only specify the `throughput` on GP3 volumes.

```ts
declare const vpc: ec2.Vpc;
declare const instanceType: ec2.InstanceType;
declare const machineImage: ec2.IMachineImage;

const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'ASG', {
vpc,
instanceType,
machineImage,
blockDevices: [{
{
deviceName: 'gp3-volume',
volume: autoscaling.BlockDeviceVolume.ebs(15, {
volumeType: autoscaling.EbsDeviceVolumeType.GP3,
throughput: 125,
}),
},
}],
// ...
});
```

## Configuring Instances using CloudFormation Init

It is possible to use the CloudFormation Init mechanism to configure the
Expand Down
26 changes: 25 additions & 1 deletion packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts
Expand Up @@ -2167,7 +2167,31 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevices: Block
}

if (ebs) {
const { iops, volumeType } = ebs;
const { iops, volumeType, throughput } = 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) {
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/lib/volume.ts
Expand Up @@ -66,6 +66,14 @@ export interface EbsDeviceOptionsBase {
* @default {@link EbsDeviceVolumeType.GP2}
*/
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;
}

/**
Expand Down
79 changes: 79 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts
Expand Up @@ -699,6 +699,12 @@ describe('auto scaling group', () => {
}, {
deviceName: 'none',
volume: autoscaling.BlockDeviceVolume.noDevice(),
}, {
deviceName: 'gp3-with-throughput',
volume: autoscaling.BlockDeviceVolume.ebs(15, {
volumeType: autoscaling.EbsDeviceVolumeType.GP3,
throughput: 350,
}),
}],
});

Expand Down Expand Up @@ -739,6 +745,14 @@ describe('auto scaling group', () => {
DeviceName: 'none',
NoDevice: true,
},
{
DeviceName: 'gp3-with-throughput',
Ebs: {
VolumeSize: 15,
VolumeType: 'gp3',
Throughput: 350,
},
},
],
});
});
Expand Down Expand Up @@ -809,6 +823,71 @@ describe('auto scaling group', () => {
}).toThrow(/maxInstanceLifetime must be between 1 and 365 days \(inclusive\)/);
});

test.each([124, 1001])('throws if throughput is set less than 125 or more than 1000', (throughput) => {
const stack = new cdk.Stack();
const vpc = mockVpc(stack);

expect(() => {
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
machineImage: new ec2.AmazonLinuxImage(),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
vpc,
maxInstanceLifetime: cdk.Duration.days(0),
blockDevices: [{
deviceName: 'ebs',
volume: autoscaling.BlockDeviceVolume.ebs(15, {
volumeType: autoscaling.EbsDeviceVolumeType.GP3,
throughput,
}),
}],
});
}).toThrow(/throughput property takes a minimum of 125 and a maximum of 1000/);
});

test.each([
...Object.values(autoscaling.EbsDeviceVolumeType).filter((v) => v !== 'gp3'),
])('throws if throughput is set on any volume type other than GP3', (volumeType) => {
const stack = new cdk.Stack();
const vpc = mockVpc(stack);

expect(() => {
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
machineImage: new ec2.AmazonLinuxImage(),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
vpc,
maxInstanceLifetime: cdk.Duration.days(0),
blockDevices: [{
deviceName: 'ebs',
volume: autoscaling.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', () => {
const stack = new cdk.Stack();
const vpc = mockVpc(stack);
expect(() => {
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
machineImage: new ec2.AmazonLinuxImage(),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
vpc,
maxInstanceLifetime: cdk.Duration.days(0),
blockDevices: [{
deviceName: 'ebs',
volume: autoscaling.BlockDeviceVolume.ebs(15, {
volumeType: autoscaling.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('can configure instance monitoring', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down
@@ -1,15 +1,15 @@
{
"version": "20.0.0",
"version": "21.0.0",
"files": {
"cb1b7cc4cd8286ab836dcbb42f408e2c39d60c34a017d1dac4b998c1891d843b": {
"2ca8f144c3e288148d58c9b9e86c9034f6a72b09cecffac3a5d406f8f53d5b18": {
"source": {
"path": "aws-cdk-asg-integ.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "cb1b7cc4cd8286ab836dcbb42f408e2c39d60c34a017d1dac4b998c1891d843b.json",
"objectKey": "2ca8f144c3e288148d58c9b9e86c9034f6a72b09cecffac3a5d406f8f53d5b18.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
Expand Up @@ -625,6 +625,130 @@
"IgnoreUnmodifiedGroupSizeProperties": true
}
}
},
"AsgWithGp3BlockdeviceInstanceSecurityGroup54D76206": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "aws-cdk-asg-integ/AsgWithGp3Blockdevice/InstanceSecurityGroup",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1"
}
],
"Tags": [
{
"Key": "Name",
"Value": "aws-cdk-asg-integ/AsgWithGp3Blockdevice"
}
],
"VpcId": {
"Ref": "VPCB9E5F0B4"
}
}
},
"AsgWithGp3BlockdeviceInstanceRoleF52FB39B": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"Tags": [
{
"Key": "Name",
"Value": "aws-cdk-asg-integ/AsgWithGp3Blockdevice"
}
]
}
},
"AsgWithGp3BlockdeviceInstanceProfile2FC414A5": {
"Type": "AWS::IAM::InstanceProfile",
"Properties": {
"Roles": [
{
"Ref": "AsgWithGp3BlockdeviceInstanceRoleF52FB39B"
}
]
}
},
"AsgWithGp3BlockdeviceLaunchConfig24411F5E": {
"Type": "AWS::AutoScaling::LaunchConfiguration",
"Properties": {
"ImageId": {
"Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter"
},
"InstanceType": "t3.micro",
"BlockDeviceMappings": [
{
"DeviceName": "ebs",
"Ebs": {
"DeleteOnTermination": true,
"Encrypted": true,
"Throughput": 125,
"VolumeSize": 15,
"VolumeType": "gp3"
}
}
],
"IamInstanceProfile": {
"Ref": "AsgWithGp3BlockdeviceInstanceProfile2FC414A5"
},
"SecurityGroups": [
{
"Fn::GetAtt": [
"AsgWithGp3BlockdeviceInstanceSecurityGroup54D76206",
"GroupId"
]
}
],
"UserData": {
"Fn::Base64": "#!/bin/bash"
}
},
"DependsOn": [
"AsgWithGp3BlockdeviceInstanceRoleF52FB39B"
]
},
"AsgWithGp3BlockdeviceASGE82AA487": {
"Type": "AWS::AutoScaling::AutoScalingGroup",
"Properties": {
"MaxSize": "10",
"MinSize": "0",
"DesiredCapacity": "5",
"LaunchConfigurationName": {
"Ref": "AsgWithGp3BlockdeviceLaunchConfig24411F5E"
},
"Tags": [
{
"Key": "Name",
"PropagateAtLaunch": true,
"Value": "aws-cdk-asg-integ/AsgWithGp3Blockdevice"
}
],
"VPCZoneIdentifier": [
{
"Ref": "VPCPrivateSubnet1Subnet8BCA10E0"
},
{
"Ref": "VPCPrivateSubnet2SubnetCFCDAA7A"
}
]
},
"UpdatePolicy": {
"AutoScalingScheduledAction": {
"IgnoreUnmodifiedGroupSizeProperties": true
}
}
}
},
"Parameters": {
Expand All @@ -636,6 +760,10 @@
"Type": "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>",
"Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2"
},
"SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": {
"Type": "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>",
"Default": "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2"
},
"BootstrapVersion": {
"Type": "AWS::SSM::Parameter::Value<String>",
"Default": "/cdk-bootstrap/hnb659fds/version",
Expand Down
@@ -1 +1 @@
{"version":"20.0.0"}
{"version":"21.0.0"}
@@ -1,5 +1,5 @@
{
"version": "20.0.0",
"version": "21.0.0",
"testCases": {
"integ.asg-lt": {
"stacks": [
Expand Down

0 comments on commit d13b64a

Please sign in to comment.