diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 3b013f3ff990e..8c5ce270d8dc1 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -843,3 +843,11 @@ IAM operator, we need it in the *key* of a `StringEquals` condition. JSON keys *must be* strings, so to circumvent this limitation, we use `CfnJson` to "delay" the rendition of this template section to deploy-time. This means that the value of `StringEquals` in the template will be `{ "Fn::GetAtt": [ "ConditionJson", "Value" ] }`, and will only "expand" to the operator we synthesized during deployment. + +### Stack Resource Limit + +When deploying to AWS CloudFormation, it needs to keep in check the amount of resources being added inside a Stack. Currently it's possible to check the limits in the [AWS CloudFormation quotas](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html) page. + +It's possible to synthesize the project with more Resources than the allowed (or even reduce the number of Resources). + +Set the context key `@aws-cdk/core:stackResourceLimit` with the proper value, being 0 for disable the limit of resources. diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index e1664e2996a90..c6a8a56916f2f 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -27,8 +27,12 @@ import { Construct as CoreConstruct } from './construct-compat'; const STACK_SYMBOL = Symbol.for('@aws-cdk/core.Stack'); const MY_STACK_CACHE = Symbol.for('@aws-cdk/core.Stack.myStack'); +export const STACK_RESOURCE_LIMIT_CONTEXT = '@aws-cdk/core:stackResourceLimit'; + const VALID_STACK_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-]*$/; +const MAX_RESOURCES = 500; + export interface StackProps { /** * A description of the stack. @@ -753,6 +757,17 @@ export class Stack extends CoreConstruct implements ITaggable { // write the CloudFormation template as a JSON file const outPath = path.join(builder.outdir, this.templateFile); + + if (this.maxResources > 0) { + const resources = template.Resources || {}; + const numberOfResources = Object.keys(resources).length; + + if (numberOfResources > this.maxResources) { + throw new Error(`Number of resources: ${numberOfResources} is greater than allowed maximum of ${this.maxResources}`); + } else if (numberOfResources >= (this.maxResources * 0.8)) { + Annotations.of(this).addInfo(`Number of resources: ${numberOfResources} is approaching allowed maximum of ${this.maxResources}`); + } + } fs.writeFileSync(outPath, JSON.stringify(template, undefined, 2)); for (const ctx of this._missingContext) { @@ -907,6 +922,16 @@ export class Stack extends CoreConstruct implements ITaggable { }; } + /** + * Maximum number of resources in the stack + * + * Set to 0 to mean "unlimited". + */ + private get maxResources(): number { + const contextLimit = this.node.tryGetContext(STACK_RESOURCE_LIMIT_CONTEXT); + return contextLimit !== undefined ? parseInt(contextLimit, 10) : MAX_RESOURCES; + } + /** * Check whether this stack has a (transitive) dependency on another stack * diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index 63c04be2e81de..581f4ed003895 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -45,6 +45,68 @@ nodeunitShim({ test.done(); }, + 'when stackResourceLimit is default, should give error'(test: Test) { + // GIVEN + const app = new App({}); + + const stack = new Stack(app, 'MyStack'); + + // WHEN + for (let index = 0; index < 1000; index++) { + new CfnResource(stack, `MyResource-${index}`, { type: 'MyResourceType' }); + } + + test.throws(() => { + app.synth(); + }, 'Number of resources: 1000 is greater than allowed maximum of 500'); + + test.done(); + }, + + 'when stackResourceLimit is defined, should give the proper error'(test: Test) { + // GIVEN + const app = new App({ + context: { + '@aws-cdk/core:stackResourceLimit': 100, + }, + }); + + const stack = new Stack(app, 'MyStack'); + + // WHEN + for (let index = 0; index < 200; index++) { + new CfnResource(stack, `MyResource-${index}`, { type: 'MyResourceType' }); + } + + test.throws(() => { + app.synth(); + }, 'Number of resources: 200 is greater than allowed maximum of 100'); + + test.done(); + }, + + 'when stackResourceLimit is 0, should not give error'(test: Test) { + // GIVEN + const app = new App({ + context: { + '@aws-cdk/core:stackResourceLimit': 0, + }, + }); + + const stack = new Stack(app, 'MyStack'); + + // WHEN + for (let index = 0; index < 1000; index++) { + new CfnResource(stack, `MyResource-${index}`, { type: 'MyResourceType' }); + } + + test.doesNotThrow(() => { + app.synth(); + }); + + test.done(); + }, + 'stack.templateOptions can be used to set template-level options'(test: Test) { const stack = new Stack();