What is an AWS CDK Stack?
An AWS CDK stack is the smallest single unit of deployment in the AWS Cloud Development Kit. It represents a collection of AWS resources that you define using CDK constructs and deploy together as a single CloudFormation stack.
When you run cdk deploy, each stack in your CDK app produces a CloudFormation template that gets deployed to a specific AWS account and region. This means everything in a stack lives or dies together: deploy together, update together, delete together.
Here's the key insight that trips up newcomers: CDK stacks are conceptually different from how CloudFormation templates are normally used. While CloudFormation templates can be deployed multiple times with different parameters, CDK takes an approach where concrete templates are resolved at synthesis time. This enables idiomatic and natural usage of programming languages, with resolution happening during synthesis rather than deployment.
You can define any number of stacks in your AWS CDK app. Think of stacks as deployment boundaries, each representing a distinct piece of infrastructure you want to manage independently.
CDK Stacks vs CloudFormation Stacks
CDK stacks map one-to-one to CloudFormation stacks. Every CDK stack you define synthesizes into exactly one CloudFormation template. This relationship has important implications:
- Same limits apply: CloudFormation's 500-resource limit per stack applies to your CDK stacks
- Same lifecycle: CloudFormation handles the actual resource creation, updates, and deletions
- Same rollback mechanics: If deployment fails, CloudFormation rolls back all changes in the stack
The difference is in how you define them. Instead of writing verbose JSON or YAML, you use programming languages with all their power: loops, conditionals, type checking, and abstractions.
The CDK Hierarchy: Apps, Stacks, and Constructs
Understanding the CDK hierarchy helps you structure your infrastructure code effectively. The relationship flows from App to Stack to Construct to AWS Resource:
At the top sits the App, which is your CDK application entry point. It contains one or more Stacks, each representing a deployment unit. Within stacks, you define CDK Constructs, which are the building blocks representing AWS resources or groups of resources. For a complete understanding of how constructs, stacks, and apps work together, see our comprehensive AWS CDK guide.
The construct hierarchy uses three levels:
- L1 (CFN Resources): Direct CloudFormation resource mappings with no abstraction
- L2 (Curated Constructs): Higher-level abstractions with sensible defaults and helper methods
- L3 (Patterns): Pre-built architectures that combine multiple resources for common use cases
Stacks themselves are constructs, specifically root constructs that represent a single CloudFormation stack.
How CDK Stacks Work: The Lifecycle
Understanding the CDK lifecycle helps you debug issues and write better infrastructure code. When synthesis is performed, the CDK app runs through three distinct phases that transform your TypeScript code into deployed AWS resources.
Construction Phase
The construction phase is where your CDK code actually runs. During this phase:
- All constructs are instantiated
- The constructor chain executes from App down through Stacks and Constructs
- Most of your application code runs here
- The construct tree is built in memory
This is the phase where you have the most control. Your loops execute, your conditionals evaluate, and your abstractions expand into concrete resources.
Synthesis Phase
Synthesis transforms your construct tree into CloudFormation templates. Running cdk synth triggers this phase:
cdk synth
The output goes to the cdk.out directory, which contains:
- CloudFormation templates (one per stack)
- Asset files (Lambda code, Docker images, etc.)
- Manifest files describing the deployment
I recommend running cdk synth separately before deployment. It validates your CDK app and catches errors before you touch your AWS environment.
When synthesizing an app with multiple stacks, the cloud assembly includes a separate template for each stack instance. You can synthesize individual stacks by name:
cdk synth MyStackName
Deployment Phase
The deployment phase is where CloudFormation takes over. Running cdk deploy uploads assets to the CDK bootstrap bucket and creates or updates CloudFormation stacks.
During deployment:
- Assets are uploaded to S3
- CloudFormation templates are submitted
- CloudFormation provisions actual AWS resources
- Rollback occurs if any resource creation fails
The key insight here: CDK's job ends once synthesis completes. From that point forward, you're working with standard CloudFormation mechanics.
Stack Properties and Configuration
Every CDK stack can be configured through StackProps. Understanding these properties helps you build production-ready infrastructure.
Environment Configuration
The env property determines where your stack deploys. You have two approaches:
Environment-specific stacks (recommended for production):
new MyStack(app, 'ProdStack', {
env: {
account: '123456789012',
region: 'us-east-1',
},
});
Environment-agnostic stacks (useful for development):
new MyStack(app, 'DevStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
Environment-agnostic stacks can deploy to any account/region but have limitations. For example, availabilityZones always returns an array with only two Availability Zones because the CDK cannot know the actual number at synthesis time.
Stack Naming
By default, a stack's physical name in CloudFormation comes from its construct ID. You can override this:
new MyStack(app, 'MyStack', {
stackName: 'production-api-stack',
});
Be careful with explicit stack names. Changing them after deployment creates a new stack rather than updating the existing one.
Stack Tags
Tags flow through your entire stack to every resource:
import { Tags } from 'aws-cdk-lib';
const stack = new MyStack(app, 'MyStack');
Tags.of(stack).add('Environment', 'Production');
Tags.of(stack).add('Project', 'CustomerAPI');
For modern CDK apps, set the @aws-cdk/core:explicitStackTags feature flag to true in your cdk.json. This gives you explicit control over stack tagging behavior.
Creating Your First CDK Stack
Now that you understand the concepts, let's build a complete stack. If you haven't already, install AWS CDK first.
Basic Stack Structure
A CDK stack is defined by extending the Stack class. The constructor receives three parameters:
- scope: The parent construct (typically the App)
- id: A unique identifier within the scope
- props: Optional configuration via
StackProps
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
export interface MyStackProps extends cdk.StackProps {
environmentName: string;
}
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MyStackProps) {
super(scope, id, props);
// Define your AWS resources here
}
}
The interface pattern for props is a TypeScript best practice. It makes your stack's configuration explicit and type-safe.
Stack Instantiation
Your stack gets instantiated in the CDK app entry point:
import * as cdk from 'aws-cdk-lib';
import { MyStack } from './lib/my-stack';
const app = new cdk.App();
new MyStack(app, 'MyStack', {
environmentName: 'production',
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
app.synth();
The app.synth() call triggers synthesis. Without it, your templates won't generate.
Complete Example
Here's a practical stack that creates an S3 bucket with proper configuration:
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
export interface StorageStackProps extends cdk.StackProps {
bucketPrefix: string;
enableVersioning?: boolean;
}
export class StorageStack extends cdk.Stack {
public readonly bucket: s3.Bucket;
constructor(scope: Construct, id: string, props: StorageStackProps) {
super(scope, id, props);
this.bucket = new s3.Bucket(this, 'DataBucket', {
bucketName: `${props.bucketPrefix}-${this.account}-${this.region}`,
versioned: props.enableVersioning ?? true,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
new cdk.CfnOutput(this, 'BucketName', {
value: this.bucket.bucketName,
description: 'The name of the S3 bucket',
});
}
}
Notice how the bucket is exposed as a public property. This pattern enables sharing resources across stacks.
Working with Multiple Stacks
Real-world applications rarely fit in a single stack. Multiple stacks let you separate concerns, manage dependencies, and deploy independently.
Stack Dependencies
When one stack references a resource from another, CDK automatically creates a dependency:
const networkStack = new NetworkStack(app, 'NetworkStack');
const appStack = new ApplicationStack(app, 'AppStack', {
vpc: networkStack.vpc,
});
This means:
NetworkStackdeploys beforeApplicationStack- Running
cdk deploy AppStackautomatically deploysNetworkStackfirst - CloudFormation outputs and imports wire everything together
You can also add dependencies explicitly using the DependsOn relationship:
appStack.addDependency(networkStack);
Cross-Stack References
When you pass a resource from one stack to another, CDK automatically:
- Creates a CloudFormation export in the source stack
- Creates an
Fn::ImportValuein the consuming stack - Establishes the deployment dependency
// In NetworkStack
export class NetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'Vpc');
}
}
// In ApplicationStack
export interface ApplicationStackProps extends cdk.StackProps {
vpc: ec2.Vpc;
}
export class ApplicationStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: ApplicationStackProps) {
super(scope, id, props);
new ecs.Cluster(this, 'Cluster', {
vpc: props.vpc,
});
}
}
Be aware: cross-stack references create coupling. Once established, you cannot easily remove the export from the source stack while the consuming stack still exists. Plan your stack boundaries carefully.
Nested Stacks
Nested stacks provide a way around CloudFormation's 500-resource limit. A nested stack counts as only one resource in the parent stack but can contain up to 500 resources of its own.
When to Use Nested Stacks
Consider nested stacks when:
- You're approaching the 500-resource limit
- You want to organize related resources into logical groups
- You need to share common infrastructure patterns
However, nested stacks have trade-offs:
- They're not listed by
cdk list - They can't be deployed independently with
cdk deploy - Security posture changes aren't displayed before deployment
For most use cases, I recommend splitting into separate top-level stacks rather than using nested stacks. Reserve nested stacks for when you truly need to exceed limits.
Implementation
Creating a nested stack:
import { NestedStack, NestedStackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class DatabaseNestedStack extends NestedStack {
constructor(scope: Construct, id: string, props?: NestedStackProps) {
super(scope, id, props);
// Define resources for the nested stack
}
}
// In the parent stack
export class ParentStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new DatabaseNestedStack(this, 'DatabaseStack');
}
}
The key difference: the scope for a NestedStack must be a Stack or another NestedStack, not an App.
Stack Limits and Quotas
CloudFormation enforces hard limits that affect your CDK stack design. Understanding these prevents deployment failures.
500 Resource Limit
Each CloudFormation stack can contain a maximum of 500 resources. This limit is deceptive because CDK constructs often create multiple CloudFormation resources:
- An L2
Bucketwith access logging creates 2 resources - An L3
ApplicationLoadBalancedFargateServicecan create 50+ resources - Real-world serverless applications typically generate 5-8 resources per API endpoint
The CDK issues a warning when your stack exceeds 80% of the limit:
[Warning at /MyStack] This stack is approaching the resource limit (425/500). Consider splitting the stack.
You can configure this threshold:
new MyStack(app, 'MyStack', {
maxResources: 400, // Custom warning threshold
});
Or disable validation entirely (not recommended):
new MyStack(app, 'MyStack', {
maxResources: 0, // Disables resource counting
});
Solutions for Exceeding Limits
When you hit the limit, you have options:
- Split into multiple stacks: The cleanest solution. Separate by domain (networking, compute, storage) or service boundary
- Use nested stacks: Each nested stack gets its own 500-resource limit
- Reduce template size: Enable
suppressTemplateIndentationfor large templates approaching the 1MB template size limit
new MyStack(app, 'MyStack', {
suppressTemplateIndentation: true,
});
Best Practices
Following these patterns will save you headaches as your CDK applications grow. For comprehensive best practices covering testing, security, CI/CD, and more, see our complete CDK best practices guide.
Model with Constructs, Deploy with Stacks
This is the most important CDK best practice: constructs model your application; stacks define deployment boundaries.
Don't create a stack for every logical component. Instead:
- Build reusable constructs that encapsulate related resources
- Compose those constructs into stacks based on deployment needs
- Use stacks to separate things that have different lifecycles
For example, if you're building a web application with frontend, backend, and database components, create constructs for each, then compose them into stacks based on how you want to deploy:
// Constructs for modeling
class ApiConstruct extends Construct { /* ... */ }
class DatabaseConstruct extends Construct { /* ... */ }
class FrontendConstruct extends Construct { /* ... */ }
// Stacks for deployment
class ProductionStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new DatabaseConstruct(this, 'Database');
new ApiConstruct(this, 'Api');
new FrontendConstruct(this, 'Frontend');
}
}
Stack Organization Patterns
For growing CDK projects, consider organizing stacks by these patterns. For detailed guidance on choosing the right organization strategy, see our CDK best practices guide:
Service boundaries: Each microservice gets its own stack
new UserServiceStack(app, 'UserService', { env });
new OrderServiceStack(app, 'OrderService', { env });
new PaymentServiceStack(app, 'PaymentService', { env });
Infrastructure layers: Separate shared infrastructure from application-specific resources
new NetworkStack(app, 'Network', { env });
new SecurityStack(app, 'Security', { env });
new ApplicationStack(app, 'Application', { env });
Environment groupings: Use a construct to instantiate multiple stacks per environment
class ServiceStacks extends Construct {
constructor(scope: Construct, id: string, envName: string) {
super(scope, id);
new ControlPlane(this, 'ControlPlane');
new DataPlane(this, 'DataPlane');
new Monitoring(this, 'Monitoring');
}
}
new ServiceStacks(app, 'Dev', 'dev');
new ServiceStacks(app, 'Prod', 'prod');
For a production-ready example of stack organization, check out the AWS CDK Starter Kit. It demonstrates best practices for project structure, stack composition, and reusable patterns.
Common Mistakes to Avoid
After working with many CDK projects, these are the pitfalls I see most often:
Cyclic dependencies: If Stack A references Stack B, Stack B cannot reference Stack A. Design your stack boundaries to have one-way dependencies.
Hardcoded account/region in shared code: Use environment variables or context for values that differ between deployments. Hardcoding creates stacks that only work in one environment.
Changing stack names after deployment: This creates a new stack rather than updating the existing one. Your resources get orphaned in the old stack.
Ignoring the 500-resource warning: That warning exists for a reason. Plan your stack split before you hit the hard limit, not after.
Over-relying on cross-stack references: Every cross-stack reference creates tight coupling. If you find yourself with a web of inter-stack dependencies, reconsider your stack boundaries.
Skipping environment-specific configuration for production: Environment-agnostic stacks are convenient for development but limit what CDK can do. Production stacks should have explicit account and region configuration.
Summary
AWS CDK stacks are the fundamental deployment units that transform your TypeScript code into CloudFormation templates and ultimately into AWS resources. They map one-to-one with CloudFormation stacks, inheriting both the capabilities and limitations of CloudFormation.
The key takeaways:
- Stacks are deployment boundaries, everything in a stack deploys together
- The lifecycle flows from construction through synthesis to deployment
- Configure stacks with environment, naming, and tagging for production readiness
- Use multiple stacks for separation of concerns, but be mindful of cross-stack coupling
- Respect the 500-resource limit and plan your stack boundaries accordingly
- Model with constructs, deploy with stacks
Now that you understand CDK stacks, you might want to explore CDK constructs to learn how to build reusable infrastructure components, or check out sharing resources across stacks for practical multi-stack patterns.
Have questions about structuring your CDK application? Drop a comment below.
Build Scalable CDK Apps That Are Easy to Maintain
Transform your complex CDK codebase into a structured, reusable architecture. Get real-world expertise from someone who's built production CDK at scale.
![What is an AWS CDK Stack? Complete Guide with Examples [2026]](/_next/image?url=%2Fimages%2Fblog%2Faws-cdk-stack%2Faws-cdk-stack.webp&w=3840&q=70)

