From 2a80e4b113bac0716f5aa1d4806e425759da1743 Mon Sep 17 00:00:00 2001 From: Jericho Tolentino <68654047+jericht@users.noreply.github.com> Date: Tue, 18 Jan 2022 08:43:42 -0600 Subject: [PATCH] fix(ec2): launch template names in imdsv2 not unique across stacks (under feature flag) (#17766) Fixes https://github.com/aws/aws-cdk/issues/17656 ### Notes Changes the name for the `LaunchTemplate` created in the aspect that enforces IMDSv2 on EC2 instances to a unique name. Introduces a new feature flag (`@aws-cdk/aws-ec2:uniqueImdsv2TemplateName`) to change the launch template name. ### Testing Added a unit test ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/aspects/require-imdsv2-aspect.ts | 11 ++-- .../aspects/require-imdsv2-aspect.test.ts | 53 +++++++++++++++++++ packages/@aws-cdk/cx-api/lib/features.ts | 13 +++++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts b/packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts index f1a5270f1fb08..abad030c160a8 100644 --- a/packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts +++ b/packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts @@ -1,4 +1,6 @@ import * as cdk from '@aws-cdk/core'; +import { FeatureFlags } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { CfnLaunchTemplate } from '../ec2.generated'; import { Instance } from '../instance'; import { LaunchTemplate } from '../launch-template'; @@ -83,17 +85,20 @@ export class InstanceRequireImdsv2Aspect extends RequireImdsv2Aspect { return; } - const name = `${node.node.id}LaunchTemplate`; const launchTemplate = new CfnLaunchTemplate(node, 'LaunchTemplate', { launchTemplateData: { metadataOptions: { httpTokens: 'required', }, }, - launchTemplateName: name, }); + if (FeatureFlags.of(node).isEnabled(cxapi.EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME)) { + launchTemplate.launchTemplateName = cdk.Names.uniqueId(launchTemplate); + } else { + launchTemplate.launchTemplateName = `${node.node.id}LaunchTemplate`; + } node.instance.launchTemplate = { - launchTemplateName: name, + launchTemplateName: launchTemplate.launchTemplateName, version: launchTemplate.getAtt('LatestVersionNumber').toString(), }; } diff --git a/packages/@aws-cdk/aws-ec2/test/aspects/require-imdsv2-aspect.test.ts b/packages/@aws-cdk/aws-ec2/test/aspects/require-imdsv2-aspect.test.ts index ade2eaeab1f1d..189244bc251f5 100644 --- a/packages/@aws-cdk/aws-ec2/test/aspects/require-imdsv2-aspect.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/aspects/require-imdsv2-aspect.test.ts @@ -4,7 +4,9 @@ import { haveResourceLike, } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; +import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { CfnLaunchTemplate, Instance, @@ -135,6 +137,57 @@ describe('RequireImdsv2Aspect', () => { trace: undefined, }); }); + + testFutureBehavior('launch template name is unique with feature flag', { [cxapi.EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME]: true }, cdk.App, (app2) => { + // GIVEN + const otherStack = new cdk.Stack(app2, 'OtherStack'); + const otherVpc = new Vpc(otherStack, 'OtherVpc'); + const otherInstance = new Instance(otherStack, 'OtherInstance', { + vpc: otherVpc, + instanceType: new InstanceType('t2.micro'), + machineImage: MachineImage.latestAmazonLinux(), + }); + const imdsv2Stack = new cdk.Stack(app2, 'RequireImdsv2Stack'); + const imdsv2Vpc = new Vpc(imdsv2Stack, 'Vpc'); + const instance = new Instance(imdsv2Stack, 'Instance', { + vpc: imdsv2Vpc, + instanceType: new InstanceType('t2.micro'), + machineImage: MachineImage.latestAmazonLinux(), + }); + const aspect = new InstanceRequireImdsv2Aspect(); + + // WHEN + cdk.Aspects.of(imdsv2Stack).add(aspect); + cdk.Aspects.of(otherStack).add(aspect); + app2.synth(); + + // THEN + const launchTemplate = instance.node.tryFindChild('LaunchTemplate') as LaunchTemplate; + const otherLaunchTemplate = otherInstance.node.tryFindChild('LaunchTemplate') as LaunchTemplate; + expect(launchTemplate).toBeDefined(); + expect(otherLaunchTemplate).toBeDefined(); + expect(launchTemplate.launchTemplateName !== otherLaunchTemplate.launchTemplateName); + }); + + testLegacyBehavior('launch template name uses legacy id without feature flag', cdk.App, (app2) => { + // GIVEN + const imdsv2Stack = new cdk.Stack(app2, 'RequireImdsv2Stack'); + const imdsv2Vpc = new Vpc(imdsv2Stack, 'Vpc'); + const instance = new Instance(imdsv2Stack, 'Instance', { + vpc: imdsv2Vpc, + instanceType: new InstanceType('t2.micro'), + machineImage: MachineImage.latestAmazonLinux(), + }); + const aspect = new InstanceRequireImdsv2Aspect(); + + // WHEN + cdk.Aspects.of(imdsv2Stack).add(aspect); + app2.synth(); + + // THEN + const launchTemplate = instance.node.tryFindChild('LaunchTemplate') as LaunchTemplate; + expect(launchTemplate.launchTemplateName).toEqual(`${instance.node.id}LaunchTemplate`); + }); }); describe('LaunchTemplateRequireImdsv2Aspect', () => { diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 71ed4856ec85a..f4ced76f60d3a 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -178,6 +178,17 @@ export const TARGET_PARTITIONS = '@aws-cdk/core:target-partitions'; */ export const ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER = '@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver'; +/** + * Enable this feature flag to have Launch Templates generated by the `InstanceRequireImdsv2Aspect` use unique names. + * + * Previously, the generated Launch Template names were only unique within a stack because they were based only on the + * `Instance` construct ID. If another stack that has an `Instance` with the same construct ID is deployed in the same + * account and region, the deployments would always fail as the generated Launch Template names were the same. + * + * The new implementation addresses this issue by generating the Launch Template name with the `Names.uniqueId` method. + */ +export const EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME = '@aws-cdk/aws-ec2:uniqueImdsv2TemplateName'; + /** * This map includes context keys and values for feature flags that enable * capabilities "from the future", which we could not introduce as the default @@ -206,6 +217,7 @@ export const FUTURE_FLAGS: { [key: string]: boolean } = { [LAMBDA_RECOGNIZE_VERSION_PROPS]: true, [CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021]: true, [ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER]: true, + [EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME]: true, // We will advertise this flag when the feature is complete // [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: 'true', @@ -245,6 +257,7 @@ const FUTURE_FLAGS_DEFAULTS: { [key: string]: boolean } = { [LAMBDA_RECOGNIZE_VERSION_PROPS]: false, [CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021]: false, [ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER]: false, + [EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME]: false, }; export function futureFlagDefault(flag: string): boolean | undefined {