From 9fcfe1210a43d8a8af73759862911c3b932c445f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 12 Aug 2021 17:47:18 +0200 Subject: [PATCH 1/5] feat(ec2/ecs): `cacheInContext` properties for machine images Most `MachineImage` implementations look up AMIs from SSM Parameters, and by default they will all look up the Parameters on each deployment. This leads to instance replacement. Since we already know the SSM Parameter Name and CDK already has a cached SSM context lookup, it should be simple to get a stable AMI ID. This is not ideal because the AMI will grow outdated over time, but users should have the option to pick non-updating images in a convenient way. Fixes #12484. --- .../@aws-cdk/aws-ec2/lib/machine-image.ts | 152 +++++++- .../aws-ec2/test/machine-image.test.ts | 19 + packages/@aws-cdk/aws-ecs/lib/amis.ts | 359 ++++++++++++++++++ packages/@aws-cdk/aws-ecs/lib/cluster.ts | 282 ++------------ packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + 5 files changed, 548 insertions(+), 265 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/amis.ts diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index df4a1eece07e0..fbe9496d2dbcc 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,77 @@ 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 = this.props.cachedInContext + ? ssm.StringParameter.valueFromLookup(scope, this.parameterName) + : ssm.StringParameter.valueForTypedStringParameter(scope, this.parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + + 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 +336,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 +370,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 +388,31 @@ 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 = this.cachedInContext + ? ssm.StringParameter.valueFromLookup(scope, this.parameterName) + : ssm.StringParameter.valueForTypedStringParameter(scope, this.parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + + const osType = OperatingSystemType.LINUX; + return { + imageId, + osType, + userData: this.props.userData ?? UserData.forLinux(), + }; } } 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/lib/amis.ts b/packages/@aws-cdk/aws-ecs/lib/amis.ts new file mode 100644 index 0000000000000..450ee7c8d6e6d --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/amis.ts @@ -0,0 +1,359 @@ +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 = this.cachedInContext + ? ssm.StringParameter.valueFromLookup(scope, this.amiParameterName) + : 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), + }; + } +} + +/** + * 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 = this.cachedInContext + ? ssm.StringParameter.valueFromLookup(scope, this.amiParameterName) + : 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; + + /** + * 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; + private readonly cachedInContext: boolean; + + /** + * Constructs a new instance of the BottleRocketImage class. + */ + public constructor(props: BottleRocketImageProps = {}) { + this.variant = props.variant ?? BottlerocketEcsVariant.AWS_ECS_1; + + // set the SSM parameter name + this.amiParameterName = `/aws/service/bottlerocket/${this.variant}/x86_64/latest/image_id`; + + this.cachedInContext = props.cachedInContext ?? false; + } + + /** + * Return the correct image + */ + public getImage(scope: CoreConstruct): ec2.MachineImageConfig { + const ami = this.cachedInContext + ? ssm.StringParameter.valueFromLookup(scope, this.amiParameterName) + : ssm.StringParameter.valueForTypedStringParameter(scope, this.amiParameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + + return { + imageId: ami, + osType: ec2.OperatingSystemType.LINUX, + userData: ec2.UserData.custom(''), + }; + } +} \ 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 5b9da054a9fe2..d2b76f626b3ac 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'; @@ -567,240 +567,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; -} - -/** - * 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; - - /** - * Constructs a new instance of the BottleRocketImage class. - */ - public constructor(props: BottleRocketImageProps = {}) { - this.variant = props.variant ?? BottlerocketEcsVariant.AWS_ECS_1; - - // set the SSM parameter name - this.amiParameterName = `/aws/service/bottlerocket/${this.variant}/x86_64/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. */ @@ -1035,11 +801,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; } @@ -1068,28 +852,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'; From f8eedbfd51e6d2896c33ccc95e6af7d5e84183b3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 12 Aug 2021 17:56:02 +0200 Subject: [PATCH 2/5] Add to README --- packages/@aws-cdk/aws-ecs/README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 9e8170f86d826..2d2d167fa38ef 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -131,6 +131,18 @@ 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 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, +you can pick a non-updating AMI by passing `cacheInContext: true`: + +```ts +const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'ASG', { + // ... + machineImage: EcsOptimizedImage.amazonLinux({ cacheInContext: true }), +}); +``` ### Bottlerocket @@ -214,7 +226,7 @@ some supporting containers which are used to support the main container, doings things like upload logs or metrics to monitoring services. To run a task or service with Amazon EC2 launch type, use the `Ec2TaskDefinition`. For AWS Fargate tasks/services, use the -`FargateTaskDefinition`. For AWS ECS Anywhere use the `ExternalTaskDefinition`. These classes +`FargateTaskDefinition`. For AWS ECS Anywhere use the `ExternalTaskDefinition`. These classes provide simplified APIs that only contain properties relevant for each specific launch type. For a `FargateTaskDefinition`, specify the task size (`memoryLimitMiB` and `cpu`): @@ -736,7 +748,7 @@ const service = new ecs.Ec2Service(stack, 'Service', { }); ``` -With `bridge` or `host` network modes, only `SRV` DNS record types are supported. +With `bridge` or `host` network modes, only `SRV` DNS record types are supported. By default, `SRV` DNS record types will target the default container and default port. However, you may target a different container and port on the same ECS task: @@ -893,7 +905,7 @@ const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { To enable using the inference accelerators in the containers, add `inferenceAcceleratorResources` field and set it to a list of device names used for the inference accelerators. Each value in the -list should match a `DeviceName` for an `InferenceAccelerator` specified in the task definition. +list should match a `DeviceName` for an `InferenceAccelerator` specified in the task definition. ```ts const inferenceAcceleratorResources = ['device1']; @@ -927,11 +939,11 @@ const service = new ecs.Ec2Service(stack, 'Service', { You can enable sending logs of your execute session commands to a CloudWatch log group or S3 bucket by configuring the `executeCommandConfiguration` property for your cluster. The default configuration will send the logs to the CloudWatch Logs using the `awslogs` log driver that is configured in your task definition. Please note, -when using your own `logConfiguration` the log group or S3 Bucket specified must already be created. +when using your own `logConfiguration` the log group or S3 Bucket specified must already be created. To encrypt data using your own KMS Customer Key (CMK), you must create a CMK and provide the key in the `kmsKey` field of the `executeCommandConfiguration`. To use this key for encrypting CloudWatch log data or S3 bucket, make sure to associate the key -to these resources on creation. +to these resources on creation. ```ts const kmsKey = new kms.Key(stack, 'KmsKey'); From 27191a5e3cbbc46c97bb04c60eaaa39f9df96a09 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 13 Aug 2021 16:00:38 +0200 Subject: [PATCH 3/5] Update expectation --- .../aws-ecs/test/ec2/integ.bottlerocket.expected.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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" } } From d65c0bc071811439005f928021684e97122fbb7d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 24 Aug 2021 11:14:39 +0200 Subject: [PATCH 4/5] Review comments --- packages/@aws-cdk/aws-ec2/lib/machine-image.ts | 14 ++++++++------ packages/@aws-cdk/aws-ecs/README.md | 9 +++++++-- packages/@aws-cdk/aws-ecs/lib/amis.ts | 18 +++++++++--------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index fbe9496d2dbcc..c7340af1d47e5 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -233,9 +233,7 @@ class GenericSsmParameterImage implements IMachineImage { * Return the image to use in the given context */ public getImage(scope: Construct): MachineImageConfig { - const imageId = this.props.cachedInContext - ? ssm.StringParameter.valueFromLookup(scope, this.parameterName) - : ssm.StringParameter.valueForTypedStringParameter(scope, this.parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + const imageId = lookupImage(scope, this.props.cachedInContext, this.parameterName); const osType = this.props.os ?? OperatingSystemType.LINUX; return { @@ -403,9 +401,7 @@ export class AmazonLinuxImage extends GenericSSMParameterImage { * Return the image to use in the given context */ public getImage(scope: Construct): MachineImageConfig { - const imageId = this.cachedInContext - ? ssm.StringParameter.valueFromLookup(scope, this.parameterName) - : ssm.StringParameter.valueForTypedStringParameter(scope, this.parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + const imageId = lookupImage(scope, this.cachedInContext, this.parameterName); const osType = OperatingSystemType.LINUX; return { @@ -678,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-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 2d2d167fa38ef..7bab3146d231f 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -132,10 +132,15 @@ 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. +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, -you can pick a non-updating AMI by passing `cacheInContext: true`: +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', { diff --git a/packages/@aws-cdk/aws-ecs/lib/amis.ts b/packages/@aws-cdk/aws-ecs/lib/amis.ts index 450ee7c8d6e6d..42ca741a044fd 100644 --- a/packages/@aws-cdk/aws-ecs/lib/amis.ts +++ b/packages/@aws-cdk/aws-ecs/lib/amis.ts @@ -146,9 +146,7 @@ export class EcsOptimizedAmi implements ec2.IMachineImage { * Return the correct image */ public getImage(scope: CoreConstruct): ec2.MachineImageConfig { - const ami = this.cachedInContext - ? ssm.StringParameter.valueFromLookup(scope, this.amiParameterName) - : ssm.StringParameter.valueForTypedStringParameter(scope, this.amiParameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + const ami = lookupImage(scope, this.cachedInContext, this.amiParameterName); const osType = this.windowsVersion ? ec2.OperatingSystemType.WINDOWS : ec2.OperatingSystemType.LINUX; return { @@ -261,9 +259,7 @@ export class EcsOptimizedImage implements ec2.IMachineImage { * Return the correct image */ public getImage(scope: CoreConstruct): ec2.MachineImageConfig { - const ami = this.cachedInContext - ? ssm.StringParameter.valueFromLookup(scope, this.amiParameterName) - : ssm.StringParameter.valueForTypedStringParameter(scope, this.amiParameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + const ami = lookupImage(scope, this.cachedInContext, this.amiParameterName); const osType = this.windowsVersion ? ec2.OperatingSystemType.WINDOWS : ec2.OperatingSystemType.LINUX; return { @@ -346,9 +342,7 @@ export class BottleRocketImage implements ec2.IMachineImage { * Return the correct image */ public getImage(scope: CoreConstruct): ec2.MachineImageConfig { - const ami = this.cachedInContext - ? ssm.StringParameter.valueFromLookup(scope, this.amiParameterName) - : ssm.StringParameter.valueForTypedStringParameter(scope, this.amiParameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID); + const ami = lookupImage(scope, this.cachedInContext, this.amiParameterName); return { imageId: ami, @@ -356,4 +350,10 @@ export class BottleRocketImage implements ec2.IMachineImage { 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 From 95b54514f203703ad6cf3d87a5680067cd2e325a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 7 Sep 2021 14:48:48 +0200 Subject: [PATCH 5/5] Update snapshots --- .../ec2/integ.firelens-s3-config.expected.json | 16 ++++++++-------- .../integ.firelens-cloudwatch.expected.json | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) 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 ffe4d55f698ed..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", @@ -1181,8 +1181,8 @@ "Description": "Artifact hash for asset \"2ca891b3a73a4a36630bba20580e3390a104d2ac9ff1f22a6bcadf575f8a5a61\"" }, "SsmParameterValueawsserviceawsforfluentbit210C96584B6F00A464EAD1953AFF4B05118Parameter": { - "Type": "AWS::SSM::Parameter::Value", + "Type": "AWS::SSM::Parameter::Value", "Default": "/aws/service/aws-for-fluent-bit/2.1.0" } } -} +} \ 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 e56c4053234f9..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", @@ -600,8 +600,8 @@ }, "Parameters": { "SsmParameterValueawsserviceawsforfluentbitlatestC96584B6F00A464EAD1953AFF4B05118Parameter": { - "Type": "AWS::SSM::Parameter::Value", + "Type": "AWS::SSM::Parameter::Value", "Default": "/aws/service/aws-for-fluent-bit/latest" } } -} +} \ No newline at end of file