Chapter 2: Core Concepts of CDK
9/1/25About 5 min
Learning Objectives
- Master the three core concepts: App, Stack, and Construct
- Understand the hierarchical structure and organization of CDK
- Learn to use basic CDK CLI commands
- Understand the CDK lifecycle and deployment process
Core Concepts Explained
The Three Core Concepts of CDK
1. App
The App is the root container for a CDK application and represents the entire CDK application.
#!/usr/bin/env python3
import aws_cdk as cdk
from my_app.vpc_stack import VpcStack
from my_app.database_stack import DatabaseStack
from my_app.web_stack import WebStack
# Create a CDK app
app = cdk.App()
# Define multiple Stacks within the app
vpc_stack = VpcStack(app, "VpcStack")
db_stack = DatabaseStack(app, "DatabaseStack", vpc=vpc_stack.vpc)
web_stack = WebStack(app, "WebStack", vpc=vpc_stack.vpc, database=db_stack.database)
# Synthesize the app
app.synth()
2. Stack
A Stack is the basic unit of deployment and corresponds to a CloudFormation stack.
from aws_cdk import Stack, Environment
from constructs import Construct
import aws_cdk as cdk
class MyStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Stack properties
self.stack_name = "MyApplicationStack"
self.description = "This stack contains my web application resources"
# Define your resource constructs here
# self.create_networking()
# self.create_compute()
# self.create_storage()
@property
def availability_zones(self):
"""Get availability zones"""
return self.get_azs()
@property
def region(self):
"""Get the current region"""
return self.region
# Stack with environment configuration
production_env = Environment(
account="123456789012",
region="us-east-1"
)
stack = MyStack(app, "ProductionStack", env=production_env)
3. Construct
A Construct is the basic building block of CDK and represents a cloud component.
from constructs import Construct
from aws_cdk import (
aws_s3 as s3,
aws_iam as iam,
RemovalPolicy
)
class S3BucketWithPolicy(Construct):
"""Custom construct: S3 bucket with a policy"""
def __init__(self, scope: Construct, construct_id: str,
bucket_name: str = None, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Create an S3 bucket
self.bucket = s3.Bucket(
self,
"Bucket",
bucket_name=bucket_name,
versioned=True,
encryption=s3.BucketEncryption.S3_MANAGED,
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True
)
# Create an IAM policy
self.read_policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=["s3:GetObject", "s3:GetObjectVersion"],
resources=[f"{self.bucket.bucket_arn}/*"],
principals=[iam.ServicePrincipal("lambda.amazonaws.com")]
)
# Apply the policy to the bucket
self.bucket.add_to_resource_policy(self.read_policy)
@property
def bucket_name(self) -> str:
"""Return the bucket name"""
return self.bucket.bucket_name
@property
def bucket_arn(self) -> str:
"""Return the bucket ARN"""
return self.bucket.bucket_arn
# Use the custom construct in a Stack
class MyApplicationStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Use the custom construct
data_bucket = S3BucketWithPolicy(
self,
"DataBucket",
bucket_name="my-app-data-bucket"
)
logs_bucket = S3BucketWithPolicy(
self,
"LogsBucket",
bucket_name="my-app-logs-bucket"
)
CDK Hierarchy
Construct Levels
Level Details
Level | Name | Features | Use Case |
---|---|---|---|
L1 | CFN Resources | 1:1 mapping to CloudFormation | Need precise control over resource properties |
L2 | Intent-based API | Encapsulates best practices | Recommended for daily development |
L3 | Patterns | Combination of multiple resources | Quickly implement common architectures |
Example: Creating S3 Buckets at Different Levels
from aws_cdk import (
Stack,
aws_s3 as s3,
aws_s3_deployment as s3deploy,
RemovalPolicy
)
from constructs import Construct
class S3ExamplesStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# L1 Construct - Direct use of CloudFormation resources
l1_bucket = s3.CfnBucket(
self,
"L1Bucket",
bucket_name="my-l1-bucket",
versioning_configuration={
"status": "Enabled"
},
public_access_block_configuration={
"blockPublicAcls": True,
"blockPublicPolicy": True,
"ignorePublicAcls": True,
"restrictPublicBuckets": True
}
)
# L2 Construct - High-level API with best practices
l2_bucket = s3.Bucket(
self,
"L2Bucket",
bucket_name="my-l2-bucket",
versioned=True, # Simplified versioning setup
block_public_access=s3.BlockPublicAccess.BLOCK_ALL, # Simplified access control
encryption=s3.BucketEncryption.S3_MANAGED,
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True
)
# L3 Construct - Pattern/composite construct (requires additional packages)
# Here is an example of a custom L3 construct
website_bucket = StaticWebsite(
self,
"StaticWebsite",
domain_name="example.com"
)
class StaticWebsite(Construct):
"""L3 construct example: static website hosting"""
def __init__(self, scope: Construct, construct_id: str,
domain_name: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Create an S3 bucket for hosting
self.bucket = s3.Bucket(
self,
"WebsiteBucket",
website_index_document="index.html",
website_error_document="error.html",
public_read_access=True,
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True
)
# Deploy website content
s3deploy.BucketDeployment(
self,
"DeployWebsite",
sources=[s3deploy.Source.asset("./website-dist")],
destination_bucket=self.bucket
)
@property
def url(self) -> str:
return self.bucket.bucket_website_url
CDK Project Structure
Typical Project Structure
my-cdk-app/
├── app.py # Application entry point
├── cdk.json # CDK configuration
├── requirements.txt # Python dependencies
├── requirements-dev.txt # Development dependencies
├── README.md
├── .gitignore
├── tests/ # Test directory
│ ├── unit/
│ └── integration/
└── my_cdk_app/ # Application package
├── __init__.py
├── stacks/ # Stack definitions
│ ├── __init__.py
│ ├── vpc_stack.py
│ ├── database_stack.py
│ └── web_stack.py
├── constructs/ # Custom constructs
│ ├── __init__.py
│ └── custom_constructs.py
└── config/ # Configuration files
├── __init__.py
└── environments.py
Modular Application Organization
# app.py
#!/usr/bin/env python3
import aws_cdk as cdk
from my_cdk_app.config.environments import get_environment_config
from my_cdk_app.stacks.vpc_stack import VpcStack
from my_cdk_app.stacks.database_stack import DatabaseStack
from my_cdk_app.stacks.web_stack import WebStack
app = cdk.App()
# Get environment configuration from context
env_name = app.node.try_get_context("env") or "dev"
env_config = get_environment_config(env_name)
# Create VPC Stack
vpc_stack = VpcStack(
app,
f"VpcStack-{env_name}",
env_config=env_config,
env=env_config.cdk_environment
)
# Create Database Stack, dependent on VPC
database_stack = DatabaseStack(
app,
f"DatabaseStack-{env_name}",
vpc=vpc_stack.vpc,
env_config=env_config,
env=env_config.cdk_environment
)
# Create Web Stack, dependent on VPC and database
web_stack = WebStack(
app,
f"WebStack-{env_name}",
vpc=vpc_stack.vpc,
database=database_stack.database,
env_config=env_config,
env=env_config.cdk_environment
)
app.synth()
# my_cdk_app/config/environments.py
from dataclasses import dataclass
from aws_cdk import Environment
@dataclass
class EnvironmentConfig:
environment_name: str
account_id: str
region: str
vpc_cidr: str
database_instance_type: str
web_instance_count: int
@property
def cdk_environment(self) -> Environment:
return Environment(account=self.account_id, region=self.region)
def get_environment_config(env_name: str) -> EnvironmentConfig:
"""Get configuration based on environment name"""
configs = {
"dev": EnvironmentConfig(
environment_name="development",
account_id="111111111111",
region="us-west-2",
vpc_cidr="10.0.0.0/16",
database_instance_type="db.t3.micro",
web_instance_count=1
),
"staging": EnvironmentConfig(
environment_name="staging",
account_id="222222222222",
region="us-west-2",
vpc_cidr="10.1.0.0/16",
database_instance_type="db.t3.small",
web_instance_count=2
),
"prod": EnvironmentConfig(
environment_name="production",
account_id="333333333333",
region="us-east-1",
vpc_cidr="10.2.0.0/16",
database_instance_type="db.r5.large",
web_instance_count=3
)
}
if env_name not in configs:
raise ValueError(f"Unknown environment: {env_name}")
return configs[env_name]
CDK Lifecycle
Development to Deployment Flow
CDK Command Details
# Project initialization
# cdk init app --language python
# View all available stacks
# cdk list
# Synthesize the template for a specific stack
# cdk synth MyStack --output ./cdk.out
# View changes to be deployed
# cdk diff MyStack
# Deploy a single stack
# cdk deploy MyStack
# Deploy all stacks
# cdk deploy --all
# Deploy with parameters
# cdk deploy --context env=prod
# Destroy a stack
# cdk destroy MyStack
# View stack metadata
# cdk metadata MyStack
# List the construct tree
# cdk tree
# Initialize the bootstrap environment
# cdk bootstrap aws://ACCOUNT-NUMBER/REGION
Context and Configuration
# cdk.json - CDK project configuration file
{
"app": "python app.py",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"requirements*.txt",
"source.bat",
"**/__pycache__",
"**/*.pyc"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
// Custom context values
"vpc-cidr": "10.0.0.0/16",
"enable-dns-hostnames": true,
"database-backup-retention": 7
},
"feature_flags": {
"@aws-cdk/core:enableStackNameDuplicates": true,
"@aws-cdk/core:enableStringConsolidation": true
}
}
# Using context in code
from aws_cdk import Stack
from constructs import Construct
class MyStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Get context values
vpc_cidr = self.node.try_get_context("vpc-cidr")
enable_dns = self.node.try_get_context("enable-dns-hostnames")
backup_retention = self.node.try_get_context("database-backup-retention")
# Override context with environment variables (passed via command line)
environment = self.node.try_get_context("environment") or "dev"
print(f"Deploying to environment: {environment}")
print(f"Using VPC CIDR: {vpc_cidr}")
Best Practices
1. Stack Design Principles
# ✅ Good practice: Separate Stacks by function and lifecycle
class NetworkStack(Stack):
"""Network infrastructure - long lifecycle, few changes"""
pass
class DatabaseStack(Stack):
"""Database layer - long lifecycle, but configuration may change"""
pass
class ApplicationStack(Stack):
"""Application layer - frequent deployments and updates"""
pass
# ❌ Avoid: Putting all resources in one giant Stack
class MonolithStack(Stack):
"""Not recommended: A giant Stack containing all resources"""
pass
2. Resource Naming Conventions
class NamingConventionStack(Stack):
def __init__(self, scope: Construct, construct_id: str,
env_name: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Use a consistent naming convention
self.app_name = "myapp"
# S3 bucket naming
bucket = s3.Bucket(
self,
"DataBucket",
bucket_name=f"{self.app_name}-{env_name}-data-{self.account}-{self.region}"
)
# Lambda function naming
function = lambda_.Function(
self,
"ProcessorFunction",
function_name=f"{self.app_name}-{env_name}-processor",
# ... other configurations
)
3. Dependency Management
# Explicitly define dependencies through the constructor
class WebStack(Stack):
def __init__(self, scope: Construct, construct_id: str,
vpc: ec2.Vpc, # Explicit dependency
database: rds.Database, # Explicit dependency
**kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Use the passed-in dependencies
self.vpc = vpc
self.database = database
# Create resources that depend on the VPC and database
self.create_application_resources()
def create_application_resources(self):
# Create application resources here
pass
This chapter has provided an in-depth look at the core architectural concepts of CDK, laying a solid foundation for subsequent practical development. Understanding these concepts is crucial for building maintainable and scalable CDK applications.