From 6c0bf4ac1b21116d94e26d740a0302f92207b3b1 Mon Sep 17 00:00:00 2001 From: Jimmy Gaussen Date: Thu, 25 Jul 2019 21:04:34 +0200 Subject: [PATCH] feat(ecs): ECS optimized Windows images (#3376) * deprecate `EcsOptimizedAmi` for `EcsOptimizedImage` * constructor(props) replaced by strongly typed static methods * Windows AMI support * deprecate `EcsOptimizedAmi`, `EcsOptimizedAmiProps` * will require changes on v2 shipment #3398 Fixes #2574 --- packages/@aws-cdk/aws-ecs/README.md | 4 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 122 +++++++++++- .../@aws-cdk/aws-ecs/test/test.ecs-cluster.ts | 175 +++++++++++++++++- 3 files changed, 291 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index f471ab3233581..59813655ed116 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -111,9 +111,9 @@ cluster.addCapacity('DefaultAutoScalingGroupCapacity', { const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'ASG', { vpc, instanceType: new ec2.InstanceType('t2.xlarge'), - machineImage: new EcsOptimizedAmi(), + machineImage: EcsOptimizedImage.amazonLinux(), // Or use Amazon ECS-Optimized Amazon Linux 2 AMI - // machineImage: new EcsOptimizedAmi({ generation: ec2.AmazonLinuxGeneration.AmazonLinux2 }), + // machineImage: EcsOptimizedImage.amazonLinux2(), desiredCapacity: 3, // ... other options here ... }); diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index d0b5883d972ae..d23fc77f99e76 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -4,9 +4,9 @@ import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); import cloudmap = require('@aws-cdk/aws-servicediscovery'); import ssm = require('@aws-cdk/aws-ssm'); -import { Construct, Duration, IResource, Resource, Stack } from '@aws-cdk/core'; -import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; -import { CfnCluster } from './ecs.generated'; +import {Construct, Duration, IResource, Resource, Stack} from '@aws-cdk/core'; +import {InstanceDrainHook} from './drain-hook/instance-drain-hook'; +import {CfnCluster} from './ecs.generated'; /** * The properties used to define an ECS cluster. @@ -231,8 +231,23 @@ export class Cluster extends Resource implements ICluster { } } +/** + * ECS-optimized Windows version list + */ +export enum WindowsOptimizedVersion { + SERVER_2019 = '2019', + SERVER_2016 = '2016', +} + +/* + * TODO:v2.0.0 + * * remove `export` keyword + * * remove @depracted + */ /** * The properties that define which ECS-optimized AMI is used. + * + * @deprecated see {@link EcsOptimizedImage} */ export interface EcsOptimizedAmiProps { /** @@ -242,6 +257,13 @@ export interface EcsOptimizedAmiProps { */ readonly generation?: ec2.AmazonLinuxGeneration; + /** + * The Windows Server version to use. + * + * @default none, uses Linux generation + */ + readonly windowsVersion?: WindowsOptimizedVersion; + /** * The ECS-optimized AMI variant to use. * @@ -250,11 +272,17 @@ export interface EcsOptimizedAmiProps { readonly hardwareType?: AmiHardwareType; } +/* + * TODO:v2.0.0 remove EcsOptimizedAmi + */ /** - * Construct a Linux machine image from the latest ECS Optimized AMI published in SSM + * Construct a Linux or Windows machine image from the latest ECS Optimized AMI published in SSM + * + * @deprecated see {@link EcsOptimizedImage#amazonLinux}, {@link EcsOptimizedImage#amazonLinux} and {@link EcsOptimizedImage#windows} */ export class EcsOptimizedAmi implements ec2.IMachineImage { - private readonly generation: ec2.AmazonLinuxGeneration; + private readonly generation?: ec2.AmazonLinuxGeneration; + private readonly windowsVersion?: WindowsOptimizedVersion; private readonly hwType: AmiHardwareType; private readonly amiParameterName: string; @@ -267,9 +295,17 @@ export class EcsOptimizedAmi implements ec2.IMachineImage { if (props && props.generation) { // generation defined in the props object if (props.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX && this.hwType !== AmiHardwareType.STANDARD) { throw new Error(`Amazon Linux does not support special hardware type. Use Amazon Linux 2 instead`); + } else if (props.windowsVersion) { + throw new Error('"windowsVersion" and Linux image "generation" cannot be both set'); } else { this.generation = props.generation; } + } else if (props && props.windowsVersion) { + if (this.hwType !== AmiHardwareType.STANDARD) { + throw new Error('Windows Server does not support special hardware type'); + } else { + this.windowsVersion = props.windowsVersion; + } } else { // generation not defined in props object // always default to Amazon Linux v2 regardless of HW this.generation = ec2.AmazonLinuxGeneration.AMAZON_LINUX_2; @@ -279,6 +315,7 @@ export class EcsOptimizedAmi implements ec2.IMachineImage { this.amiParameterName = "/aws/service/ecs/optimized-ami/" + ( this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX ? "amazon-linux/" : "" ) + ( this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 ? "amazon-linux-2/" : "" ) + + ( this.windowsVersion ? `windows_server/${this.windowsVersion}/english/full/` : "" ) + ( this.hwType === AmiHardwareType.GPU ? "gpu/" : "" ) + ( this.hwType === AmiHardwareType.ARM ? "arm64/" : "" ) + "recommended/image_id"; @@ -291,7 +328,78 @@ export class EcsOptimizedAmi implements ec2.IMachineImage { const ami = ssm.StringParameter.valueForStringParameter(scope, this.amiParameterName); return { imageId: ami, - osType: ec2.OperatingSystemType.LINUX + osType: this.windowsVersion ? ec2.OperatingSystemType.WINDOWS : ec2.OperatingSystemType.LINUX + }; + } +} + +/** + * Construct a Linux or Windows machine image from the latest ECS Optimized AMI published in SSM + */ +export class EcsOptimizedImage implements ec2.IMachineImage { + /** + * Construct an Amazon Linux 2 image from the latest ECS Optimized AMI published in SSM + * + * @param hardwareType ECS-optimized AMI variant to use + */ + public static amazonLinux2(hardwareType = AmiHardwareType.STANDARD): EcsOptimizedImage { + return new EcsOptimizedImage({generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, hardwareType}); + } + + /** + * Construct an Amazon Linux AMI image from the latest ECS Optimized AMI published in SSM + */ + public static amazonLinux(): EcsOptimizedImage { + return new EcsOptimizedImage({generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX}); + } + + /** + * Construct a Windows image from the latest ECS Optimized AMI published in SSM + * + * @param windowsVersion Windows Version to use + */ + public static windows(windowsVersion: WindowsOptimizedVersion): EcsOptimizedImage { + return new EcsOptimizedImage({windowsVersion}); + } + + private readonly generation?: ec2.AmazonLinuxGeneration; + private readonly windowsVersion?: WindowsOptimizedVersion; + private readonly hwType?: AmiHardwareType; + + private readonly amiParameterName: string; + + /** + * Constructs a new instance of the EcsOptimizedAmi class. + */ + private constructor(props: EcsOptimizedAmiProps) { + this.hwType = props && props.hardwareType; + + if (props.windowsVersion) { + this.windowsVersion = props.windowsVersion; + } else if (props.generation) { + this.generation = props.generation; + } else { + throw new Error('This error should never be thrown'); + } + + // set the SSM parameter name + this.amiParameterName = "/aws/service/ecs/optimized-ami/" + + ( this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX ? "amazon-linux/" : "" ) + + ( this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 ? "amazon-linux-2/" : "" ) + + ( this.windowsVersion ? `windows_server/${this.windowsVersion}/english/full/` : "" ) + + ( this.hwType === AmiHardwareType.GPU ? "gpu/" : "" ) + + ( this.hwType === AmiHardwareType.ARM ? "arm64/" : "" ) + + "recommended/image_id"; + } + + /** + * Return the correct image + */ + public getImage(scope: Construct): ec2.MachineImageConfig { + const ami = ssm.StringParameter.valueForStringParameter(scope, this.amiParameterName); + return { + imageId: ami, + osType: this.windowsVersion ? ec2.OperatingSystemType.WINDOWS : ec2.OperatingSystemType.LINUX }; } } @@ -510,7 +618,7 @@ export interface CloudMapNamespaceOptions { export enum AmiHardwareType { /** - * Use the Amazon ECS-optimized Amazon Linux 2 AMI. + * Use the standard Amazon ECS-optimized AMI. */ STANDARD = 'Standard', diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index 961d93c38c2d8..8007c40d2d51e 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -235,9 +235,13 @@ export = { test.done(); }, + /* + * TODO:v2.0.0 BEGINNING OF OBSOLETE BLOCK + */ "allows specifying special HW AMI Type"(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -249,12 +253,21 @@ export = { }); // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { ImageId: { Ref: "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" } })); + test.deepEqual(template.Parameters, { + SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: "AWS::SSM::Parameter::Value", + Default: "/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended/image_id" + } + }); + test.done(); }, @@ -274,6 +287,166 @@ export = { hardwareType: ecs.AmiHardwareType.GPU, }), }); + }, /Amazon Linux does not support special hardware type/); + + test.done(); + }, + + "allows specifying windows image"(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('WindowsAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: new ecs.EcsOptimizedAmi({ + windowsVersion: ecs.WindowsOptimizedVersion.SERVER_2019, + }), + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + test.deepEqual(template.Parameters, { + SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: "AWS::SSM::Parameter::Value", + Default: "/aws/service/ecs/optimized-ami/windows_server/2019/english/full/recommended/image_id" + } + }); + + test.done(); + }, + + "errors if windows given with special HW type"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + // THEN + test.throws(() => { + cluster.addCapacity('WindowsGpuAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: new ecs.EcsOptimizedAmi({ + windowsVersion: ecs.WindowsOptimizedVersion.SERVER_2019, + hardwareType: ecs.AmiHardwareType.GPU, + }), + }); + }, /Windows Server does not support special hardware type/); + + test.done(); + }, + + "errors if windowsVersion and linux generation are set"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + // THEN + test.throws(() => { + cluster.addCapacity('WindowsScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: new ecs.EcsOptimizedAmi({ + windowsVersion: ecs.WindowsOptimizedVersion.SERVER_2019, + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX + }), + }); + }, /"windowsVersion" and Linux image "generation" cannot be both set/); + + test.done(); + }, + + /* + * TODO:v2.0.0 END OF OBSOLETE BLOCK + */ + + "allows specifying special HW AMI Type v2"(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('GpuAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(ecs.AmiHardwareType.GPU) + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { + ImageId: { + Ref: "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + } + })); + + test.deepEqual(template.Parameters, { + SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: "AWS::SSM::Parameter::Value", + Default: "/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended/image_id" + } + }); + + test.done(); + }, + + "allows specifying Amazon Linux v1 AMI"(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('GpuAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ecs.EcsOptimizedImage.amazonLinux() + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { + ImageId: { + Ref: "SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + } + })); + + test.deepEqual(template.Parameters, { + SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: "AWS::SSM::Parameter::Value", + Default: "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + } + }); + + test.done(); + }, + + "allows specifying windows image v2"(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'test'); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('WindowsAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ecs.EcsOptimizedImage.windows(ecs.WindowsOptimizedVersion.SERVER_2019), + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + test.deepEqual(template.Parameters, { + SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { + Type: "AWS::SSM::Parameter::Value", + Default: "/aws/service/ecs/optimized-ami/windows_server/2019/english/full/recommended/image_id" + } }); test.done();