DEV Community

Kenta Goto for AWS Heroes

Posted on

2 1 1 1 1

How to Use AWS CDK Stage and When to Choose Static vs Dynamic Stack Creation

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);
Enter fullscreen mode Exit fullscreen mode

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}`);
  }
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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',
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 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)

Conclusion

I think knowing when to use Stage will surely come in handy someday.

Top comments (2)

Collapse
 
m0ltzart profile image
Roman

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.

Collapse
 
k_goto profile image
Kenta Goto • Edited

Thanks for the comment! It's very important thing. I added an easy description for the notification for now.

logical IDs won't change for MOST resources.

But some resources will be replaced if not explicitly changed. Therefore, any migration to a stage should be thoroughly checked using snapshot tests, etc.

If I find more detailed resources later, I will add them as well.