Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(ec2): EC2 API cleanups #2954

Merged
merged 9 commits into from
Jun 21, 2019
Prev Previous commit
Next Next commit
Simplify MachineImage and OS handling
rix0rrr committed Jun 20, 2019
commit 948a48303826661bd95db24e1842609b4537a2c1
10 changes: 5 additions & 5 deletions packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts
Original file line number Diff line number Diff line change
@@ -175,7 +175,7 @@ export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps {
/**
* AMI to launch
*/
readonly machineImage: ec2.IMachineImageSource;
readonly machineImage: ec2.IMachineImage;

/**
* Specific UserData to use
@@ -399,13 +399,13 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
});

// use delayed evaluation
const machineImage = props.machineImage.getImage(this);
this.userData = machineImage.os.createDefaultUserData();
const imageConfig = props.machineImage.getImage(this);
this.userData = props.userData || imageConfig.userData || ec2.UserData.forOperatingSystem(imageConfig.osType);
const userDataToken = Lazy.stringValue({ produce: () => Fn.base64(this.userData.render()) });
const securityGroupsToken = Lazy.listValue({ produce: () => this.securityGroups.map(sg => sg.securityGroupId) });

const launchConfig = new CfnLaunchConfiguration(this, 'LaunchConfig', {
imageId: machineImage.imageId,
imageId: imageConfig.imageId,
keyName: props.keyName,
instanceType: props.instanceType.toString(),
securityGroups: securityGroupsToken,
@@ -456,7 +456,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
}

this.autoScalingGroup = new CfnAutoScalingGroup(this, 'ASG', asgProps);
this.osType = machineImage.os.type;
this.osType = imageConfig.osType;
this.autoScalingGroupName = this.autoScalingGroup.refAsString;
this.autoScalingGroupArn = Stack.of(this).formatArn({
service: 'autoscaling',
50 changes: 50 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts
Original file line number Diff line number Diff line change
@@ -155,6 +155,56 @@ export = {
test.done();
},

'userdata can be overriden by image'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);

const ud = ec2.UserData.forLinux();
ud.addCommands('it me!');

// WHEN
const asg = new autoscaling.AutoScalingGroup(stack, 'MyFleet', {
instanceType: ec2.InstanceType.pair(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage({
userData: ud
}),
vpc,
});

// THEN
test.equals(asg.userData.render(), '#!/bin/bash\nit me!');

test.done();
},

'userdata can be overriden at ASG directly'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const vpc = mockVpc(stack);

const ud1 = ec2.UserData.forLinux();
ud1.addCommands('it me!');

const ud2 = ec2.UserData.forLinux();
ud2.addCommands('no me!');

// WHEN
const asg = new autoscaling.AutoScalingGroup(stack, 'MyFleet', {
instanceType: ec2.InstanceType.pair(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage({
userData: ud1
}),
vpc,
userData: ud2
});

// THEN
test.equals(asg.userData.render(), '#!/bin/bash\nno me!');

test.done();
},

'can specify only min capacity'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
147 changes: 84 additions & 63 deletions packages/@aws-cdk/aws-ec2/lib/machine-image.ts
Original file line number Diff line number Diff line change
@@ -6,11 +6,45 @@ import { WindowsVersion } from './windows-versions';
/**
* Interface for classes that can select an appropriate machine image to use
*/
export interface IMachineImageSource {
export interface IMachineImage {
/**
* Return the image to use in the given context
*/
getImage(scope: Construct): MachineImage;
getImage(scope: Construct): MachineImageConfig;
}

/**
* Configuration for a machine image
*/
export interface MachineImageConfig {
/**
* The AMI ID of the image to use
*/
readonly imageId: string;

/**
* Operating system type for this image
*/
readonly osType: OperatingSystemType;

/**
* Initial UserData for this image
*
* @default - Default UserData appropriate for the osType is created
*/
readonly userData?: UserData;
}

/**
* Configuration options for WindowsImage
*/
export interface WindowsImageProps {
/**
* Initial user data
*
* @default - Empty UserData for Windows machines
*/
readonly userData?: UserData;
}

/**
@@ -20,24 +54,28 @@ export interface IMachineImageSource {
*
* https://aws.amazon.com/blogs/mt/query-for-the-latest-windows-ami-using-systems-manager-parameter-store/
*/
export class WindowsImage implements IMachineImageSource {
constructor(private readonly version: WindowsVersion) {
export class WindowsImage implements IMachineImage {
constructor(private readonly version: WindowsVersion, private readonly props: WindowsImageProps = {}) {
}

/**
* Return the image to use in the given context
*/
public getImage(scope: Construct): MachineImage {
const parameterName = this.imageParameterName(this.version);
public getImage(scope: Construct): MachineImageConfig {
const parameterName = this.imageParameterName();
const ami = ssm.StringParameter.valueForStringParameter(scope, parameterName);
return new MachineImage(ami, OperatingSystem.windows());
return {
imageId: ami,
userData: this.props.userData,
osType: OperatingSystemType.WINDOWS,
};
}

/**
* Construct the SSM parameter name for the given Windows image
*/
private imageParameterName(version: WindowsVersion): string {
return '/aws/service/ami-windows-latest/' + version;
private imageParameterName(): string {
return '/aws/service/ami-windows-latest/' + this.version;
}
}

@@ -72,20 +110,27 @@ export interface AmazonLinuxImageProps {
* @default GeneralPurpose
*/
readonly storage?: AmazonLinuxStorage;

/**
* Initial user data
*
* @default - Empty UserData for Linux machines
*/
readonly userData?: UserData;
}

/**
* Selects the latest version of Amazon Linux
*
* The AMI ID is selected using the values published to the SSM parameter store.
*/
export class AmazonLinuxImage implements IMachineImageSource {
export class AmazonLinuxImage implements IMachineImage {
private readonly generation: AmazonLinuxGeneration;
private readonly edition: AmazonLinuxEdition;
private readonly virtualization: AmazonLinuxVirt;
private readonly storage: AmazonLinuxStorage;

constructor(props?: AmazonLinuxImageProps) {
constructor(private readonly props: AmazonLinuxImageProps = {}) {
this.generation = (props && props.generation) || AmazonLinuxGeneration.AMAZON_LINUX;
this.edition = (props && props.edition) || AmazonLinuxEdition.STANDARD;
this.virtualization = (props && props.virtualization) || AmazonLinuxVirt.HVM;
@@ -95,7 +140,7 @@ export class AmazonLinuxImage implements IMachineImageSource {
/**
* Return the image to use in the given context
*/
public getImage(scope: Construct): MachineImage {
public getImage(scope: Construct): MachineImageConfig {
const parts: Array<string|undefined> = [
this.generation,
'ami',
@@ -107,7 +152,12 @@ export class AmazonLinuxImage implements IMachineImageSource {

const parameterName = '/aws/service/ami-amazon-linux-latest/' + parts.join('-');
const ami = ssm.StringParameter.valueForStringParameter(scope, parameterName);
return new MachineImage(ami, OperatingSystem.linux());

return {
imageId: ami,
userData: this.props.userData,
osType: OperatingSystemType.LINUX,
};
}
}

@@ -173,17 +223,29 @@ export enum AmazonLinuxStorage {
GENERAL_PURPOSE = 'gp2',
}

/**
* Configuration options for GenericLinuxImage
*/
export interface GenericLinuxImageProps {
/**
* Initial user data
*
* @default - Empty UserData for Windows machines
*/
readonly userData?: UserData;
}

/**
* Construct a Linux machine image from an AMI map
*
* Linux images IDs are not published to SSM parameter store yet, so you'll have to
* manually specify an AMI map.
*/
export class GenericLinuxImage implements IMachineImageSource {
constructor(private readonly amiMap: {[region: string]: string}) {
export class GenericLinuxImage implements IMachineImage {
constructor(private readonly amiMap: {[region: string]: string}, private readonly props: GenericLinuxImageProps = {}) {
}

public getImage(scope: Construct): MachineImage {
public getImage(scope: Construct): MachineImageConfig {
const region = Stack.of(scope).region;
if (Token.isUnresolved(region)) {
throw new Error(`Unable to determine AMI from AMI map since stack is region-agnostic`);
@@ -194,17 +256,11 @@ export class GenericLinuxImage implements IMachineImageSource {
throw new Error(`Unable to find AMI in AMI map: no AMI specified for region '${region}'`);
}

return new MachineImage(ami, OperatingSystem.linux());
}
}

/**
* Representation of a machine to be launched
*
* Combines an AMI ID with an OS.
*/
export class MachineImage {
constructor(public readonly imageId: string, public readonly os: OperatingSystem) {
return {
imageId: ami,
userData: this.props.userData,
osType: OperatingSystemType.LINUX,
};
}
}

@@ -214,39 +270,4 @@ export class MachineImage {
export enum OperatingSystemType {
LINUX,
WINDOWS,
}

/**
* Abstraction of OS features we need to be aware of
*/
export abstract class OperatingSystem {
/**
* OS features specialized for Windows
*/
public static windows(): OperatingSystem {
class WindowsOS extends OperatingSystem {
public readonly type = OperatingSystemType.WINDOWS;
public createDefaultUserData() {
return UserData.forWindows();
}
}
return new WindowsOS();
}

/**
* OS features specialized for Linux
*/
public static linux(): OperatingSystem {
class LinuxOS extends OperatingSystem {
public readonly type = OperatingSystemType.LINUX;
public createDefaultUserData() {
return UserData.forLinux();
}
}
return new LinuxOS();
}

public abstract createDefaultUserData(): UserData;

public abstract get type(): OperatingSystemType;
}
}
9 changes: 9 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/user-data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { OperatingSystemType } from "./machine-image";

/**
* Instance User Data
*/
@@ -16,6 +18,13 @@ export abstract class UserData {
return new WindowsUserData();
}

public static forOperatingSystem(os: OperatingSystemType): UserData {
switch (os) {
case OperatingSystemType.LINUX: return UserData.forLinux();
case OperatingSystemType.WINDOWS: return UserData.forWindows();
}
}

/**
* Add one or more commands to the user data
*/
11 changes: 7 additions & 4 deletions packages/@aws-cdk/aws-ecs/lib/cluster.ts
Original file line number Diff line number Diff line change
@@ -251,7 +251,7 @@ export interface EcsOptimizedAmiProps {
/**
* Construct a Linux machine image from the latest ECS Optimized AMI published in SSM
*/
export class EcsOptimizedAmi implements ec2.IMachineImageSource {
export class EcsOptimizedAmi implements ec2.IMachineImage {
private readonly generation: ec2.AmazonLinuxGeneration;
private readonly hwType: AmiHardwareType;

@@ -285,9 +285,12 @@ export class EcsOptimizedAmi implements ec2.IMachineImageSource {
/**
* Return the correct image
*/
public getImage(scope: Construct): ec2.MachineImage {
public getImage(scope: Construct): ec2.MachineImageConfig {
const ami = ssm.StringParameter.valueForStringParameter(scope, this.amiParameterName);
return new ec2.MachineImage(ami, ec2.OperatingSystem.linux());
return {
imageId: ami,
osType: ec2.OperatingSystemType.LINUX
};
}
}

@@ -467,7 +470,7 @@ export interface AddCapacityOptions extends AddAutoScalingGroupCapacityOptions,
*
* @default - Amazon Linux 1
*/
readonly machineImage?: ec2.IMachineImageSource;
readonly machineImage?: ec2.IMachineImage;
}

export interface NamespaceOptions {
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-eks/lib/ami.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ export interface EksOptimizedAmiProps {
/**
* Source for EKS optimized AMIs
*/
export class EksOptimizedAmi extends ec2.GenericLinuxImage implements ec2.IMachineImageSource {
export class EksOptimizedAmi extends ec2.GenericLinuxImage implements ec2.IMachineImage {
constructor(props: EksOptimizedAmiProps = {}) {
const version = props.kubernetesVersion || LATEST_KUBERNETES_VERSION;
if (!(version in EKS_AMI)) {