Skip to content

Commit

Permalink
feat(autoscaling): blockDevices property (#3622)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Jimmy Gaussen authored and mergify[bot] committed Aug 13, 2019
1 parent 16ad0a8 commit 6953e03
Show file tree
Hide file tree
Showing 3 changed files with 360 additions and 1 deletion.
193 changes: 193 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,20 @@ export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps {
* @default A role will automatically be created, it can be accessed via the `role` property
*/
readonly role?: iam.IRole;

/**
* Specifies how block devices are exposed to the instance. You can specify virtual devices and EBS volumes.
*
* Each instance that is launched has an associated root device volume,
* either an Amazon EBS volume or an instance store volume.
* You can use block device mappings to specify additional EBS volumes or
* instance store volumes to attach to an instance when it is launched.
*
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
*
* @default - Uses the block device mapping of the AMI
*/
readonly blockDevices?: BlockDevice[];
}

abstract class AutoScalingGroupBase extends Resource implements IAutoScalingGroup {
Expand Down Expand Up @@ -416,6 +430,29 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
userData: userDataToken,
associatePublicIpAddress: props.associatePublicIpAddress,
spotPrice: props.spotPrice,
blockDeviceMappings: (props.blockDevices !== undefined ? props.blockDevices.map<CfnLaunchConfiguration.BlockDeviceMappingProperty>(
({deviceName, volume, mappingEnabled}) => {
const {virtualName, ebsDevice: ebs} = volume;

if (ebs) {
const {iops, volumeType} = ebs;

if (!iops) {
if (volumeType === EbsDeviceVolumeType.IO1) {
throw new Error('iops property is required with volumeType: EbsDeviceVolumeType.IO1');
}
} else if (volumeType !== EbsDeviceVolumeType.IO1) {
this.node.addWarning('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1');
}
}

return {
deviceName,
ebs,
virtualName,
noDevice: mappingEnabled !== undefined ? !mappingEnabled : undefined,
};
}) : undefined),
});

launchConfig.node.addDependency(this.role);
Expand Down Expand Up @@ -864,3 +901,159 @@ export interface MetricTargetTrackingProps extends BaseTargetTrackingProps {
*/
readonly targetValue: number;
}

export interface BlockDevice {
/**
* The device name exposed to the EC2 instance
*
* @example '/dev/sdh', 'xvdh'
*
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html
*/
readonly deviceName: string;

/**
* Defines the block device volume, to be either an Amazon EBS volume or an ephemeral instance store volume
*
* @example BlockDeviceVolume.ebs(15), BlockDeviceVolume.ephemeral(0)
*
*/
readonly volume: BlockDeviceVolume;

/**
* If false, the device mapping will be suppressed.
* If set to false for the root device, the instance might fail the Amazon EC2 health check.
* Amazon EC2 Auto Scaling launches a replacement instance if the instance fails the health check.
*
* @default true - device mapping is left untouched
*/
readonly mappingEnabled?: boolean;
}

export interface EbsDeviceOptionsBase {
/**
* Indicates whether to delete the volume when the instance is terminated.
*
* @default - true for Amazon EC2 Auto Scaling, false otherwise (e.g. EBS)
*/
readonly deleteOnTermination?: boolean;

/**
* The number of I/O operations per second (IOPS) to provision for the volume.
*
* Must only be set for {@link volumeType}: {@link EbsDeviceVolumeType.IO1}
*
* The maximum ratio of IOPS to volume size (in GiB) is 50:1, so for 5,000 provisioned IOPS,
* you need at least 100 GiB storage on the volume.
*
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
*/
readonly iops?: number;

/**
* The EBS volume type
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
*
* @default {@link EbsDeviceVolumeType.GP2}
*/
readonly volumeType?: EbsDeviceVolumeType;
}

export interface EbsDeviceOptions extends EbsDeviceOptionsBase {
/**
* Specifies whether the EBS volume is encrypted.
* Encrypted EBS volumes can only be attached to instances that support Amazon EBS encryption
*
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#EBSEncryption_supported_instances
*
* @default false
*/
readonly encrypted?: boolean;
}

export interface EbsDeviceSnapshotOptions extends EbsDeviceOptionsBase {
/**
* The volume size, in Gibibytes (GiB)
*
* If you specify volumeSize, it must be equal or greater than the size of the snapshot.
*
* @default - The snapshot size
*/
readonly volumeSize?: number;
}

export interface EbsDeviceProps extends EbsDeviceOptionsBase, EbsDeviceSnapshotOptions {
readonly snapshotId?: string;
}

/**
* Describes a block device mapping for an Auto Scaling group.
*/
export class BlockDeviceVolume {
/**
* Creates a new Elastic Block Storage device
*
* @param volumeSize The volume size, in Gibibytes (GiB)
* @param options additional device options
*/
public static ebs(volumeSize: number, options: EbsDeviceOptions = {}): BlockDeviceVolume {
return new this({...options, volumeSize});
}

/**
* Creates a new Elastic Block Storage device from an existing snapshot
*
* @param snapshotId The snapshot ID of the volume to use
* @param options additional device options
*/
public static ebsFromSnapshot(snapshotId: string, options: EbsDeviceSnapshotOptions = {}): BlockDeviceVolume {
return new this({...options, snapshotId});
}

/**
* Creates a virtual, ephemeral device.
* The name will be in the form ephemeral{volumeIndex}.
*
* @param volumeIndex the volume index. Must be equal or greater than 0
*/
public static ephemeral(volumeIndex: number) {
if (volumeIndex < 0) {
throw new Error(`volumeIndex must be a number starting from 0, got "${volumeIndex}"`);
}

return new this(undefined, `ephemeral${volumeIndex}`);
}

protected constructor(public readonly ebsDevice?: EbsDeviceProps, public readonly virtualName?: string) {
}
}

