diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index c7b8613c4f6eb..4f77528d922e6 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -1509,6 +1509,18 @@ const template = new ec2.LaunchTemplate(this, 'LaunchTemplate', { }); ``` +And the following demonstrates how to enable metadata options support. + +```ts +new ec2.LaunchTemplate(this, 'LaunchTemplate', { + httpEndpoint: true, + httpProtocolIpv6: true, + httpPutResponseHopLimit: 1, + httpTokens: ec2.LaunchTemplateHttpTokens.REQUIRED, + instanceMetadataTags: true, +}); +``` + ## Detailed Monitoring The following demonstrates how to enable [Detailed Monitoring](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html) for an EC2 instance. Keep in mind that Detailed Monitoring results in [additional charges](http://aws.amazon.com/cloudwatch/pricing/). diff --git a/packages/@aws-cdk/aws-ec2/lib/launch-template.ts b/packages/@aws-cdk/aws-ec2/lib/launch-template.ts index 790940efc6e95..24e7424bfeb93 100644 --- a/packages/@aws-cdk/aws-ec2/lib/launch-template.ts +++ b/packages/@aws-cdk/aws-ec2/lib/launch-template.ts @@ -12,10 +12,8 @@ import { TagType, Tags, Token, - Aspects, } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { LaunchTemplateRequireImdsv2Aspect } from '.'; import { Connections, IConnectable } from './connections'; import { CfnLaunchTemplate } from './ec2.generated'; import { InstanceType } from './instance-types'; @@ -193,6 +191,23 @@ export interface LaunchTemplateSpotOptions { readonly validUntil?: Expiration; }; +/** + * The state of token usage for your instance metadata requests. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions-httptokens + */ +export enum LaunchTemplateHttpTokens { + /** + * If the state is optional, you can choose to retrieve instance metadata with or without a signed token header on your request. + */ + OPTIONAL = 'optional', + /** + * If the state is required, you must send a signed token header with any instance metadata retrieval requests. In this state, + * retrieving the IAM role credentials always returns the version 2.0 credentials; the version 1.0 credentials are not available. + */ + REQUIRED = 'required', +} + /** * Properties of a LaunchTemplate. */ @@ -341,6 +356,52 @@ export interface LaunchTemplateProps { * @default - false */ readonly requireImdsv2?: boolean; + + /** + * Enables or disables the HTTP metadata endpoint on your instances. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions-httpendpoint + * + * @default true + */ + readonly httpEndpoint?: boolean; + + /** + * Enables or disables the IPv6 endpoint for the instance metadata service. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions-httpprotocolipv6 + * + * @default true + */ + readonly httpProtocolIpv6?: boolean; + + /** + * The desired HTTP PUT response hop limit for instance metadata requests. The larger the number, the further instance metadata requests can travel. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions-httpputresponsehoplimit + * + * @default 1 + */ + readonly httpPutResponseHopLimit?: number; + + /** + * The state of token usage for your instance metadata requests. The default state is `optional` if not specified. However, + * if requireImdsv2 is true, the state must be `required`. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions-httptokens + * + * @default LaunchTemplateHttpTokens.OPTIONAL + */ + readonly httpTokens?: LaunchTemplateHttpTokens; + + /** + * Set to enabled to allow access to instance tags from the instance metadata. Set to disabled to turn off access to instance tags from the instance metadata. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions-instancemetadatatags + * + * @default false + */ + readonly instanceMetadataTags?: boolean; } /** @@ -506,6 +567,12 @@ export class LaunchTemplate extends Resource implements ILaunchTemplate, iam.IGr Annotations.of(this).addError('Spot block duration must be exactly 1, 2, 3, 4, 5, or 6 hours.'); } + // Basic validation of the provided httpPutResponseHopLimit + if (props.httpPutResponseHopLimit !== undefined && (props.httpPutResponseHopLimit < 1 || props.httpPutResponseHopLimit > 64)) { + // See: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions-httpputresponsehoplimit + Annotations.of(this).addError('HttpPutResponseHopLimit must between 1 and 64'); + } + this.role = props.role; this._grantPrincipal = this.role; const iamProfile: iam.CfnInstanceProfile | undefined = this.role ? new iam.CfnInstanceProfile(this, 'Profile', { @@ -639,6 +706,7 @@ export class LaunchTemplate extends Resource implements ILaunchTemplate, iam.IGr securityGroupIds: securityGroupsToken, tagSpecifications: tagsToken, userData: userDataToken, + metadataOptions: this.renderMetadataOptions(props), // Fields not yet implemented: // ========================== @@ -663,9 +731,6 @@ export class LaunchTemplate extends Resource implements ILaunchTemplate, iam.IGr // Also not implemented in Instance L2 // licenseSpecifications: undefined, - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions - // metadataOptions: undefined, - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-tagspecifications // Should be implemented via the Tagging aspect in CDK core. Complication will be that this tagging interface is very unique to LaunchTemplates. // tagSpecification: undefined @@ -686,9 +751,31 @@ export class LaunchTemplate extends Resource implements ILaunchTemplate, iam.IGr this.latestVersionNumber = resource.attrLatestVersionNumber; this.launchTemplateId = resource.ref; this.versionNumber = Token.asString(resource.getAtt('LatestVersionNumber')); + } - if (props.requireImdsv2) { - Aspects.of(this).add(new LaunchTemplateRequireImdsv2Aspect()); + private renderMetadataOptions(props: LaunchTemplateProps) { + let requireMetadataOptions = false; + // if requireImdsv2 is true, httpTokens must be required. + if (props.requireImdsv2 === true && props.httpTokens === LaunchTemplateHttpTokens.OPTIONAL) { + Annotations.of(this).addError('httpTokens must be required when requireImdsv2 is true'); + } + if (props.httpEndpoint !== undefined || props.httpProtocolIpv6 !== undefined || props.httpPutResponseHopLimit !== undefined || + props.httpTokens !== undefined || props.instanceMetadataTags !== undefined || props.requireImdsv2 === true) { + requireMetadataOptions = true; + } + if (requireMetadataOptions) { + return { + httpEndpoint: props.httpEndpoint === true ? 'enabled' : + props.httpEndpoint === false ? 'disabled' : undefined, + httpProtocolIpv6: props.httpProtocolIpv6 === true ? 'enabled' : + props.httpProtocolIpv6 === false ? 'disabled' : undefined, + httpPutResponseHopLimit: props.httpPutResponseHopLimit, + httpTokens: props.requireImdsv2 === true ? LaunchTemplateHttpTokens.REQUIRED : props.httpTokens, + instanceMetadataTags: props.instanceMetadataTags === true ? 'enabled' : + props.instanceMetadataTags === false ? 'disabled' : undefined, + }; + } else { + return undefined; } } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.launch-template.ts b/packages/@aws-cdk/aws-ec2/test/integ.launch-template.ts new file mode 100644 index 0000000000000..5ee628b4bacdc --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.launch-template.ts @@ -0,0 +1,24 @@ +import * as cdk from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import * as ec2 from '../lib'; + + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-ec2-lt-metadata-1'); + +new ec2.LaunchTemplate(stack, 'LT', { + httpEndpoint: true, + httpProtocolIpv6: true, + httpPutResponseHopLimit: 2, + httpTokens: ec2.LaunchTemplateHttpTokens.REQUIRED, + instanceMetadataTags: true, +}); + + +new integ.IntegTest(app, 'LambdaTest', { + testCases: [stack], +}); + +app.synth(); + diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/LambdaTestDefaultTestDeployAssert1AF2B360.assets.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/LambdaTestDefaultTestDeployAssert1AF2B360.assets.json new file mode 100644 index 0000000000000..1d2e86648cf93 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/LambdaTestDefaultTestDeployAssert1AF2B360.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "LambdaTestDefaultTestDeployAssert1AF2B360.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/LambdaTestDefaultTestDeployAssert1AF2B360.template.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/LambdaTestDefaultTestDeployAssert1AF2B360.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/LambdaTestDefaultTestDeployAssert1AF2B360.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/TestStack.assets.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/TestStack.assets.json new file mode 100644 index 0000000000000..e9c52dc148e48 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/TestStack.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "d17275411e4cfac0e49078863acdd9766783f143736b4534a3a0f1b5a4de118a": { + "source": { + "path": "TestStack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "d17275411e4cfac0e49078863acdd9766783f143736b4534a3a0f1b5a4de118a.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/TestStack.template.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/TestStack.template.json new file mode 100644 index 0000000000000..3a078b81f8fc5 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/TestStack.template.json @@ -0,0 +1,83 @@ +{ + "Resources": { + "LTC4631592": { + "Type": "AWS::EC2::LaunchTemplate", + "Properties": { + "LaunchTemplateData": { + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "enabled", + "HttpPutResponseHopLimit": 2, + "HttpTokens": "required", + "InstanceMetadataTags": "enabled" + }, + "TagSpecifications": [ + { + "ResourceType": "instance", + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/LT" + } + ] + }, + { + "ResourceType": "volume", + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/LT" + } + ] + } + ] + }, + "TagSpecifications": [ + { + "ResourceType": "launch-template", + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/LT" + } + ] + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/aws-cdk-ec2-lt-metadata-1.assets.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/aws-cdk-ec2-lt-metadata-1.assets.json new file mode 100644 index 0000000000000..9ed72b653b653 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/aws-cdk-ec2-lt-metadata-1.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "ea313fee581c8e898c158e9d123dd48345192689bb08a3f7e84716db61247c6c": { + "source": { + "path": "aws-cdk-ec2-lt-metadata-1.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "ea313fee581c8e898c158e9d123dd48345192689bb08a3f7e84716db61247c6c.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/aws-cdk-ec2-lt-metadata-1.template.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/aws-cdk-ec2-lt-metadata-1.template.json new file mode 100644 index 0000000000000..3ff46f6e06e20 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/aws-cdk-ec2-lt-metadata-1.template.json @@ -0,0 +1,83 @@ +{ + "Resources": { + "LTC4631592": { + "Type": "AWS::EC2::LaunchTemplate", + "Properties": { + "LaunchTemplateData": { + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "enabled", + "HttpPutResponseHopLimit": 2, + "HttpTokens": "required", + "InstanceMetadataTags": "enabled" + }, + "TagSpecifications": [ + { + "ResourceType": "instance", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-lt-metadata-1/LT" + } + ] + }, + { + "ResourceType": "volume", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-lt-metadata-1/LT" + } + ] + } + ] + }, + "TagSpecifications": [ + { + "ResourceType": "launch-template", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-lt-metadata-1/LT" + } + ] + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/integ.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/integ.json new file mode 100644 index 0000000000000..92f821d9eb880 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "21.0.0", + "testCases": { + "LambdaTest/DefaultTest": { + "stacks": [ + "aws-cdk-ec2-lt-metadata-1" + ], + "assertionStack": "LambdaTest/DefaultTest/DeployAssert", + "assertionStackName": "LambdaTestDefaultTestDeployAssert1AF2B360" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..ac57774761bca --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/manifest.json @@ -0,0 +1,111 @@ +{ + "version": "21.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-cdk-ec2-lt-metadata-1.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-ec2-lt-metadata-1.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-ec2-lt-metadata-1": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-ec2-lt-metadata-1.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ea313fee581c8e898c158e9d123dd48345192689bb08a3f7e84716db61247c6c.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-ec2-lt-metadata-1.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-ec2-lt-metadata-1.assets" + ], + "metadata": { + "/aws-cdk-ec2-lt-metadata-1/LT/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "LTC4631592" + } + ], + "/aws-cdk-ec2-lt-metadata-1/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-ec2-lt-metadata-1/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-ec2-lt-metadata-1" + }, + "LambdaTestDefaultTestDeployAssert1AF2B360.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "LambdaTestDefaultTestDeployAssert1AF2B360.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "LambdaTestDefaultTestDeployAssert1AF2B360": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "LambdaTestDefaultTestDeployAssert1AF2B360.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "LambdaTestDefaultTestDeployAssert1AF2B360.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "LambdaTestDefaultTestDeployAssert1AF2B360.assets" + ], + "metadata": { + "/LambdaTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/LambdaTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "LambdaTest/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/tree.json b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/tree.json new file mode 100644 index 0000000000000..36dbaccbedb90 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.integ.snapshot/tree.json @@ -0,0 +1,130 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.108" + } + }, + "aws-cdk-ec2-lt-metadata-1": { + "id": "aws-cdk-ec2-lt-metadata-1", + "path": "aws-cdk-ec2-lt-metadata-1", + "children": { + "LT": { + "id": "LT", + "path": "aws-cdk-ec2-lt-metadata-1/LT", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ec2-lt-metadata-1/LT/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::LaunchTemplate", + "aws:cdk:cloudformation:props": { + "launchTemplateData": { + "tagSpecifications": [ + { + "resourceType": "instance", + "tags": [ + { + "key": "Name", + "value": "aws-cdk-ec2-lt-metadata-1/LT" + } + ] + }, + { + "resourceType": "volume", + "tags": [ + { + "key": "Name", + "value": "aws-cdk-ec2-lt-metadata-1/LT" + } + ] + } + ], + "metadataOptions": { + "httpEndpoint": "enabled", + "httpProtocolIpv6": "enabled", + "httpPutResponseHopLimit": 2, + "httpTokens": "required", + "instanceMetadataTags": "enabled" + } + }, + "tagSpecifications": [ + { + "resourceType": "launch-template", + "tags": [ + { + "key": "Name", + "value": "aws-cdk-ec2-lt-metadata-1/LT" + } + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "LambdaTest": { + "id": "LambdaTest", + "path": "LambdaTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "LambdaTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "LambdaTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.108" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "LambdaTest/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts b/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts index 29b4f25676418..1a2f6aee6f8f7 100644 --- a/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts @@ -21,6 +21,7 @@ import { InstanceInitiatedShutdownBehavior, InstanceType, LaunchTemplate, + LaunchTemplateHttpTokens, OperatingSystemType, SecurityGroup, SpotInstanceInterruption, @@ -760,3 +761,146 @@ describe('LaunchTemplate marketOptions', () => { }); }); }); + +describe('LaunchTemplate metadataOptions', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app); + }); + + test.each([ + [true, 'enabled'], + [false, 'disabled'], + ])('given httpEndpoint %p', (given: boolean, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + httpEndpoint: given, + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + MetadataOptions: { + HttpEndpoint: expected, + }, + }, + }); + }); + + test.each([ + [true, 'enabled'], + [false, 'disabled'], + ])('given httpProtocolIpv6 %p', (given: boolean, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + httpProtocolIpv6: given, + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + MetadataOptions: { + HttpProtocolIpv6: expected, + }, + }, + }); + }); + + test.each([ + [1, 1], + [2, 2], + ])('given httpPutResponseHopLimit %p', (given: number, expected: number) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + httpPutResponseHopLimit: given, + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + MetadataOptions: { + HttpPutResponseHopLimit: expected, + }, + }, + }); + }); + + test.each([ + [LaunchTemplateHttpTokens.OPTIONAL, 'optional'], + [LaunchTemplateHttpTokens.REQUIRED, 'required'], + ])('given httpTokens %p', (given: LaunchTemplateHttpTokens, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + httpTokens: given, + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + MetadataOptions: { + HttpTokens: expected, + }, + }, + }); + }); + + test.each([ + [true, 'enabled'], + [false, 'disabled'], + ])('given instanceMetadataTags %p', (given: boolean, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + instanceMetadataTags: given, + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + MetadataOptions: { + InstanceMetadataTags: expected, + }, + }, + }); + }); + + test.each([ + [0, 1], + [-1, 1], + [1, 0], + [64, 0], + [65, 1], + ])('given instanceMetadataTags %p', (given: number, expected: number) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + httpPutResponseHopLimit: given, + }); + // THEN + const errors = Annotations.fromStack(stack).findError('/Default/Template', Match.anyValue()); + expect(errors).toHaveLength(expected); + }); + + test('throw when requireImdsv2 is true and httpTokens is OPTIONAL', () => { + // WHEN + new LaunchTemplate(stack, 'Template', { + requireImdsv2: true, + httpTokens: LaunchTemplateHttpTokens.OPTIONAL, + }); + // THEN + const errors = Annotations.fromStack(stack).findError('/Default/Template', Match.anyValue()); + expect(errors[0].entry.data).toMatch(/httpTokens must be required when requireImdsv2 is true/); + }); + test('httpTokens REQUIRED is allowed when requireImdsv2 is true', () => { + // WHEN + new LaunchTemplate(stack, 'Template', { + requireImdsv2: true, + httpTokens: LaunchTemplateHttpTokens.REQUIRED, + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + MetadataOptions: { + HttpTokens: 'required', + }, + }, + }); + + }); +});