DEV Community

Cover image for Mastering AWS CDK #3 - AWS CDK Development: Best Practices and Workflow

Mastering AWS CDK #3 - AWS CDK Development: Best Practices and Workflow

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

CDK Development Workflow

The typical AWS CDK development workflow follows these steps:

  1. Initialize a project with cdk init
  2. Edit the App definition to define your stacks
  3. Create stack definitions with the resources you need
  4. Write tests for your infrastructure code
  5. For the first deployment:
    • Run cdk bootstrap to prepare your AWS environment
    • Execute cdk deploy to deploy your resources
  6. For subsequent deployments:
    • (Optional) Run cdk diff to check for changes
    • Execute cdk deploy to update your resources

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

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:

  1. Resource lifecycles differ significantly
    • Example: You want to preserve storage resources even if you delete application resources
  2. You approach CloudFormation limits
  3. You need different deployment frequencies
    • Example: Network infrastructure rarely changes, while application resources change frequently
  4. 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:

  1. Use exports and imports sparingly
  2. Consider using parameters for values that might be shared
  3. Design for independence where possible
  4. 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", {});
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

In the generated CloudFormation template:

"Resources": {
  "LogsBucket9C4D8843": {
   "Type": "AWS::S3::Bucket",
   "Properties": {
     // ...
   }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

Usage:

npm run deploy:all --project=myapp --env=dev
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

path-metadata-true

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

This path (Dev/SandboxVPCBasic/Vpc/Resource) shows exactly where this VPC is defined in your CDK construct hierarchy.

With path metadata disabled:

path-metadata-false

cdk synth --path-metadata false
#or
cdk synth --no-path-metadata
Enter fullscreen mode Exit fullscreen mode

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

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.

  1. Organize your project with a clear directory structure that separates concerns
  2. Think carefully about stack boundaries and the implications of cross-stack references
  3. Follow consistent naming conventions for Construct IDs
  4. Implement safety measures for production deployments
  5. Use tags consistently across all resources
  6. 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)