AWS CDK: infrastructure in code

Amazon Web Services · Advanced · 18 min

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-dev

Remember

  • 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.

Check yourself

  1. What does table.grant_read_write_data(fn) do?
  2. You move a DynamoDB table construct into a new parent construct. What can happen on deploy?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.aws.amazon.com/cdk/v2/guide/home.html