CDK gives you loops, types, tests and reusable components for infrastructure while keeping CloudFormation's safety.
The AWS CDK (v2, package aws-cdk-lib) lets you define infrastructure in TypeScript, Python, Java, C# or Go. Your program builds a tree: an App contains Stacks, which contain constructs. cdk synth turns it into CloudFormation templates, and cdk deploy deploys them as stacks.
Construct levels:
- L1 (
CfnBucket,CfnQueue): a one-to-one mapping of CloudFormation resources. - L2 (
s3.Bucket,lambda_.Function): curated APIs with secure defaults and helper methods. - L3 patterns (such as
ApplicationLoadBalancedFargateService): whole architectures in one construct.
The killer feature is grants: table.grant_read_write_data(fn) adds a least-privilege IAM policy to the function's role, scoped to that table. Similar methods connect security groups, event sources and KMS keys.
Workflow: run cdk bootstrap once per account and Region (it creates the asset bucket, ECR repository and deployment roles), then cdk diff to preview and cdk deploy. Assets such as Lambda code and Docker images are packaged and uploaded automatically.
Things that bite:
- A construct's ID path determines the CloudFormation logical ID. Renaming or moving a construct means delete-and-recreate, which for a table means data loss unless retained.
- Stateful L2 resources often default to
RemovalPolicy.RETAIN; be explicit. - Keep stacks deterministic: no API lookups at synth time without caching them in
cdk.context.json.
Test with aws_cdk.assertions.Template.from_stack(...) and add Aspects or cdk-nag for policy checks.
Example
from aws_cdk import App, Duration, Environment, RemovalPolicy, Stack
from aws_cdk import aws_dynamodb as ddb, aws_lambda as lambda_, aws_sqs as sqs
from aws_cdk.aws_lambda_event_sources import SqsEventSource
from constructs import Construct
class OrdersStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
table = ddb.TableV2(self, "Orders", removal_policy=RemovalPolicy.RETAIN,
partition_key=ddb.Attribute(name="PK", type=ddb.AttributeType.STRING),
sort_key=ddb.Attribute(name="SK", type=ddb.AttributeType.STRING))
queue = sqs.Queue(self, "Incoming", visibility_timeout=Duration.seconds(180))
fn = lambda_.Function(self, "Worker", runtime=lambda_.Runtime.PYTHON_3_13,
architecture=lambda_.Architecture.ARM_64, handler="app.handler",
code=lambda_.Code.from_asset("src"), timeout=Duration.seconds(30),
environment={"TABLE": table.table_name})
fn.add_event_source(SqsEventSource(queue, batch_size=10, report_batch_item_failures=True))
table.grant_read_write_data(fn) # least-privilege IAM, generated
app = App()
OrdersStack(app, "orders-dev", env=Environment(account="111122223333", region="eu-west-1"))
app.synth()
# cdk bootstrap aws://111122223333/eu-west-1 && cdk diff && cdk deploy orders-devRemember
- CDK code synthesizes to CloudFormation; stacks deploy as usual
- Prefer L2 constructs; use grant methods for least-privilege IAM
- Bootstrap each account/Region once; cdk diff before deploy
- Construct IDs become logical IDs: renaming replaces resources
Watch out for
- Refactoring construct names or nesting, which silently changes logical IDs and replaces stateful resources.