Skip to content

Commit 6953e03

Browse files
Jimmy Gaussenmergify[bot]
authored andcommitted
feat(autoscaling): blockDevices property (#3622)
* feat(autoscaling): blockDevices property * Update auto-scaling-group.ts * fix: increase BlockDeviceVolume constructor visibility * chore: volume doc * fix: inverse noDevice -> mappingEnabled * chore: fix deleteOnTermination doc * chore: fix volumeType, EbsDeviceVolumeType doc * chore: fix iops doc * chore: fix volumeSize doc * chore: fix missing parentheses * chore: remove long @see link * Revert auto-scaling-group.ts * Revert "Revert auto-scaling-group.ts" * chore: JSDoc fix * fix: iops warning * move ebs error checking to blockDevices mapping * add @aws-cdk/cx-api devDependency
1 parent 16ad0a8 commit 6953e03

File tree

3 files changed

+360
-1
lines changed

3 files changed

+360
-1
lines changed

packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@ export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps {
208208
* @default A role will automatically be created, it can be accessed via the `role` property
209209
*/
210210
readonly role?: iam.IRole;
211+
212+
/**
213+
* Specifies how block devices are exposed to the instance. You can specify virtual devices and EBS volumes.
214+
*
215+
* Each instance that is launched has an associated root device volume,
216+
* either an Amazon EBS volume or an instance store volume.
217+
* You can use block device mappings to specify additional EBS volumes or
218+
* instance store volumes to attach to an instance when it is launched.
219+
*
220+
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
221+
*
222+
* @default - Uses the block device mapping of the AMI
223+
*/
224+
readonly blockDevices?: BlockDevice[];
211225
}
212226

213227
abstract class AutoScalingGroupBase extends Resource implements IAutoScalingGroup {
@@ -416,6 +430,29 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
416430
userData: userDataToken,
417431
associatePublicIpAddress: props.associatePublicIpAddress,
418432
spotPrice: props.spotPrice,
433+
blockDeviceMappings: (props.blockDevices !== undefined ? props.blockDevices.map<CfnLaunchConfiguration.BlockDeviceMappingProperty>(
434+
({deviceName, volume, mappingEnabled}) => {
435+
const {virtualName, ebsDevice: ebs} = volume;
436+
437+
if (ebs) {
438+
const {iops, volumeType} = ebs;
439+
440+
if (!iops) {
441+
if (volumeType === EbsDeviceVolumeType.IO1) {
442+
throw new Error('iops property is required with volumeType: EbsDeviceVolumeType.IO1');
443+
}
444+
} else if (volumeType !== EbsDeviceVolumeType.IO1) {
445+
this.node.addWarning('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1');
446+
}
447+
}
448+
449+
return {
450+
deviceName,
451+
ebs,
452+
virtualName,
453+
noDevice: mappingEnabled !== undefined ? !mappingEnabled : undefined,
454+
};
455+
}) : undefined),
419456
});
420457

