diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index af8c5ff64594e..20b9dab23c434 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -1475,3 +1475,19 @@ const template = new ec2.LaunchTemplate(this, 'LaunchTemplate', { }), }); ``` + +## 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/). + +```ts +declare const vpc: ec2.Vpc; +declare const instanceType: ec2.InstanceType; + +new ec2.Instance(this, 'Instance1', { + vpc, + instanceType, + machineImage: new ec2.AmazonLinuxImage(), + detailedMonitoring: true, +}); +``` diff --git a/packages/@aws-cdk/aws-ec2/lib/instance.ts b/packages/@aws-cdk/aws-ec2/lib/instance.ts index a12c3a45d6108..213b1ef0e4629 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance.ts @@ -245,6 +245,15 @@ export interface InstanceProps { * @default - false */ readonly requireImdsv2?: boolean; + + /** + * Whether "Detailed Monitoring" is enabled for this instance + * Keep in mind that Detailed Monitoring results in extra charges + * + * @see http://aws.amazon.com/cloudwatch/pricing/ + * @default - false + */ + readonly detailedMonitoring?: boolean; } /** @@ -381,6 +390,7 @@ export class Instance extends Resource implements IInstance { blockDeviceMappings: props.blockDevices !== undefined ? instanceBlockDeviceMappings(this, props.blockDevices) : undefined, privateIpAddress: props.privateIpAddress, propagateTagsToVolumeOnCreation: props.propagateTagsToVolumeOnCreation, + monitoring: props.detailedMonitoring, }); this.instance.node.addDependency(this.role); diff --git a/packages/@aws-cdk/aws-ec2/test/instance.test.ts b/packages/@aws-cdk/aws-ec2/test/instance.test.ts index 5f4011aa12f3e..6fd215af31bff 100644 --- a/packages/@aws-cdk/aws-ec2/test/instance.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/instance.test.ts @@ -5,8 +5,19 @@ import { Asset } from '@aws-cdk/aws-s3-assets'; import { StringParameter } from '@aws-cdk/aws-ssm'; import { Stack } from '@aws-cdk/core'; import { - AmazonLinuxImage, BlockDeviceVolume, CloudFormationInit, - EbsDeviceVolumeType, InitCommand, Instance, InstanceArchitecture, InstanceClass, InstanceSize, InstanceType, LaunchTemplate, UserData, Vpc, + AmazonLinuxImage, + BlockDeviceVolume, + CloudFormationInit, + EbsDeviceVolumeType, + InitCommand, + Instance, + InstanceArchitecture, + InstanceClass, + InstanceSize, + InstanceType, + LaunchTemplate, + UserData, + Vpc, } from '../lib'; let stack: Stack; @@ -144,7 +155,7 @@ describe('instance', () => { for (const instanceClass of sampleInstanceClassKeys) { // WHEN - const key = instanceClass.key as keyof(typeof InstanceClass); + const key = instanceClass.key as keyof (typeof InstanceClass); const instanceType = InstanceClass[key]; // THEN @@ -432,6 +443,62 @@ describe('instance', () => { }, }); }); + + describe('Detailed Monitoring', () => { + test('instance with Detailed Monitoring enabled', () => { + // WHEN + new Instance(stack, 'Instance', { + vpc, + machineImage: new AmazonLinuxImage(), + instanceType: new InstanceType('t2.micro'), + detailedMonitoring: true, + }); + + // Force stack synth so the Instance is applied + Template.fromStack(stack); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { + Monitoring: true, + }); + }); + + test('instance with Detailed Monitoring disabled', () => { + // WHEN + new Instance(stack, 'Instance', { + vpc, + machineImage: new AmazonLinuxImage(), + instanceType: new InstanceType('t2.micro'), + detailedMonitoring: false, + }); + + // Force stack synth so the Instance is applied + Template.fromStack(stack); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { + Monitoring: false, + }); + }); + + test('instance with Detailed Monitoring unset falls back to disabled', () => { + // WHEN + new Instance(stack, 'Instance', { + vpc, + machineImage: new AmazonLinuxImage(), + instanceType: new InstanceType('t2.micro'), + }); + + // Force stack synth so the Instance is applied + Template.fromStack(stack); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Instance', { + Monitoring: Match.absent(), + }); + }); + }); + }); test('add CloudFormation Init to instance', () => { @@ -519,8 +586,14 @@ test('cause replacement from s3 asset in userdata', () => { const hash = 'f88eace39faf39d7'; Template.fromStack(stack).templateMatches(Match.objectLike({ Resources: Match.objectLike({ - [`InstanceOne5B821005${hash}`]: Match.objectLike({ Type: 'AWS::EC2::Instance', Properties: Match.anyValue() }), - [`InstanceTwoDC29A7A7${hash}`]: Match.objectLike({ Type: 'AWS::EC2::Instance', Properties: Match.anyValue() }), + [`InstanceOne5B821005${hash}`]: Match.objectLike({ + Type: 'AWS::EC2::Instance', + Properties: Match.anyValue(), + }), + [`InstanceTwoDC29A7A7${hash}`]: Match.objectLike({ + Type: 'AWS::EC2::Instance', + Properties: Match.anyValue(), + }), }), })); }); diff --git a/packages/@aws-cdk/aws-ec2/test/integ.instance.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.instance.expected.json index 957af9c0f453e..d30b3a47c711e 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.instance.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.instance.expected.json @@ -18,11 +18,11 @@ "VPCPublicSubnet1SubnetB4246D30": { "Type": "AWS::EC2::Subnet", "Properties": { - "CidrBlock": "10.0.0.0/19", "VpcId": { "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.0.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { @@ -95,15 +95,15 @@ "VPCPublicSubnet1NATGatewayE0556630": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet1EIP6AD938E8", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet1SubnetB4246D30" - }, "Tags": [ { "Key": "Name", @@ -115,11 +115,11 @@ "VPCPublicSubnet2Subnet74179F39": { "Type": "AWS::EC2::Subnet", "Properties": { - "CidrBlock": "10.0.32.0/19", "VpcId": { "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.32.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { @@ -192,15 +192,15 @@ "VPCPublicSubnet2NATGateway3C070193": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet2EIP4947BC00", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet2Subnet74179F39" - }, "Tags": [ { "Key": "Name", @@ -212,11 +212,11 @@ "VPCPublicSubnet3Subnet631C5E25": { "Type": "AWS::EC2::Subnet", "Properties": { - "CidrBlock": "10.0.64.0/19", "VpcId": { "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.64.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { @@ -289,15 +289,15 @@ "VPCPublicSubnet3NATGatewayD3048F5C": { "Type": "AWS::EC2::NatGateway", "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, "AllocationId": { "Fn::GetAtt": [ "VPCPublicSubnet3EIPAD4BC883", "AllocationId" ] }, - "SubnetId": { - "Ref": "VPCPublicSubnet3Subnet631C5E25" - }, "Tags": [ { "Key": "Name", @@ -309,11 +309,11 @@ "VPCPrivateSubnet1Subnet8BCA10E0": { "Type": "AWS::EC2::Subnet", "Properties": { - "CidrBlock": "10.0.96.0/19", "VpcId": { "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.96.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { @@ -371,11 +371,11 @@ "VPCPrivateSubnet2SubnetCFCDAA7A": { "Type": "AWS::EC2::Subnet", "Properties": { - "CidrBlock": "10.0.128.0/19", "VpcId": { "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.128.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { @@ -433,11 +433,11 @@ "VPCPrivateSubnet3Subnet3EDCD457": { "Type": "AWS::EC2::Subnet", "Properties": { - "CidrBlock": "10.0.160.0/19", "VpcId": { "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.160.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { @@ -620,6 +620,7 @@ "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" }, "InstanceType": "t3.nano", + "Monitoring": true, "SecurityGroupIds": [ { "Fn::GetAtt": [ diff --git a/packages/@aws-cdk/aws-ec2/test/integ.instance.ts b/packages/@aws-cdk/aws-ec2/test/integ.instance.ts index bae57818521cc..1b120c511eaaa 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.instance.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.instance.ts @@ -15,6 +15,7 @@ class TestStack extends cdk.Stack { vpc, instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.NANO), machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 }), + detailedMonitoring: true, }); instance.addToRolePolicy(new PolicyStatement({ diff --git a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts index c385ce83a7254..fc877cbb2c7c9 100644 --- a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts @@ -1,5 +1,5 @@ -import { Bucket } from '@aws-cdk/aws-s3'; import { Template, Match } from '@aws-cdk/assertions'; +import { Bucket } from '@aws-cdk/aws-s3'; import { Aws, Stack, CfnResource } from '@aws-cdk/core'; import * as ec2 from '../lib'; diff --git a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts index d931401484ba2..7640557032510 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts @@ -452,7 +452,7 @@ testFutureBehavior('nested assemblies share assets: default synth edition', flag // Read the asset manifests to verify the file paths for (const stageName of ['Stage1', 'Stage2']) { - const manifestArtifact = assembly.getNestedAssembly(`assembly-${stageName}`).artifacts.filter(isAssetManifestArtifact)[0]; + const manifestArtifact = assembly.getNestedAssembly(`assembly-${stageName}`).artifacts.filter(cxapi.AssetManifestArtifact.isAssetManifestArtifact)[0]; const manifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); expect(manifest.dockerImages[DEMO_IMAGE_ASSET_HASH].source).toEqual({ @@ -464,7 +464,3 @@ testFutureBehavior('nested assemblies share assets: default synth edition', flag function isStackArtifact(x: any): x is cxapi.CloudFormationStackArtifact { return x instanceof cxapi.CloudFormationStackArtifact; } - -function isAssetManifestArtifact(x: any): x is cxapi.AssetManifestArtifact { - return x instanceof cxapi.AssetManifestArtifact; -} diff --git a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts index f04218f95929d..23666d832f7e2 100644 --- a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts +++ b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts @@ -275,7 +275,7 @@ test('nested assemblies share assets: default synth edition', () => { // Read the asset manifests to verify the file paths for (const stageName of ['Stage1', 'Stage2']) { - const manifestArtifact = assembly.getNestedAssembly(`assembly-${stageName}`).artifacts.filter(isAssetManifestArtifact)[0]; + const manifestArtifact = assembly.getNestedAssembly(`assembly-${stageName}`).artifacts.filter(cxapi.AssetManifestArtifact.isAssetManifestArtifact)[0]; const manifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); expect(manifest.files[SAMPLE_ASSET_HASH].source).toEqual({ @@ -410,7 +410,3 @@ function mkdtempSync() { function isStackArtifact(x: any): x is cxapi.CloudFormationStackArtifact { return x instanceof cxapi.CloudFormationStackArtifact; } - -function isAssetManifestArtifact(x: any): x is cxapi.AssetManifestArtifact { - return x instanceof cxapi.AssetManifestArtifact; -} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/feature-flags.ts b/packages/@aws-cdk/core/lib/feature-flags.ts index 44fd5e138bdc7..2da5397b8ec23 100644 --- a/packages/@aws-cdk/core/lib/feature-flags.ts +++ b/packages/@aws-cdk/core/lib/feature-flags.ts @@ -1,5 +1,5 @@ import * as cxapi from '@aws-cdk/cx-api'; -import { IConstruct } from '../lib/construct-compat'; +import { IConstruct, Node } from 'constructs'; /** * Features that are implemented behind a flag in order to preserve backwards @@ -24,7 +24,7 @@ export class FeatureFlags { * module. */ public isEnabled(featureFlag: string): boolean | undefined { - const context = this.construct.node.tryGetContext(featureFlag); + const context = Node.of(this.construct).tryGetContext(featureFlag); if (cxapi.FUTURE_FLAGS_EXPIRED.includes(featureFlag)) { if (context !== undefined) { throw new Error(`Unsupported feature flag '${featureFlag}'. This flag existed on CDKv1 but has been removed in CDKv2.` diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts index 3c8c102f2c5ab..10ddea69b624e 100644 --- a/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts @@ -3,10 +3,33 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { CloudArtifact } from '../cloud-artifact'; import { CloudAssembly } from '../cloud-assembly'; +const ASSET_MANIFEST_ARTIFACT_SYM = Symbol.for('@aws-cdk/cx-api.AssetManifestArtifact'); + /** * Asset manifest is a description of a set of assets which need to be built and published */ export class AssetManifestArtifact extends CloudArtifact { + /** + * Checks if `art` is an instance of this class. + * + * Use this method instead of `instanceof` to properly detect `AssetManifestArtifact` + * instances, even when the construct library is symlinked. + * + * Explanation: in JavaScript, multiple copies of the `cx-api` library on + * disk are seen as independent, completely different libraries. As a + * consequence, the class `AssetManifestArtifact` in each copy of the `cx-api` library + * is seen as a different class, and an instance of one class will not test as + * `instanceof` the other class. `npm install` will not create installations + * like this, but users may manually symlink construct libraries together or + * use a monorepo tool: in those cases, multiple copies of the `cx-api` + * library can be accidentally installed, and `instanceof` will behave + * unpredictably. It is safest to avoid using `instanceof`, and using + * this type-testing method instead. + */ + public static isAssetManifestArtifact(art: any): art is AssetManifestArtifact { + return art && typeof art === 'object' && art[ASSET_MANIFEST_ARTIFACT_SYM]; + } + /** * The file name of the asset manifest */ @@ -36,3 +59,15 @@ export class AssetManifestArtifact extends CloudArtifact { this.bootstrapStackVersionSsmParameter = properties.bootstrapStackVersionSsmParameter; } } + +/** + * Mark all instances of 'AssetManifestArtifact' + * + * Why not put this in the constructor? Because this is a class property, + * not an instance property. It applies to all instances of the class. + */ +Object.defineProperty(AssetManifestArtifact.prototype, ASSET_MANIFEST_ARTIFACT_SYM, { + value: true, + enumerable: false, + writable: false, +}); diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 82233d7412b63..632d8357b9976 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -414,6 +414,7 @@ "./aws-autoscalingplans": "./aws-autoscalingplans/index.js", "./aws-backup": "./aws-backup/index.js", "./aws-batch": "./aws-batch/index.js", + "./aws-billingconductor": "./aws-billingconductor/index.js", "./aws-budgets": "./aws-budgets/index.js", "./aws-cassandra": "./aws-cassandra/index.js", "./aws-ce": "./aws-ce/index.js", diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index 60b883f13fb40..ebf40bbb7442b 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -405,7 +405,7 @@ export class CloudFormationDeployments { */ private async publishStackAssets(stack: cxapi.CloudFormationStackArtifact, toolkitInfo: ToolkitInfo) { const stackEnv = await this.sdkProvider.resolveEnvironment(stack.environment); - const assetArtifacts = stack.dependencies.filter(isAssetManifestArtifact); + const assetArtifacts = stack.dependencies.filter(cxapi.AssetManifestArtifact.isAssetManifestArtifact); for (const assetArtifact of assetArtifacts) { await this.validateBootstrapStackVersion( @@ -437,7 +437,3 @@ export class CloudFormationDeployments { } } } - -function isAssetManifestArtifact(art: cxapi.CloudArtifact): art is cxapi.AssetManifestArtifact { - return art instanceof cxapi.AssetManifestArtifact; -} diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 76b9386cc9550..d8f8a390677bf 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -299,6 +299,7 @@ async function prepareAndExecuteChangeSet( StackName: deployName, ChangeSetName: changeSetName, ChangeSetType: update ? 'UPDATE' : 'CREATE', + IncludeNestedStacks: true, Description: `CDK Changeset for execution ${executionId}`, TemplateBody: bodyParameter.TemplateBody, TemplateURL: bodyParameter.TemplateURL, diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 2b199ea225b87..d8eb55bf77eaa 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -164,6 +164,18 @@ test('correctly passes CFN parameters, ignoring ones with empty values', async ( })); }); +test('correctly passes IncludeNestedStacks', async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + }); + + // THEN + expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + IncludeNestedStacks: true, + })); +}); + test('reuse previous parameters if requested', async () => { // GIVEN givenStackExists({ diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 7f65950550cb4..627dc150c36bf 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -638,10 +638,13 @@ integTest('fast deploy', withDefaultFixture(async (fixture) => { const changeSet2 = await getLatestChangeSet(); expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); - // Deploy the stack again with --force, now we should create a changeset - await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + // Deploy the stack again with --force. This creates a changeset which will be + // empty (since CFN now tracks changes into nested stacks as well), so we delete + // it again because it couldn't be executed anyway. + const output = await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); const changeSet3 = await getLatestChangeSet(); - expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + expect(output).toContain('No changes are to be performed on'); + expect(changeSet3.ChangeSetId).toEqual(changeSet2.ChangeSetId); // Deploy the stack again with tags, expected to create a new changeset // even though the resources didn't change. diff --git a/packages/aws-cdk/test/integ/helpers/cdk.ts b/packages/aws-cdk/test/integ/helpers/cdk.ts index 75787ab7b747c..62a6abd08afce 100644 --- a/packages/aws-cdk/test/integ/helpers/cdk.ts +++ b/packages/aws-cdk/test/integ/helpers/cdk.ts @@ -720,11 +720,9 @@ export async function installNpmPackages(fixture: TestFixture, packages: Record< const installNpm7 = memoize0(async (): Promise => { const installDir = path.join(os.tmpdir(), 'cdk-integ-npm7'); await shell(['rm', '-rf', installDir]); - await shell(['mkdir', '-p', installDir]); + await shell(['mkdir', '-p', `${installDir}/node_modules`]); - await shell(['npm', 'install', - '--prefix', installDir, - 'npm@7']); + await shell(['npm', 'install', 'npm@7'], { cwd: installDir }); return path.join(installDir, 'node_modules', '.bin', 'npm'); });