/**
* Supported EBS volume types for {@link AutoScalingGroupProps.blockDevices}
*/
export enum EbsDeviceVolumeType {
/**
* Magnetic
*/
STANDARD = 'standard',

/**
* Provisioned IOPS SSD
*/
IO1 = 'io1',

/**
* General Purpose SSD
*/
GP2 = 'gp2',

/**
* Throughput Optimized HDD
*/
ST1 = 'st1',

/**
* Cold HDD
*/
SC1 = 'sc1',
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-autoscaling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
},
"license": "Apache-2.0",
"devDependencies": {
"@aws-cdk/cx-api": "^1.3.0",
"@aws-cdk/assert": "^1.3.0",
"cdk-build-tools": "file:../../../tools/cdk-build-tools",
"cdk-integ-tools": "file:../../../tools/cdk-integ-tools",
Expand Down
167 changes: 166 additions & 1 deletion packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { expect, haveResource, haveResourceLike, InspectionFailure, ResourcePart } from '@aws-cdk/assert';
import {expect, haveResource, haveResourceLike, InspectionFailure, ResourcePart} from '@aws-cdk/assert';
import ec2 = require('@aws-cdk/aws-ec2');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/core');
import cxapi = require('@aws-cdk/cx-api');
import { Test } from 'nodeunit';
import autoscaling = require('../lib');

Expand Down Expand Up @@ -665,6 +666,170 @@ export = {

test.done();
},

'can set blockDeviceMappings'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
blockDevices: [{
deviceName: 'ebs',
mappingEnabled: true,
volume: autoscaling.BlockDeviceVolume.ebs(15, {
deleteOnTermination: true,
encrypted: true,
volumeType: autoscaling.EbsDeviceVolumeType.IO1,
iops: 5000,
})
}, {
deviceName: 'ebs-snapshot',
mappingEnabled: false,
volume: autoscaling.BlockDeviceVolume.ebsFromSnapshot('snapshot-id', {
volumeSize: 500,
deleteOnTermination: false,
volumeType: autoscaling.EbsDeviceVolumeType.SC1,
})
}, {
deviceName: 'ephemeral',
volume: autoscaling.BlockDeviceVolume.ephemeral(0)
}]
});

// THEN
expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", {
BlockDeviceMappings: [
{
DeviceName: "ebs",
Ebs: {
DeleteOnTermination: true,
Encrypted: true,
Iops: 5000,
VolumeSize: 15,
VolumeType: "io1"
},
NoDevice: false
},
{
DeviceName: "ebs-snapshot",
Ebs: {
DeleteOnTermination: false,
SnapshotId: "snapshot-id",
VolumeSize: 500,
VolumeType: "sc1"
},
NoDevice: true
},
{
DeviceName: "ephemeral",
VirtualName: "ephemeral0"
}
]
}));

test.done();
},

'throws if ephemeral volumeIndex < 0'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);

// THEN
test.throws(() => {
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
blockDevices: [{
deviceName: 'ephemeral',
volume: autoscaling.BlockDeviceVolume.ephemeral(-1)
}]
});
}, /volumeIndex must be a number starting from 0/);

test.done();
},

'throws if volumeType === IO1 without iops'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);

// THEN
test.throws(() => {
new autoscaling.AutoScalingGroup(stack, 'MyStack', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
blockDevices: [{
deviceName: 'ebs',
volume: autoscaling.BlockDeviceVolume.ebs(15, {
deleteOnTermination: true,
encrypted: true,
volumeType: autoscaling.EbsDeviceVolumeType.IO1,
})
}]
});
}, /ops property is required with volumeType: EbsDeviceVolumeType.IO1/);

test.done();
},

'warning if iops without volumeType'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);

const asg = new autoscaling.AutoScalingGroup(stack, 'MyStack', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
blockDevices: [{
deviceName: 'ebs',
volume: autoscaling.BlockDeviceVolume.ebs(15, {
deleteOnTermination: true,
encrypted: true,
iops: 5000,
})
}]
});

// THEN
test.deepEqual(asg.node.metadata[0].type, cxapi.WARNING_METADATA_KEY);
test.deepEqual(asg.node.metadata[0].data, 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1');

test.done();
},

'warning if iops and volumeType !== IO1'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);

const asg = new autoscaling.AutoScalingGroup(stack, 'MyStack', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage(),
vpc,
blockDevices: [{
deviceName: 'ebs',
volume: autoscaling.BlockDeviceVolume.ebs(15, {
deleteOnTermination: true,
encrypted: true,
volumeType: autoscaling.EbsDeviceVolumeType.GP2,
iops: 5000,
})
}]
});

// THEN
test.deepEqual(asg.node.metadata[0].type, cxapi.WARNING_METADATA_KEY);
test.deepEqual(asg.node.metadata[0].data, 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1');

test.done();
},
};

function mockVpc(stack: cdk.Stack) {
Expand Down

0 comments on commit 6953e03

Please sign in to comment.