421458
launchConfig.node.addDependency(this.role);
@@ -864,3 +901,159 @@ export interface MetricTargetTrackingProps extends BaseTargetTrackingProps {
864901
*/
865902
readonly targetValue: number;
866903
}
904+
905+
export interface BlockDevice {
906+
/**
907+
* The device name exposed to the EC2 instance
908+
*
909+
* @example '/dev/sdh', 'xvdh'
910+
*
911+
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html
912+
*/
913+
readonly deviceName: string;
914+
915+
/**
916+
* Defines the block device volume, to be either an Amazon EBS volume or an ephemeral instance store volume
917+
*
918+
* @example BlockDeviceVolume.ebs(15), BlockDeviceVolume.ephemeral(0)
919+
*
920+
*/
921+
readonly volume: BlockDeviceVolume;
922+
923+
/**
924+
* If false, the device mapping will be suppressed.
925+
* If set to false for the root device, the instance might fail the Amazon EC2 health check.
926+
* Amazon EC2 Auto Scaling launches a replacement instance if the instance fails the health check.
927+
*
928+
* @default true - device mapping is left untouched
929+
*/
930+
readonly mappingEnabled?: boolean;
931+
}
932+
933+
export interface EbsDeviceOptionsBase {
934+
/**
935+
* Indicates whether to delete the volume when the instance is terminated.
936+
*
937+
* @default - true for Amazon EC2 Auto Scaling, false otherwise (e.g. EBS)
938+
*/
939+
readonly deleteOnTermination?: boolean;
940+
941+
/**
942+
* The number of I/O operations per second (IOPS) to provision for the volume.
943+
*
944+
* Must only be set for {@link volumeType}: {@link EbsDeviceVolumeType.IO1}
945+
*
946+
* The maximum ratio of IOPS to volume size (in GiB) is 50:1, so for 5,000 provisioned IOPS,
947+
* you need at least 100 GiB storage on the volume.
948+
*
949+
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
950+
*/
951+
readonly iops?: number;
952+
953+
/**
954+
* The EBS volume type
955+
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
956+
*
957+
* @default {@link EbsDeviceVolumeType.GP2}
958+
*/
959+
readonly volumeType?: EbsDeviceVolumeType;
960+
}
961+
962+
export interface EbsDeviceOptions extends EbsDeviceOptionsBase {
963+
/**
964+
* Specifies whether the EBS volume is encrypted.
965+
* Encrypted EBS volumes can only be attached to instances that support Amazon EBS encryption
966+
*
967+
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#EBSEncryption_supported_instances
968+
*
969+
* @default false
970+
*/
971+
readonly encrypted?: boolean;
972+
}
973+
974+
export interface EbsDeviceSnapshotOptions extends EbsDeviceOptionsBase {
975+
/**
976+
* The volume size, in Gibibytes (GiB)
977+
*
978+
* If you specify volumeSize, it must be equal or greater than the size of the snapshot.
979+
*
980+
* @default - The snapshot size
981+
*/
982+
readonly volumeSize?: number;
983+
}
984+
985+
export interface EbsDeviceProps extends EbsDeviceOptionsBase, EbsDeviceSnapshotOptions {
986+
readonly snapshotId?: string;
987+
}
988+
989+
/**
990+
* Describes a block device mapping for an Auto Scaling group.
991+
*/
992+
export class BlockDeviceVolume {
993+
/**
994+
* Creates a new Elastic Block Storage device
995+
*
996+
* @param volumeSize The volume size, in Gibibytes (GiB)
997+
* @param options additional device options
998+
*/
999+
public static ebs(volumeSize: number, options: EbsDeviceOptions = {}): BlockDeviceVolume {
1000+
return new this({...options, volumeSize});
1001+
}
1002+
1003+
/**
1004+
* Creates a new Elastic Block Storage device from an existing snapshot
1005+
*
1006+
* @param snapshotId The snapshot ID of the volume to use
1007+
* @param options additional device options
1008+
*/
1009+
public static ebsFromSnapshot(snapshotId: string, options: EbsDeviceSnapshotOptions = {}): BlockDeviceVolume {
1010+
return new this({...options, snapshotId});
1011+
}
1012+
1013+
/**
1014+
* Creates a virtual, ephemeral device.
1015+
* The name will be in the form ephemeral{volumeIndex}.
1016+
*
1017+
* @param volumeIndex the volume index. Must be equal or greater than 0
1018+
*/
1019+
public static ephemeral(volumeIndex: number) {
1020+
if (volumeIndex < 0) {
1021+
throw new Error(`volumeIndex must be a number starting from 0, got "${volumeIndex}"`);
1022+
}
1023+
1024+
return new this(undefined, `ephemeral${volumeIndex}`);
1025+
}
1026+
1027+
protected constructor(public readonly ebsDevice?: EbsDeviceProps, public readonly virtualName?: string) {
1028+
}
1029+
}
1030+
1031+
/**
1032+
* Supported EBS volume types for {@link AutoScalingGroupProps.blockDevices}
1033+
*/
1034+
export enum EbsDeviceVolumeType {
1035+
/**
1036+
* Magnetic
1037+
*/
1038+
STANDARD = 'standard',
1039+
1040+
/**
1041+
* Provisioned IOPS SSD
1042+
*/
1043+
IO1 = 'io1',
1044+
1045+
/**
1046+
* General Purpose SSD
1047+
*/
1048+
GP2 = 'gp2',
1049+
1050+
/**
1051+
* Throughput Optimized HDD
1052+
*/
1053+
ST1 = 'st1',
1054+
1055+
/**
1056+
* Cold HDD
1057+
*/
1058+
SC1 = 'sc1',
1059+
}

