diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index df4a1eece07e0..c7340af1d47e5 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -80,11 +80,27 @@ export abstract class MachineImage { * @param parameterName The name of SSM parameter containing the AMi id * @param os The operating system type of the AMI * @param userData optional user data for the given image + * @deprecated Use `MachineImage.fromSsmParameter()` instead */ public static fromSSMParameter(parameterName: string, os: OperatingSystemType, userData?: UserData): IMachineImage { return new GenericSSMParameterImage(parameterName, os, userData); } + /** + * An image specified in SSM parameter store + * + * By default, the SSM parameter is refreshed at every deployment, + * causing your instances to be replaced whenever a new version of the AMI + * is released. + * + * Pass `{ cachedInContext: true }` to keep the AMI ID stable. If you do, you + * will have to remember to periodically invalidate the context to refresh + * to the newest AMI ID. + */ + public static fromSsmParameter(parameterName: string, options?: SsmParameterImageOptions): IMachineImage { + return new GenericSsmParameterImage(parameterName, options); + } + /** * Look up a shared Machine Image using DescribeImages * @@ -96,6 +112,8 @@ export abstract class MachineImage { * will be used on future runs. To refresh the AMI lookup, you will have to * evict the value from the cache using the `cdk context` command. See * https://docs.aws.amazon.com/cdk/latest/guide/context.html for more information. + * + * This function can not be used in environment-agnostic stacks. */ public static lookup(props: LookupMachineImageProps): IMachineImage { return new LookupMachineImage(props); @@ -131,10 +149,17 @@ export interface MachineImageConfig { * on the instance if you are using this image. * * The AMI ID is selected using the values published to the SSM parameter store. + * + * @deprecated Use `MachineImage.fromSsmParameter()` instead */ export class GenericSSMParameterImage implements IMachineImage { + /** + * Name of the SSM parameter we're looking up + */ + public readonly parameterName: string; - constructor(private readonly parameterName: string, private readonly os: OperatingSystemType, private readonly userData?: UserData) { + constructor(parameterName: string, private readonly os: OperatingSystemType, private readonly userData?: UserData) { + this.parameterName = parameterName; } /** @@ -150,6 +175,75 @@ export class GenericSSMParameterImage implements IMachineImage { } } +/** + * Properties for GenericSsmParameterImage + */ +export interface SsmParameterImageOptions { + /** + * Operating system + * + * @default OperatingSystemType.LINUX + */ + readonly os?: OperatingSystemType; + + /** + * Custom UserData + * + * @default - UserData appropriate for the OS + */ + readonly userData?: UserData; + + /** + * Whether the AMI ID is cached to be stable between deployments + * + * By default, the newest image is used on each deployment. This will cause + * instances to be replaced whenever a new version is released, and may cause + * downtime if there aren't enough running instances in the AutoScalingGroup + * to reschedule the tasks on. + * + * If set to true, the AMI ID will be cached in `cdk.context.json` and the + * same value will be used on future runs. Your instances will not be replaced + * but your AMI version will grow old over time. To refresh the AMI lookup, + * you will have to evict the value from the cache using the `cdk context` + * command. See https://docs.aws.amazon.com/cdk/latest/guide/context.html for + * more information. + * + * Can not be set to `true` in environment-agnostic stacks. + * + * @default false + */ + readonly cachedInContext?: boolean; +} + +/** + * Select the image based on a given SSM parameter + * + * This Machine Image automatically updates to the latest version on every + * deployment. Be aware this will cause your instances to be replaced when a + * new version of the image becomes available. Do not store stateful information + * on the instance if you are using this image. + * + * The AMI ID is selected using the values published to the SSM parameter store. + */ +class GenericSsmParameterImage implements IMachineImage { + constructor(private readonly parameterName: string, private readonly props: SsmParameterImageOptions = {}) { + } + + /** + * Return the image to use in the given context + */ + public getImage(scope: Construct): MachineImageConfig { + const imageId = lookupImage(scope, this.props.cachedInContext, this.parameterName); + + const osType = this.props.os ?? OperatingSystemType.LINUX; + return { + imageId, + osType, + userData: this.props.userData ?? (osType === OperatingSystemType.WINDOWS ? UserData.forWindows() : UserData.forLinux()), + }; + } +} + /** * Configuration options for WindowsImage */ @@ -240,6 +334,27 @@ export interface AmazonLinuxImageProps { * @default X86_64 */ readonly cpuType?: AmazonLinuxCpuType; + + /** + * Whether the AMI ID is cached to be stable between deployments + * + * By default, the newest image is used on each deployment. This will cause + * instances to be replaced whenever a new version is released, and may cause + * downtime if there aren't enough running instances in the AutoScalingGroup + * to reschedule the tasks on. + * + * If set to true, the AMI ID will be cached in `cdk.context.json` and the + * same value will be used on future runs. Your instances will not be replaced + * but your AMI version will grow old over time. To refresh the AMI lookup, + * you will have to evict the value from the cache using the `cdk context` + * command. See https://docs.aws.amazon.com/cdk/latest/guide/context.html for + * more information. + * + * Can not be set to `true` in environment-agnostic stacks. + * + * @default false + */ + readonly cachedInContext?: boolean; } /** @@ -253,8 +368,10 @@ export interface AmazonLinuxImageProps { * The AMI ID is selected using the values published to the SSM parameter store. */ export class AmazonLinuxImage extends GenericSSMParameterImage { - - constructor(props: AmazonLinuxImageProps = {}) { + /** + * Return the SSM parameter name that will contain the Amazon Linux image with the given attributes + */ + public static ssmParameterName(props: AmazonLinuxImageProps = {}) { const generation = (props && props.generation) || AmazonLinuxGeneration.AMAZON_LINUX; const edition = (props && props.edition) || AmazonLinuxEdition.STANDARD; const virtualization = (props && props.virtualization) || AmazonLinuxVirt.HVM; @@ -269,8 +386,29 @@ export class AmazonLinuxImage extends GenericSSMParameterImage { storage, ].filter(x => x !== undefined); // Get rid of undefineds - const parameterName = '/aws/service/ami-amazon-linux-latest/' + parts.join('-'); - super(parameterName, OperatingSystemType.LINUX, props.userData); + return '/aws/service/ami-amazon-linux-latest/' + parts.join('-'); + } + + private readonly cachedInContext: boolean; + + constructor(private readonly props: AmazonLinuxImageProps = {}) { + super(AmazonLinuxImage.ssmParameterName(props), OperatingSystemType.LINUX, props.userData); + + this.cachedInContext = props.cachedInContext ?? false; + } + + /** + * Return the image to use in the given context + */ + public getImage(scope: Construct): MachineImageConfig { + const imageId = lookupImage(scope, this.cachedInContext, this.parameterName); + + const osType = OperatingSystemType.LINUX; + return { + imageId, + osType, + userData: this.props.userData ?? UserData.forLinux(), + }; } } @@ -536,3 +674,9 @@ export interface LookupMachineImageProps { */ readonly userData?: UserData; } + +function lookupImage(scope: Construct, cachedInContext: boolean | undefined, parameterName: string) { + return cachedInContext + ? ssm.StringParameter.valueFromLookup(scope, parameterName) + : ssm.StringParameter.valueForTypedStringParameter(scope, parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts b/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts index 6a6a6f3c7332f..a25acdfbb4cfa 100644 --- a/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts @@ -158,6 +158,25 @@ test('LookupMachineImage creates correct type of UserData', () => { expect(isLinuxUserData(linuxDetails.userData)).toBeTruthy(); }); +test('cached lookups of Amazon Linux', () => { + // WHEN + const ami = ec2.MachineImage.latestAmazonLinux({ cachedInContext: true }).getImage(stack).imageId; + + // THEN + expect(ami).toEqual('dummy-value-for-/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2'); + expect(app.synth().manifest.missing).toEqual([ + { + key: 'ssm:account=1234:parameterName=/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2:region=testregion', + props: { + account: '1234', + region: 'testregion', + parameterName: '/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2', + }, + provider: 'ssm', + }, + ]); +}); + function isWindowsUserData(ud: ec2.UserData) { return ud.render().indexOf('powershell') > -1; } diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 5454d93e61095..8ffbde1d9a0e8 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -131,6 +131,23 @@ cluster.addAutoScalingGroup(autoScalingGroup); If you omit the property `vpc`, the construct will create a new VPC with two AZs. +By default, all machine images will auto-update to the latest version +on each deployment, causing a replacement of the instances in your AutoScalingGroup +if the AMI has been updated since the last deployment. + +If task draining is enabled, ECS will transparently reschedule tasks on to the new +instances before terminating your old instances. If you have disabled task draining, +the tasks will be terminated along with the instance. To prevent that, you +can pick a non-updating AMI by passing `cacheInContext: true`, but be sure +to periodically update to the latest AMI manually by using the [CDK CLI +context management commands](https://docs.aws.amazon.com/cdk/latest/guide/context.html): + +```ts +const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'ASG', { + // ... + machineImage: EcsOptimizedImage.amazonLinux({ cacheInContext: true }), +}); +``` ### Bottlerocket diff --git a/packages/@aws-cdk/aws-ecs/lib/amis.ts b/packages/@aws-cdk/aws-ecs/lib/amis.ts new file mode 100644 index 0000000000000..f7e4139c10c3a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/amis.ts @@ -0,0 +1,373 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ssm from '@aws-cdk/aws-ssm'; + +// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. +// eslint-disable-next-line +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * The ECS-optimized AMI variant to use. For more information, see + * [Amazon ECS-optimized AMIs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html). + */ +export enum AmiHardwareType { + + /** + * Use the standard Amazon ECS-optimized AMI. + */ + STANDARD = 'Standard', + + /** + * Use the Amazon ECS GPU-optimized AMI. + */ + GPU = 'GPU', + + /** + * Use the Amazon ECS-optimized Amazon Linux 2 (arm64) AMI. + */ + ARM = 'ARM64', +} + + +/** + * ECS-optimized Windows version list + */ +export enum WindowsOptimizedVersion { + SERVER_2019 = '2019', + SERVER_2016 = '2016', +} + +/* + * TODO:v2.0.0 + * * remove `export` keyword + * * remove @deprecated + */ +/** + * The properties that define which ECS-optimized AMI is used. + * + * @deprecated see {@link EcsOptimizedImage} + */ +export interface EcsOptimizedAmiProps { + /** + * The Amazon Linux generation to use. + * + * @default AmazonLinuxGeneration.AmazonLinux2 + */ + 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. + * + * @default AmiHardwareType.Standard + */ + readonly hardwareType?: AmiHardwareType; + + /** + * Whether the AMI ID is cached to be stable between deployments + * + * By default, the newest image is used on each deployment. This will cause + * instances to be replaced whenever a new version is released, and may cause + * downtime if there aren't enough running instances in the AutoScalingGroup + * to reschedule the tasks on. + * + * If set to true, the AMI ID will be cached in `cdk.context.json` and the + * same value will be used on future runs. Your instances will not be replaced + * but your AMI version will grow old over time. To refresh the AMI lookup, + * you will have to evict the value from the cache using the `cdk context` + * command. See https://docs.aws.amazon.com/cdk/latest/guide/context.html for + * more information. + * + * Can not be set to `true` in environment-agnostic stacks. + * + * @default false + */ + readonly cachedInContext?: boolean; +} + +/* + * TODO:v2.0.0 remove EcsOptimizedAmi + */ +/** + * 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 windowsVersion?: WindowsOptimizedVersion; + private readonly hwType: AmiHardwareType; + + private readonly amiParameterName: string; + private readonly cachedInContext: boolean; + + /** + * Constructs a new instance of the EcsOptimizedAmi class. + */ + constructor(props?: EcsOptimizedAmiProps) { + this.hwType = (props && props.hardwareType) || AmiHardwareType.STANDARD; + 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; + } + + // 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'; + + this.cachedInContext = props?.cachedInContext ?? false; + } + + /** + * Return the correct image + */ + public getImage(scope: CoreConstruct): ec2.MachineImageConfig { + const ami = lookupImage(scope, this.cachedInContext, this.amiParameterName); + + const osType = this.windowsVersion ? ec2.OperatingSystemType.WINDOWS : ec2.OperatingSystemType.LINUX; + return { + imageId: ami, + osType, + userData: ec2.UserData.forOperatingSystem(osType), + }; + } +} + +/** + * Additional configuration properties for EcsOptimizedImage factory functions + */ +export interface EcsOptimizedImageOptions { + /** + * Whether the AMI ID is cached to be stable between deployments + * + * By default, the newest image is used on each deployment. This will cause + * instances to be replaced whenever a new version is released, and may cause + * downtime if there aren't enough running instances in the AutoScalingGroup + * to reschedule the tasks on. + * + * If set to true, the AMI ID will be cached in `cdk.context.json` and the + * same value will be used on future runs. Your instances will not be replaced + * but your AMI version will grow old over time. To refresh the AMI lookup, + * you will have to evict the value from the cache using the `cdk context` + * command. See https://docs.aws.amazon.com/cdk/latest/guide/context.html for + * more information. + * + * Can not be set to `true` in environment-agnostic stacks. + * + * @default false + */ + readonly cachedInContext?: boolean; +} + +/** + * 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, options: EcsOptimizedImageOptions = {}): EcsOptimizedImage { + return new EcsOptimizedImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + hardwareType, + cachedInContext: options.cachedInContext, + }); + } + + /** + * Construct an Amazon Linux AMI image from the latest ECS Optimized AMI published in SSM + */ + public static amazonLinux(options: EcsOptimizedImageOptions = {}): EcsOptimizedImage { + return new EcsOptimizedImage({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX, + cachedInContext: options.cachedInContext, + }); + } + + /** + * Construct a Windows image from the latest ECS Optimized AMI published in SSM + * + * @param windowsVersion Windows Version to use + */ + public static windows(windowsVersion: WindowsOptimizedVersion, options: EcsOptimizedImageOptions = {}): EcsOptimizedImage { + return new EcsOptimizedImage({ + windowsVersion, + cachedInContext: options.cachedInContext, + }); + } + + private readonly generation?: ec2.AmazonLinuxGeneration; + private readonly windowsVersion?: WindowsOptimizedVersion; + private readonly hwType?: AmiHardwareType; + + private readonly amiParameterName: string; + private readonly cachedInContext: boolean; + + /** + * 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'; + + this.cachedInContext = props?.cachedInContext ?? false; + } + + /** + * Return the correct image + */ + public getImage(scope: CoreConstruct): ec2.MachineImageConfig { + const ami = lookupImage(scope, this.cachedInContext, this.amiParameterName); + + const osType = this.windowsVersion ? ec2.OperatingSystemType.WINDOWS : ec2.OperatingSystemType.LINUX; + return { + imageId: ami, + osType, + userData: ec2.UserData.forOperatingSystem(osType), + }; + } +} + +/** + * Amazon ECS variant + */ +export enum BottlerocketEcsVariant { + /** + * aws-ecs-1 variant + */ + AWS_ECS_1 = 'aws-ecs-1' + +} + +/** + * Properties for BottleRocketImage + */ +export interface BottleRocketImageProps { + /** + * The Amazon ECS variant to use. + * Only `aws-ecs-1` is currently available + * + * @default - BottlerocketEcsVariant.AWS_ECS_1 + */ + readonly variant?: BottlerocketEcsVariant; + + /** + * The CPU architecture + * + * @default - x86_64 + */ + readonly architecture?: ec2.InstanceArchitecture; + + /** + * Whether the AMI ID is cached to be stable between deployments + * + * By default, the newest image is used on each deployment. This will cause + * instances to be replaced whenever a new version is released, and may cause + * downtime if there aren't enough running instances in the AutoScalingGroup + * to reschedule the tasks on. + * + * If set to true, the AMI ID will be cached in `cdk.context.json` and the + * same value will be used on future runs. Your instances will not be replaced + * but your AMI version will grow old over time. To refresh the AMI lookup, + * you will have to evict the value from the cache using the `cdk context` + * command. See https://docs.aws.amazon.com/cdk/latest/guide/context.html for + * more information. + * + * Can not be set to `true` in environment-agnostic stacks. + * + * @default false + */ + readonly cachedInContext?: boolean; +} + +/** + * Construct an Bottlerocket image from the latest AMI published in SSM + */ +export class BottleRocketImage implements ec2.IMachineImage { + private readonly amiParameterName: string; + /** + * Amazon ECS variant for Bottlerocket AMI + */ + private readonly variant: string; + + /** + * Instance architecture + */ + private readonly architecture: ec2.InstanceArchitecture; + + private readonly cachedInContext: boolean; + + /** + * Constructs a new instance of the BottleRocketImage class. + */ + public constructor(props: BottleRocketImageProps = {}) { + this.variant = props.variant ?? BottlerocketEcsVariant.AWS_ECS_1; + this.architecture = props.architecture ?? ec2.InstanceArchitecture.X86_64; + + // set the SSM parameter name + this.amiParameterName = `/aws/service/bottlerocket/${this.variant}/${this.architecture}/latest/image_id`; + + this.cachedInContext = props.cachedInContext ?? false; + } + + /** + * Return the correct image + */ + public getImage(scope: CoreConstruct): ec2.MachineImageConfig { + const ami = lookupImage(scope, this.cachedInContext, this.amiParameterName); + + return { + imageId: ami, + osType: ec2.OperatingSystemType.LINUX, + userData: ec2.UserData.custom(''), + }; + } +} + +function lookupImage(scope: CoreConstruct, cachedInContext: boolean | undefined, parameterName: string) { + return cachedInContext + ? ssm.StringParameter.valueFromLookup(scope, parameterName) + : ssm.StringParameter.valueForTypedStringParameter(scope, parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 4ea5e24b7529c..49d99bf68925d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -6,9 +6,9 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; -import * as ssm from '@aws-cdk/aws-ssm'; import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, IConstruct } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { BottleRocketImage, EcsOptimizedAmi } from './amis'; import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; import { ECSMetrics } from './ecs-canned-metrics.generated'; import { CfnCluster, CfnCapacityProvider, CfnClusterCapacityProviderAssociations } from './ecs.generated'; @@ -570,253 +570,6 @@ 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 @deprecated - */ -/** - * The properties that define which ECS-optimized AMI is used. - * - * @deprecated see {@link EcsOptimizedImage} - */ -export interface EcsOptimizedAmiProps { - /** - * The Amazon Linux generation to use. - * - * @default AmazonLinuxGeneration.AmazonLinux2 - */ - 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. - * - * @default AmiHardwareType.Standard - */ - readonly hardwareType?: AmiHardwareType; -} - -/* - * TODO:v2.0.0 remove EcsOptimizedAmi - */ -/** - * 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 windowsVersion?: WindowsOptimizedVersion; - private readonly hwType: AmiHardwareType; - - private readonly amiParameterName: string; - - /** - * Constructs a new instance of the EcsOptimizedAmi class. - */ - constructor(props?: EcsOptimizedAmiProps) { - this.hwType = (props && props.hardwareType) || AmiHardwareType.STANDARD; - 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; - } - - // 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: CoreConstruct): ec2.MachineImageConfig { - const ami = ssm.StringParameter.valueForTypedStringParameter(scope, this.amiParameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); - const osType = this.windowsVersion ? ec2.OperatingSystemType.WINDOWS : ec2.OperatingSystemType.LINUX; - return { - imageId: ami, - osType, - userData: ec2.UserData.forOperatingSystem(osType), - }; - } -} - -/** - * 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: CoreConstruct): ec2.MachineImageConfig { - const ami = ssm.StringParameter.valueForTypedStringParameter(scope, this.amiParameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); - const osType = this.windowsVersion ? ec2.OperatingSystemType.WINDOWS : ec2.OperatingSystemType.LINUX; - return { - imageId: ami, - osType, - userData: ec2.UserData.forOperatingSystem(osType), - }; - } -} - -/** - * Amazon ECS variant - */ -export enum BottlerocketEcsVariant { - /** - * aws-ecs-1 variant - */ - AWS_ECS_1 = 'aws-ecs-1' - -} - -/** - * Properties for BottleRocketImage - */ -export interface BottleRocketImageProps { - /** - * The Amazon ECS variant to use. - * Only `aws-ecs-1` is currently available - * - * @default - BottlerocketEcsVariant.AWS_ECS_1 - */ - readonly variant?: BottlerocketEcsVariant; - - /** - * The CPU architecture - * - * @default - x86_64 - */ - readonly architecture?: ec2.InstanceArchitecture; -} - -/** - * Construct an Bottlerocket image from the latest AMI published in SSM - */ -export class BottleRocketImage implements ec2.IMachineImage { - private readonly amiParameterName: string; - /** - * Amazon ECS variant for Bottlerocket AMI - */ - private readonly variant: string; - - /** - * Instance architecture - */ - private readonly architecture: ec2.InstanceArchitecture; - - /** - * Constructs a new instance of the BottleRocketImage class. - */ - public constructor(props: BottleRocketImageProps = {}) { - this.variant = props.variant ?? BottlerocketEcsVariant.AWS_ECS_1; - this.architecture = props.architecture ?? ec2.InstanceArchitecture.X86_64; - - // set the SSM parameter name - this.amiParameterName = `/aws/service/bottlerocket/${this.variant}/${this.architecture}/latest/image_id`; - } - - /** - * Return the correct image - */ - public getImage(scope: CoreConstruct): ec2.MachineImageConfig { - const ami = ssm.StringParameter.valueForStringParameter(scope, this.amiParameterName); - return { - imageId: ami, - osType: ec2.OperatingSystemType.LINUX, - userData: ec2.UserData.custom(''), - }; - } -} - /** * A regional grouping of one or more container instances on which you can run tasks and services. */ @@ -1058,11 +811,29 @@ export interface AddCapacityOptions extends AddAutoScalingGroupCapacityOptions, readonly instanceType: ec2.InstanceType; /** - * The ECS-optimized AMI variant to use. For more information, see - * [Amazon ECS-optimized AMIs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html). + * The ECS-optimized AMI variant to use + * + * The default is to use an ECS-optimized AMI of Amazon Linux 2 which is + * automatically updated to the latest version on every deployment. This will + * replace the instances in the AutoScalingGroup. Make sure you have not disabled + * task draining, to avoid downtime when the AMI updates. + * + * To use an image that does not update on every deployment, pass: + * + * ```ts + * { + * machineImage: EcsOptimizedImage.amazonLinux2(AmiHardwareType.STANDARD, { + * cachedInContext: true, + * }), + * } + * ``` + * + * For more information, see [Amazon ECS-optimized + * AMIs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html). + * * You must define either `machineImage` or `machineImageType`, not both. * - * @default - Amazon Linux 2 + * @default - Automatically updated, ECS-optimized Amazon Linux 2 */ readonly machineImage?: ec2.IMachineImage; } @@ -1091,28 +862,6 @@ export interface CloudMapNamespaceOptions { readonly vpc?: ec2.IVpc; } -/** - * The ECS-optimized AMI variant to use. For more information, see - * [Amazon ECS-optimized AMIs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html). - */ -export enum AmiHardwareType { - - /** - * Use the standard Amazon ECS-optimized AMI. - */ - STANDARD = 'Standard', - - /** - * Use the Amazon ECS GPU-optimized AMI. - */ - GPU = 'GPU', - - /** - * Use the Amazon ECS-optimized Amazon Linux 2 (arm64) AMI. - */ - ARM = 'ARM64', -} - enum ContainerInsights { /** * Enable CloudWatch Container Insights for the cluster diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 0c1cee2a56ff9..bd076ccfd05f7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -4,6 +4,7 @@ export * from './base/task-definition'; export * from './container-definition'; export * from './container-image'; +export * from './amis'; export * from './cluster'; export * from './environment-file'; export * from './firelens-log-router'; diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index cfe65183d1c02..52de16b353137 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -1694,7 +1694,7 @@ describe('cluster', () => { const template = assembly.getStackByName(stack.stackName).template; expect(template.Parameters).toEqual({ SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { - Type: 'AWS::SSM::Parameter::Value', + Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/bottlerocket/aws-ecs-1/arm64/latest/image_id', }, }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.bottlerocket.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.bottlerocket.expected.json index 616a80172092e..45ea355b976e9 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.bottlerocket.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.bottlerocket.expected.json @@ -95,15 +95,15 @@ "VpcPublicSubnet1NATGateway4D7517AA": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, "AllocationId": { "Fn::GetAtt": [ "VpcPublicSubnet1EIPD7E02669", "AllocationId" ] }, - "SubnetId": { - "Ref": "VpcPublicSubnet1Subnet5C2D37C4" - }, "Tags": [ { "Key": "Name", @@ -835,7 +835,7 @@ }, "Parameters": { "SsmParameterValueawsservicebottlerocketawsecs1x8664latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { - "Type": "AWS::SSM::Parameter::Value", + "Type": "AWS::SSM::Parameter::Value", "Default": "/aws/service/bottlerocket/aws-ecs-1/x86_64/latest/image_id" } } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json index e863d75fa16fb..b082b18fd7281 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json @@ -95,15 +95,15 @@ "VpcPublicSubnet1NATGateway4D7517AA": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, "AllocationId": { "Fn::GetAtt": [ "VpcPublicSubnet1EIPD7E02669", "AllocationId" ] }, - "SubnetId": { - "Ref": "VpcPublicSubnet1Subnet5C2D37C4" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "VpcPublicSubnet2NATGateway9182C01D": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, "AllocationId": { "Fn::GetAtt": [ "VpcPublicSubnet2EIP3C605A87", "AllocationId" ] }, - "SubnetId": { - "Ref": "VpcPublicSubnet2Subnet691E08A3" - }, "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.expected.json index fb6c80ff66b00..f2c15441bbe28 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.graviton-bottlerocket.expected.json @@ -867,8 +867,8 @@ }, "Parameters": { "SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { - "Type": "AWS::SSM::Parameter::Value", + "Type": "AWS::SSM::Parameter::Value", "Default": "/aws/service/bottlerocket/aws-ecs-1/arm64/latest/image_id" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.firelens-cloudwatch.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.firelens-cloudwatch.expected.json index c6efbdee23f66..a10c635e498d6 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.firelens-cloudwatch.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.firelens-cloudwatch.expected.json @@ -95,15 +95,15 @@ "VpcPublicSubnet1NATGateway4D7517AA": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, "AllocationId": { "Fn::GetAtt": [ "VpcPublicSubnet1EIPD7E02669", "AllocationId" ] }, - "SubnetId": { - "Ref": "VpcPublicSubnet1Subnet5C2D37C4" - }, "Tags": [ { "Key": "Name", @@ -192,15 +192,15 @@ "VpcPublicSubnet2NATGateway9182C01D": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, "AllocationId": { "Fn::GetAtt": [ "VpcPublicSubnet2EIP3C605A87", "AllocationId" ] }, - "SubnetId": { - "Ref": "VpcPublicSubnet2Subnet691E08A3" - }, "Tags": [ { "Key": "Name",