AWS CDK Development: Best Practices and Workflow
In the previous parts of this series, we covered Part1: AWS CDK fundamentals and Part2: components and commands. Now, let's explore how to structure your CDK projects effectively and implement best practices that will help you build maintainable infrastructure code.
AWS CDK Development Workflow
The typical AWS CDK development workflow follows these steps:
- Initialize a project with
cdk init - Edit the App definition to define your stacks
- Create stack definitions with the resources you need
- Write tests for your infrastructure code
- For the first deployment:
- Run
cdk bootstrapto prepare your AWS environment - Execute
cdk deployto deploy your resources
- Run
- For subsequent deployments:
- (Optional) Run
cdk diffto check for changes - Execute
cdk deployto update your resources
- (Optional) Run
Let's explore what you need to get started and how to organize your projects for maximum efficiency.
Prerequisites for AWS CDK Development
Knowledge Requirements
Before diving into AWS CDK, it helps to have:
- A solid understanding of AWS services and concepts (AWS Solutions Architect Associate level is ideal)
- Experience with CloudFormation templates
- Programming experience in at least one of the supported languages
Technical Requirements
At minimum, you'll need:
- AWS CLI v2 installed and configured
- Node.js (even if you're using Python, Java, or another language for your CDK code)
- A text editor or IDE
For a more productive development experience, consider:
- Git for version control
- An IDE like VS Code with AWS CDK extensions
- Docker for local testing
- A CI/CD pipeline for automated deployments
Recommended Project Structure
When I first started with CDK, I used the default structure from cdk init.
After managing several production deployments, I evolved to this more organized
approach that has served me well across multiple projects:
project-root/
├─ bin/ # App definition (entry point)
├─ lib/
│ ├─ stages/ # Stage definitions
│ ├─ stacks/ # Stack definitions
│ ├─ aspects/ # Apply policies consistently across resources
│ ├─ constructs/ # Build reusable, standardized infrastructure components
│ ├─ types/ # Ensure type safety and clear contracts between components
│ ├─ utils/ # Shared utility functions
├─ parameters/ # Environment-specific configuration files
├─ src/ # Lambda functions, HTML, and other source files
├─ test/ # Test files
│ ├─ snapshot/ # Snapshot tests
│ ├─ compliance/ # Security and compliance tests
| ├─ validation/ # validation tests
│ ├─ unit/ # Unit tests
│ ├─ integration/ # integration tests
├─ node_modules/
├─ cdk.context.json # Context values used by CDK
├─ cdk.json # CDK configuration
├─ jest.config.js # Test configuration
├─ package.json
├─ tsconfig.json
├─ README.md
The key advantages of this structure:
- Clear separation of concerns: Each directory has a specific purpose
- Dynamic configuration: Environment-specific settings in the parameters directory
- Reusable components: Common resources in dedicated directories
- Organized asset management: Application code separate from infrastructure code
Stack Organization Strategies
One critical decision in CDK development is how to organize your resources into stacks. While CloudFormation has a limit of 500 resources per stack(see CloudFormation quotas), the decision to split or combine stacks shouldn't be driven by technical limits alone.
When to Keep Resources in a Single Stack
As a general rule, you should prefer single, cohesive stacks unless you have a specific reason to split them. Resources that share the same lifecycle are good candidates to keep together.
Benefits of single stacks:
- Simpler deployment and rollback
- Atomic updates (all or nothing)
- No cross-stack reference complications
- Easier to understand resource relationships
When to Split Resources into Multiple Stacks
Consider splitting stacks when:
-
Resource lifecycles differ significantly
- Example: You want to preserve storage resources even if you delete application resources
-
You approach CloudFormation limits
- CloudFormation quotas include limits on template size, number of resources, etc.
-
You need different deployment frequencies
- Example: Network infrastructure rarely changes, while application resources change frequently
-
You need to apply different security or compliance controls
- Example: Production database stack requires stricter approval processes than development stacks
The Challenge of Cross-Stack References
One major consideration when splitting stacks is managing cross-stack references. While AWS CDK makes these references easier to work with, they still create dependencies that can complicate stack updates and deletions.
For example, if Stack B references a resource in Stack A, you can't delete Stack A while Stack B is still using that resource—leading to a potential "deadlock" situation.
To resolve cross-stack reference issues:
- Use exports and imports sparingly
- Consider using parameters for values that might be shared
- Design for independence where possible
- Document dependencies between stacks clearly
Construct ID Naming Conventions
The Construct ID is the second parameter when creating a construct:
const bucket = new s3.Bucket(this, "MyBucket", {});
Following consistent naming conventions for Construct IDs improves readability and maintenance:
Rule 1: Use PascalCase
Prefer PascalCase (e.g., DatabaseCluster, ApiGateway) for Construct IDs. This aligns with CloudFormation's conventions for logical IDs.
// Good
const userTable = new dynamodb.Table(this, "UserTable", {...});
// Less ideal
const userTable = new dynamodb.Table(this, "user_table", {...});
Tip: When generating Construct IDs dynamically from configuration files or external sources, use the change-case-commonjs library to ensure consistent naming:
import { pascalCase } from 'change-case-commonjs';
// Convert from various formats to PascalCase
const resourceName = pascalCase('user_profile_table'); // "UserProfileTable"
const bucket = new s3.Bucket(this, resourceName, {...});
Rule 2: Keep Names Simple and Descriptive
Avoid overly long or complex names. CDK truncates IDs at 240 characters when generating CloudFormation logical IDs.
// Good
const prodDatabase = new rds.DatabaseInstance(this, "ProductionDatabase", {...});
// Too verbose
const prodDatabase = new rds.DatabaseInstance(this, "ProductionMySQLDatabaseInstanceForUserManagementSystem", {...});
Rule 3: Avoid Redundancy in Nested Constructs
This one trips up a lot of developers (including me when I was starting out!).
When creating custom constructs that will be nested, avoid repeating the same
information in IDs.
// Avoid redundancy like this:
export class DatabaseConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
// "DatabaseDatabase" becomes redundant in the generated logical ID
this.database = new rds.DatabaseInstance(this, 'Database', {...});
}
}
// Usage:
const db = new DatabaseConstruct(this, 'Database');
From Construct ID to Logical ID
It's helpful to understand how CDK transforms your Construct IDs into CloudFormation logical IDs:
const logBucket = new s3.Bucket(this, 'LogsBucket', {});
In the generated CloudFormation template:
"Resources": {
"LogsBucket9C4D8843": {
"Type": "AWS::S3::Bucket",
"Properties": {
// ...
}
}
}
The logical ID LogsBucket9C4D8843 is created by combining your Construct ID with a hash suffix that ensures uniqueness within the CloudFormation template.
Dynamic Parameters and Environment Management
When working with multiple environments (development, testing, production), you need a way to parameterize your infrastructure code.
Passing Parameters to CDK Commands
You can pass context values to CDK commands using the -c flag:
cdk deploy -c env=prod -c region=us-east-1
And access these values in your code:
const environment = scope.node.tryGetContext('env') || 'dev';
const region = scope.node.tryGetContext('region') || process.env.CDK_DEFAULT_REGION;
Environment-Specific Validations
You can implement safety checks to prevent accidental deployments:
// Environment validation
const envName = app.node.tryGetContext('env');
const validEnvs = ['dev', 'test', 'stage', 'prod'];
if (!validEnvs.includes(envName)) {
console.error(`Invalid environment specified. Please use: ${validEnvs.join(', ')}`);
process.exit(1);
}
// Visual warning for production deployments
const isProduction = envName === 'prod';
if (isProduction) {
console.log('\x1b[31m'); // Red text
console.log('!!!!!!!!!! CAUTION !!!!!!!!!!');
console.log(' DEPLOYING TO PRODUCTION ');
console.log('!!!!!!!!!! CAUTION !!!!!!!!!!');
console.log('\x1b[0m'); // Reset color
}
Preventing Stack Deletion
For critical infrastructure stacks, you can enable termination protection:
const databaseStack = new DatabaseStack(app, 'ProductionDatabase', {
terminationProtection: true, // Prevents accidental deletion
// ...other properties
});
To delete this stack, you'll need to first disable termination protection using the AWS Console or AWS CLI.
# Enabled
aws cloudformation update-termination-protection --stack-name your-stackname --enable-termination-protection
# Disabled
aws cloudformation update-termination-protection --stack-name your-stackname --no-enable-termination-protection
Tagging Resources
Consistent tagging is essential for resource management, cost allocation, and security compliance. AWS CDK makes it easy to apply tags to all resources in your application or specific stacks:
// Tag all resources in the application
cdk.Tags.of(app).add('Project', 'MyProject');
cdk.Tags.of(app).add('Environment', envName);
// Tag all resources in a specific stack
cdk.Tags.of(myStack).add('Component', 'API');
// Apply additional tags
const tags: Record<string, string> = {
'Owner': 'DevTeam',
'CostCenter': 'CC1234',
'Compliance': 'SOC2'
};
Object.entries(tags).forEach(([key, value]) => {
cdk.Tags.of(app).add(key, value);
});
Linking AWS CLI Profiles to Environments
To reduce the risk of deploying to the wrong environment, you can create a convention that links your AWS CLI profiles to your environment names:
// package.json
{
"scripts": {
"deploy:all": "cdk deploy --all -c project=${npm_config_project} -c env=${npm_config_env} --profile ${npm_config_project}-${npm_config_env}",
"destroy:all": "cdk destroy --all -c project=${npm_config_project} -c env=${npm_config_env} --profile ${npm_config_project}-${npm_config_env}"
}
}
Usage:
npm run deploy:all --project=myapp --env=dev
This command uses the AWS CLI profile named myapp-dev for deployments to the development environment.
[profile myapp-dev]
region = us-west-2
[profile myapp-prod]
region = us-east-1
This approach ensures the AWS CLI profile matches the environment you're targeting, reducing the risk of deployment mistakes.
CDK Metadata: What It Is and When to Disable It
By default, CDK adds metadata to the generated CloudFormation templates for telemetry purposes. If you prefer not to include this metadata, you can disable it:
Version Reporting
see Configure AWS CDK Library usage data reporting
This information is used to improve the AWS CDK.
The AWS team collects and analyzes this information to understand how AWS CDK is being used.
CDKMetadata:
CDKMetadata:
Type: AWS::CDK::Metadata
Properties:
Analytics: v2:deflate64:H4sIAAAAAAAA/zPSszDXM1BMLC/WTU7J1s3JTNKrDi5JTM7WAQrFpyYb6Tmn5YUFOOsAqaDU4vzSouRUENs5Py8lsyQzP69WJy8/JVUvq1i/zMhAz1TPUDGrODNTt6g0ryQzN1UvCEIDANEI+cVnAAAA
Metadata:
aws:cdk:path: Dev/SandboxVPCBasic/CDKMetadata/Default
When to disable:
- You have privacy or security concerns about sharing usage data
- Your organization's policies prohibit telemetry
- You want to minimize template size
How to disable:
cdk synth --version-reporting false
#or
cdk synth --no-version-reporting
Path Metadata
Path metadata provides a hierarchical view of your CDK constructs in the CloudFormation console, making it easier to understand the relationship between resources and their construct definitions.
With path metadata enabled (default):
The CloudFormation console displays a tree structure showing how constructs are organized in your CDK code. Each resource includes metadata like:
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
Metadata:
aws:cdk:path: Dev/SandboxVPCBasic/Vpc/Resource
This path (Dev/SandboxVPCBasic/Vpc/Resource) shows exactly where this VPC is defined in your CDK construct hierarchy.
With path metadata disabled:
cdk synth --path-metadata false
#or
cdk synth --no-path-metadata
Without path metadata, the CloudFormation console shows a flat list of resources without the construct hierarchy.
Recommendation: Keep path metadata enabled (the default setting).
I know some teams disable this to reduce template size, and I respect that
decision, but in my view, the debugging benefits far outweigh the minimal size
savings. I've spent hours debugging CloudFormation stacks, and having the
construct hierarchy visible has saved me countless times.
Only consider disabling path metadata if you're hitting CloudFormation template size limits and have exhausted other optimization options.
Configuring Metadata in cdk.json
To permanently disable metadata in your project, add these settings to your cdk.json:
{
"app": "npx ts-node bin/my-app.ts",
"versionReporting": false, // Disable version reporting
"pathMetadata": false // Not recommended - see above
}
Best practice: Consider disabling only versionReporting while keeping pathMetadata enabled for better operational visibility.
Conclusion
Adopting these AWS CDK development best practices will help you create infrastructure that is maintainable, scalable, and secure. The structure and patterns you establish early in your CDK journey will pay dividends as your infrastructure grows in complexity.
- Organize your project with a clear directory structure that separates concerns
- Think carefully about stack boundaries and the implications of cross-stack references
- Follow consistent naming conventions for Construct IDs
- Implement safety measures for production deployments
- Use tags consistently across all resources
- Align AWS CLI profiles with your environment names
In the next article, we'll explore how to test your AWS CDK applications effectively using snapshot tests, unit tests, and other validation techniques.
What project structure do you use for your CDK applications? Have you found other techniques that help manage multi-environment deployments? Share your experiences in the comments!
This article is part of the "Mastering AWS CDK" series, where we explore all aspects of using the AWS Cloud Development Kit for infrastructure as code.



Top comments (0)