packages/@aws-cdk/aws-autoscaling/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
},
6363
"license": "Apache-2.0",
6464
"devDependencies": {
65+
"@aws-cdk/cx-api": "^1.3.0",
6566
"@aws-cdk/assert": "^1.3.0",
6667
"cdk-build-tools": "file:../../../tools/cdk-build-tools",
6768
"cdk-integ-tools": "file:../../../tools/cdk-integ-tools",

packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { expect, haveResource, haveResourceLike, InspectionFailure, ResourcePart } from '@aws-cdk/assert';
1+
import {expect, haveResource, haveResourceLike, InspectionFailure, ResourcePart} from '@aws-cdk/assert';
22
import ec2 = require('@aws-cdk/aws-ec2');
33
import iam = require('@aws-cdk/aws-iam');
44
import cdk = require('@aws-cdk/core');
5+
import cxapi = require('@aws-cdk/cx-api');
56
import { Test } from 'nodeunit';
67
import autoscaling = require('../lib');
78

@@ -665,6 +666,170 @@ export = {
665666

666667
test.done();
667668
},
669+
670+
'can set blockDeviceMappings'(test: Test) {
671+
// GIVEN
672+
const stack = new cdk.Stack();
673+
const vpc = mockVpc(stack);
674+
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
675+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
676+
machineImage: new ec2.AmazonLinuxImage(),
677+
vpc,
678+
blockDevices: [{
679+
deviceName: 'ebs',
680+
mappingEnabled: true,
681+
volume: autoscaling.BlockDeviceVolume.ebs(15, {
682+
deleteOnTermination: true,
683+
encrypted: true,
684+
volumeType: autoscaling.EbsDeviceVolumeType.IO1,
685+
iops: 5000,
686+
})
687+
}, {
688+
deviceName: 'ebs-snapshot',
689+
mappingEnabled: false,
690+
volume: autoscaling.BlockDeviceVolume.ebsFromSnapshot('snapshot-id', {
691+
volumeSize: 500,
692+
deleteOnTermination: false,
693+
volumeType: autoscaling.EbsDeviceVolumeType.SC1,
694+
})
695+
}, {
696+
deviceName: 'ephemeral',
697+
volume: autoscaling.BlockDeviceVolume.ephemeral(0)
698+
}]
699+
});
700+
701+
// THEN
702+
expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", {
703+
BlockDeviceMappings: [
704+
{
705+
DeviceName: "ebs",
706+
Ebs: {
707+
DeleteOnTermination: true,
708+
Encrypted: true,
709+
Iops: 5000,
710+
VolumeSize: 15,
711+
VolumeType: "io1"
712+
},
713+
NoDevice: false
714+
},
715+
{
716+
DeviceName: "ebs-snapshot",
717+
Ebs: {
718+
DeleteOnTermination: false,
719+
SnapshotId: "snapshot-id",
720+
VolumeSize: 500,
721+
VolumeType: "sc1"
722+
},
723+
NoDevice: true
724+
},
725+
{
726+
DeviceName: "ephemeral",
727+
VirtualName: "ephemeral0"
728+
}
729+
]
730+
}));
731+
732+
test.done();
733+
},
734+
735+
'throws if ephemeral volumeIndex < 0'(test: Test) {
736+
// GIVEN
737+
const stack = new cdk.Stack();
738+
const vpc = mockVpc(stack);
739+
740+
// THEN
741+
test.throws(() => {
742+
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
743+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
744+
machineImage: new ec2.AmazonLinuxImage(),
745+
vpc,
746+
blockDevices: [{
747+
deviceName: 'ephemeral',
748+
volume: autoscaling.BlockDeviceVolume.ephemeral(-1)
749+
}]
750+
});
751+
}, /volumeIndex must be a number starting from 0/);
752+
753+
test.done();
754+
},
755+
756+
'throws if volumeType === IO1 without iops'(test: Test) {
757+
// GIVEN
758+
const stack = new cdk.Stack();
759+
const vpc = mockVpc(stack);
760+
761+
// THEN
762+
test.throws(() => {
763+
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
764+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
765+
machineImage: new ec2.AmazonLinuxImage(),
766+
vpc,
767+
blockDevices: [{
768+
deviceName: 'ebs',
769+
volume: autoscaling.BlockDeviceVolume.ebs(15, {
770+
deleteOnTermination: true,
771+
encrypted: true,
772+
volumeType: autoscaling.EbsDeviceVolumeType.IO1,
773+
})
774+
}]
775+
});
776+
}, /ops property is required with volumeType: EbsDeviceVolumeType.IO1/);
777+
778+
test.done();
779+
},
780+
781+
'warning if iops without volumeType'(test: Test) {
782+
// GIVEN
783+
const stack = new cdk.Stack();
784+
const vpc = mockVpc(stack);
785+
786+
const asg = new autoscaling.AutoScalingGroup(stack, 'MyStack', {
787+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
788+
machineImage: new ec2.AmazonLinuxImage(),
789+
vpc,
790+
blockDevices: [{
791+
deviceName: 'ebs',
792+
volume: autoscaling.BlockDeviceVolume.ebs(15, {
793+
deleteOnTermination: true,
794+
encrypted: true,
795+
iops: 5000,
796+
})
797+
}]
798+
});
799+
800+
// THEN
801+
test.deepEqual(asg.node.metadata[0].type, cxapi.WARNING_METADATA_KEY);
802+
test.deepEqual(asg.node.metadata[0].data, 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1');
803+
804+
test.done();
805+
},
806+
807+
'warning if iops and volumeType !== IO1'(test: Test) {
808+
// GIVEN
809+
const stack = new cdk.Stack();
810+
const vpc = mockVpc(stack);
811+
812+
const asg = new autoscaling.AutoScalingGroup(stack, 'MyStack', {
813+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
814+
machineImage: new ec2.AmazonLinuxImage(),
815+
vpc,
816+
blockDevices: [{
817+
deviceName: 'ebs',
818+
volume: autoscaling.BlockDeviceVolume.ebs(15, {
819+
deleteOnTermination: true,
820+
encrypted: true,
821+
volumeType: autoscaling.EbsDeviceVolumeType.GP2,
822+
iops: 5000,
823+
})
824+
}]
825+
});
826+
827+
// THEN
828+
test.deepEqual(asg.node.metadata[0].type, cxapi.WARNING_METADATA_KEY);
829+
test.deepEqual(asg.node.metadata[0].data, 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1');
830+
831+
test.done();
832+
},
668833
};
669834

670835
function mockVpc(stack: cdk.Stack) {

0 commit comments

Comments
 (0)