Overview
This article summarizes effective architectural patterns for reusing the same stack across different environments in AWS CDK.
- Stack creation patterns in AWS CDK
- Use cases where AWS CDK Stage is suitable and other considerations
- Use cases suited for static vs. dynamic stack creation methods
Stack Creation Patterns
When limited to cases where you want to reuse the same stack across different environments, here are three stack creation pattern examples:
- Pattern for creating the same stack statically per environment
- Pattern for creating the same stack dynamically per environment
- Pattern utilizing CDK Stage
Pattern for Creating the Same Stack Statically per Environment
new SampleStack(app, 'DevSampleStack', devProps);
new SampleStack(app, 'StgSampleStack', stgProps);
new SampleStack(app, 'PrdSampleStack', prdProps);
Pattern for Creating the Same Stack Dynamically per Environment
const env: string = app.node.tryGetContext('env') ?? 'dev';
const props = getStackProps(env);
new SampleStack(app, 'SampleStack', props);
// Managed in a separate file (config.ts or parameter.ts, etc.)
const getStackProps = (env: string): SampleStackProps => {
switch (env) {
case 'dev':
return {
key: 'dev-key',
env: {
account: '111111111111',
region: 'us-east-1',
},
};
case 'stg':
return {
key: 'stg-key',
env: {
account: '222222222222',
region: 'us-east-1',
},
};
case 'prd':
return {
key: 'prd-key',
env: {
account: '333333333333',
region: 'us-east-1',
},
};
default:
throw new Error(`Invalid environment: ${env}`);
}
};
Pattern Utilizing CDK Stage
export class MyStage extends Stage {
constructor(scope: Construct, id: string, props?: MyStageProps) {
super(scope, id, props);
new SampleStack(this, 'SampleStack', props); // In practice, props would be formatted for StackProps before passing
}
}
new MyStage(app, 'DevStage', devProps);
new MyStage(app, 'StgStage', stgProps);
new MyStage(app, 'PrdStage', prdProps);
Use Cases Where Stage is Suitable
Personally, I find stages convenient for "creating multi-stack configurations statically per environment".
Without stages in this scenario, the app file would contain calls equal to the number of stacks × number of stages, making the code complex and increasing cognitive load.
// my-stage.ts
export class MyStage extends Stage {
constructor(scope: Construct, id: string, props?: MyStageProps) {
super(scope, id, props);
new SampleStack1(this, 'SampleStack1', props); // In practice, props would be formatted for StackProps before passing
new SampleStack2(this, 'SampleStack2', props); // In practice, props would be formatted for StackProps before passing
new SampleStack3(this, 'SampleStack3', props); // In practice, props would be formatted for StackProps before passing
}
}
// app file
new MyStage(app, 'DevStage', devProps);
new MyStage(app, 'StgStage', stgProps);
new MyStage(app, 'PrdStage', prdProps);
On the other hand, for single-stack cases or when creating stacks dynamically, I don't really use stages much.
However, when creating stacks statically, if there's a prediction that you might move from single-stack to multi-stack in the future or you want to ensure extensibility, it's good to create stages in advance.
Since there aren't many cases where "creating stages causes losses," I think it's viable to create stages from the beginning.
Introducing Stage Later is Also OK
It's also possible to introduce stages later.
When using stages, stack names are created according to the rule ${stageName}-${stackID}
(e.g., MyStage-SampleStack), so just introducing stages will change the stack names. This means new stack creation processes will run (deletion processes for the original stacks won't run).
However, since StackProps has a stackName
property, by explicitly specifying this (specifying the previous stack name), you can prevent the ${stageName}-
prefix from being added to the stack name, avoiding new stack creation and maintaining the existing stack.
export class MyStage extends Stage {
constructor(scope: Construct, id: string, props?: MyStageProps) {
super(scope, id, props);
new SampleStack(this, 'SampleStack', {
...props,
stackName: 'SampleStack',
});
}
}
Won't Existing Resources Be Rebuilt When Introducing Stage Later?
Generally, those familiar with CDK know that when you change a construct's path (moving one construct under another), the resource's logical ID changes and rebuilding occurs.
When placing stacks under stages as in this case, you might wonder if the construct path changes and resources get rebuilt.
However, since resource logical IDs are calculated from paths below the stack level, logical IDs won't change for MOST resources.
* That said, the Metadata
for each resource in the CloudFormation template ("aws:cdk:path": "MyStage/SampleStack/MyTopic/Resource"
) will change, so CloudFormation will apply UPDATEs to resources, but there won't be rebuilding.
But some resources will be replaced if not explicitly changed. Therefore, any migration to a stage should be thoroughly checked using snapshot tests, etc.
Cases to Be Careful with CDK Stage
First, when moving stacks that didn't use stages under stages, if any resources originally used construct paths (this.node.path
) or addresses (this.node.addr
) (unique values calculated from paths) to generate physical names or similar, this would cause impacts and unexpected resource changes.
To prevent this, you would either avoid using such features to generate strings (like physical names) yourself, or write logic that calculates from paths without including stages, similar to CDK's internal logical name generation logic.
Additionally, there are potential pitfalls with specifying deployment target stacks in the cdk deploy
command. (However, this is temporary, so it's not really a disadvantage.)
# Even with only 1 stack, you get this error
❯ cdk deploy
Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`
Stacks: MyStage/SampleStack
# It says to specify --all but...
❯ cdk deploy --all
No stack found in the main cloud assembly. Use "list" to print manifest
# Stage specification alone doesn't work
❯ cdk deploy MyStage
No stacks match the name(s) MyStage
# Just adding a wildcard at the end doesn't work either
❯ cdk deploy MyStage*
No stacks match the name(s) MyStage*
# Success!!
❯ cdk deploy MyStage/*
# Success!!
❯ cdk deploy MyStage/SampleStack
Static vs. Dynamic: Which is Better?
Cases Where Static is Suitable
- When you want to visually understand the stack list from the app file, or when you want to output the stack list with
cdk list
- When you want to check if errors occur in other environments even during synth or deploy of one environment
- As part of CDK's lifecycle, even when synth or deploy targets are limited to some stacks, synthesis processing for all stages and stacks in the app runs
- This means when deploying StageA, if there's code that would cause errors in StageB's stack, StageA's deployment will also error
- When you want to explicitly include per-environment stack information in the app configuration/tree
- = When utilizing cloud assembly (
cdk.out
) - Even when synthesizing or deploying only one environment, information for all environments is generated as cloud assembly
- = When utilizing cloud assembly (
- When you don't want to use context like
cdk deploy -c ENV=dev
Cases Where Dynamic is Suitable
- When you want to create simple app files without using Stage
- When you want to create not just 3 types like dev, stg, prd, but an unspecified number of stacks
- Example: When you want to create multiple personal environments in dev
- dev-goto1-api-stack
- dev-goto2-api-stack
- dev-main-api-stack
- However, even with static creation, the same thing is possible by passing only personal or main information from context (-c), but that could be considered dynamic
- When you want to deploy per environment without specifying stack names in the
cdk deploy
command- Cases where stack names are long and painful (
-c ENV=dev
is shorter)
- Cases where stack names are long and painful (
Conclusion
I think knowing when to use Stage will surely come in handy someday.
Top comments (2)
Putting existing stacks into stage can defo change IDs as there are known bugs in CDK. Notably a SecurityGroupIngress changes which is so common. But there are others.
Thanks for the comment! It's very important thing. I added an easy description for the notification for now.
If I find more detailed resources later, I will add them as well.