CDK Stages are the closest thing AWS CloudFormation has to a built-in, first-class primitive for managing application deployments across multiple environments.
Let’s see this in action. Imagine a simple CDK app that deploys a S3 bucket.
# app.py
from aws_cdk import App, Environment
from my_s3_stack import MyS3Stack
app = App()
# Development environment
dev_env = Environment(account="111111111111", region="us-east-1")
MyS3Stack(app, "MyS3Dev", env=dev_env)
# Production environment
prod_env = Environment(account="222222222222", region="us-east-1")
MyS3Stack(app, "MyS3Prod", env=prod_env)
app.synth()
# my_s3_stack.py
from aws_cdk import Stack, aws_s3 as s3
from constructs import Construct
class MyS3Stack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
s3.Bucket(self, "MyBucket")
When you run cdk synth, it generates CloudFormation templates for each stack. If you wanted to deploy this to two environments, you’d typically have two separate stack definitions, perhaps conditionally deployed. Stages elegantly bundle these environment-specific deployments together.
Here’s how you’d refactor the above to use Stages:
# app.py
from aws_cdk import App, Environment
from cdk_staging_app.my_s3_stack import MyS3Stack
from cdk_staging_app.my_s3_stage import MyS3Stage # New Stage class
app = App()
# Development environment
dev_env = Environment(account="111111111111", region="us-east-1")
MyS3Stage(app, "Dev", env=dev_env)
# Production environment
prod_env = Environment(account="222222222222", region="us-east-1")
MyS3Stage(app, "Prod", env=prod_env)
app.synth()
# my_s3_stage.py
from aws_cdk import Stage, Environment
from my_s3_stack import MyS3Stack
class MyS3Stage(Stage):
def __init__(self, scope: Construct, construct_id: str, *, env: Environment, **kwargs):
super().__init__(scope, construct_id, env=env, **kwargs)
# Each instance of MyS3Stack deployed by this stage will have a unique stack ID
# within the CloudFormation account, e.g., "Dev-MyS3Stack-1234567890AB"
MyS3Stack(self, "MyS3Stack", env=env)
Now, when you run cdk deploy --all, the CDK CLI understands that "Dev" and "Prod" are distinct deployment targets, each containing a MyS3Stack. The cdk deploy command will synthesize and deploy the CloudFormation stacks for each stage separately. You can also deploy a specific stage: cdk deploy Dev.
The core problem Stages solve is managing the lifecycle of multiple, related stacks that represent different environments of the same application. Without Stages, you’d be manually orchestrating deployments, potentially using separate CDK apps or complex conditional logic within a single app. Stages provide a structured, idiomatic way to define and deploy these multi-environment configurations. Each Stage instance in your app.py becomes a distinct CloudFormation stack set (or a series of individual stacks, depending on your CDK version and configuration) that the CDK CLI can manage. This separation is crucial for maintaining distinct configurations, IAM roles, and deployment pipelines per environment.
When you deploy a Stage, the CDK CLI treats it as a unit. It synthesizes all the CloudFormation templates for the stacks within that stage and then deploys them. The env parameter on the Stage constructor is key here; it dictates the target AWS account and region for all stacks within that stage. This allows you to define your application’s core infrastructure once in MyS3Stack and then instantiate it with different environmental configurations in app.py via MyS3Stage.
The mental model here is that your App object is the root. Within the App, you define Stages. Each Stage is a logical grouping of one or more Stacks that are deployed together to a specific env (account/region). This creates a clear hierarchy: App -> Stage -> Stack. You can then target specific stages for deployment.
The most surprising thing about Stages is that they are also Constructs. This means you can nest Stages within other Stages. This is how you’d typically build complex applications with multiple environments and multiple distinct services per environment. For example, you might have a PipelineStage that deploys your CI/CD pipeline, and within that pipeline stage, you deploy multiple application stages (Dev, Staging, Prod) for different services.
A common misconception is that Stages are purely for defining different environments (dev, prod). While that’s their primary use case, they are fundamentally about grouping stacks that share a common deployment target and lifecycle. You could, for example, have a single App that defines two Stages for the same environment, but each Stage deploys a different set of independent services. This is less common but demonstrates the flexibility of the Stage construct.
The next concept you’ll likely run into is how to integrate Stage deployments into a CI/CD pipeline, often using cdk-pipelines.