Jonatan Matajonmatum.com
conceptsnotesexperimentsessays
© 2026 Jonatan Mata. All rights reserved.v2.1.1
Concepts

AWS CDK

AWS infrastructure as code framework that allows defining cloud resources using programming languages like TypeScript, Python, or Java, generating CloudFormation.

evergreen#aws#cdk#iac#typescript#cloudformation#devops

What it is

AWS CDK (Cloud Development Kit) is an infrastructure as code framework that allows defining AWS resources using familiar programming languages like TypeScript, Python, Java, C#, and Go. Unlike CloudFormation which uses YAML/JSON, CDK leverages programming language capabilities to create reusable abstractions, apply conditional logic, and perform compile-time validations.

The framework works by synthesizing code into CloudFormation templates that are then deployed using AWS's native engine. This architecture maintains CloudFormation's robustness and features (automatic rollbacks, drift detection, stack dependencies) while gaining the benefits of imperative programming.

CDK introduces the concept of "constructs" — reusable components that encapsulate one or more AWS resources with sensible default configurations. These constructs range from direct CloudFormation resource mappings to complete architectural patterns that configure multiple services with integrated best practices.

Construct hierarchy

CDK organizes constructs into three abstraction levels that determine the degree of control versus convenience:

LevelNameDescriptionExampleUse case
L1CFN Resources1:1 mapping with CloudFormationCfnBucket, CfnFunctionGranular control, new resources
L2AWS ConstructsAbstractions with defaultss3.Bucket, lambda.FunctionProductive development
L3PatternsComplete architecturesLambdaRestApi, ApplicationLoadBalancedFargateServiceCommon patterns

L2 constructs are the sweet spot for most cases — they provide sensible defaults (encryption enabled, minimal IAM policies) while maintaining configuration flexibility. L3 constructs implement complete architectural patterns but may be too rigid for specific cases.

Complete stack example

import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';
 
export class UserServiceStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
 
    // DynamoDB table with production configuration
    const userTable = new dynamodb.Table(this, 'UserTable', {
      tableName: 'users',
      partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      encryption: dynamodb.TableEncryption.AWS_MANAGED,
      pointInTimeRecovery: true,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });
 
    // Lambda function with optimized configuration
    const userHandler = new lambda.Function(this, 'UserHandler', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      environment: {
        TABLE_NAME: userTable.tableName,
      },
      timeout: cdk.Duration.seconds(30),
      memorySize: 512,
      tracing: lambda.Tracing.ACTIVE,
    });
 
    // Granular permissions for DynamoDB
    userTable.grantReadWriteData(userHandler);
 
    // API Gateway with production configuration
    const api = new apigateway.RestApi(this, 'UserApi', {
      restApiName: 'User Service API',
      description: 'API for user management',
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
      },
      deployOptions: {
        stageName: 'prod',
        throttlingRateLimit: 1000,
        throttlingBurstLimit: 2000,
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
      },
    });
 
    const users = api.root.addResource('users');
    users.addMethod('GET', new apigateway.LambdaIntegration(userHandler));
    users.addMethod('POST', new apigateway.LambdaIntegration(userHandler));
 
    const userById = users.addResource('{userId}');
    userById.addMethod('GET', new apigateway.LambdaIntegration(userHandler));
    userById.addMethod('PUT', new apigateway.LambdaIntegration(userHandler));
    userById.addMethod('DELETE', new apigateway.LambdaIntegration(userHandler));
 
    // Outputs for integration with other stacks
    new cdk.CfnOutput(this, 'ApiUrl', {
      value: api.url,
      description: 'URL of the User API',
    });
 
    new cdk.CfnOutput(this, 'TableName', {
      value: userTable.tableName,
      description: 'Name of the DynamoDB table',
    });
  }
}

CDK vs Terraform: detailed comparison

AspectAWS CDKTerraform
LanguagesTypeScript, Python, Java, C#, GoHCL (HashiCorp Configuration Language)
ProvidersAWS only (native)Multi-cloud (AWS, Azure, GCP, etc.)
StateManaged by CloudFormationLocal/remote state file
AbstractionsNative L1/L2/L3 constructsCommunity modules
TestingNative language unit testsTerratest, kitchen-terraform
IDE SupportFull IntelliSenseHCL extensions
Learning curveFamiliar to developersDomain-specific language
RollbacksAutomatic via CloudFormationManual or via CI/CD
Drift detectionNative in CloudFormationTerraform plan
EcosystemConstructs Hub, CDK PatternsTerraform Registry

Migration path Terraform → CDK

For teams considering migrating from Terraform to CDK:

  1. Assessment: Identify stacks that would benefit from programmatic abstractions
  2. Pilot: Migrate a small, non-critical stack first
  3. Coexistence: Use CDK for new resources, maintain Terraform for existing ones
  4. Gradual migration: Use cdk import to import existing resources
  5. Consolidation: Refactor using L2/L3 constructs to reduce boilerplate

Testing with CDK

CDK enables real unit testing using language frameworks:

import { Template, Match } from 'aws-cdk-lib/assertions';
import { UserServiceStack } from '../lib/user-service-stack';
import { App } from 'aws-cdk-lib';
 
test('DynamoDB table created with correct configuration', () => {
  const app = new App();
  const stack = new UserServiceStack(app, 'TestStack');
  const template = Template.fromStack(stack);
 
  template.hasResourceProperties('AWS::DynamoDB::Table', {
    BillingMode: 'PAY_PER_REQUEST',
    SSESpecification: {
      SSEEnabled: true,
    },
    PointInTimeRecoverySpecification: {
      PointInTimeRecoveryEnabled: true,
    },
  });
});
 
test('Lambda has correct environment variables', () => {
  const app = new App();
  const stack = new UserServiceStack(app, 'TestStack');
  const template = Template.fromStack(stack);
 
  template.hasResourceProperties('AWS::Lambda::Function', {
    Environment: {
      Variables: {
        TABLE_NAME: { Ref: Match.stringLikeRegexp('UserTable') },
      },
    },
  });
});

Advanced patterns

Cross-stack references

// Database stack
export class DatabaseStack extends cdk.Stack {
  public readonly userTable: dynamodb.Table;
 
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    this.userTable = new dynamodb.Table(this, 'UserTable', {
      // configuration...
    });
  }
}
 
// API stack that consumes the database
export class ApiStack extends cdk.Stack {
  constructor(scope: Construct, id: string, 
              databaseStack: DatabaseStack, 
              props?: cdk.StackProps) {
    super(scope, id, props);
 
    const handler = new lambda.Function(this, 'Handler', {
      environment: {
        TABLE_NAME: databaseStack.userTable.tableName,
      },
      // configuration...
    });
 
    databaseStack.userTable.grantReadWriteData(handler);
  }
}

Reusable custom constructs

export interface ApiLambdaProps {
  tableName: string;
  timeout?: cdk.Duration;
  memorySize?: number;
}
 
export class ApiLambda extends Construct {
  public readonly function: lambda.Function;
  public readonly api: apigateway.RestApi;
 
  constructor(scope: Construct, id: string, props: ApiLambdaProps) {
    super(scope, id);
 
    this.function = new lambda.Function(this, 'Function', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      environment: { TABLE_NAME: props.tableName },
      timeout: props.timeout ?? cdk.Duration.seconds(30),
      memorySize: props.memorySize ?? 512,
    });
 
    this.api = new apigateway.LambdaRestApi(this, 'Api', {
      handler: this.function,
      proxy: false,
    });
  }
}

Why it matters

CDK represents a paradigmatic shift in infrastructure as code by eliminating the artificial barrier between application code and infrastructure. For senior engineering teams, this means being able to apply the same development practices — unit testing, refactoring, abstractions, composition — to infrastructure definition.

The ability to create reusable constructs enables establishing organizational "golden paths" that encapsulate security, observability, and cost best practices. A custom L3 construct can guarantee that all APIs include structured logging, CloudWatch metrics, and minimal IAM policies without requiring expert knowledge from every developer.

The native testing model eliminates the need for external tools like Terratest, enabling real TDD for infrastructure. This is especially valuable in organizations that prioritize code quality and test coverage. IDE integration provides autocompletion, real-time error detection, and automatic refactoring — capabilities impossible with DSLs like HCL or YAML.

References

  • AWS Cloud Development Kit (AWS CDK) v2 — AWS, 2024. Complete official documentation.
  • CDK Best Practices — AWS, 2024. Official best practices guide.
  • Construct Hub — AWS, 2024. Repository of reusable constructs.
  • CDK Patterns — Matt Coulter, 2024. Architectural patterns with CDK.
  • Test AWS CDK Applications — AWS, 2024. Official testing guide.
  • CDK Workshop — AWS, 2024. Official interactive tutorial.

Related content

  • Infrastructure as Code

    Practice of defining and managing infrastructure through versioned configuration files instead of manual processes. Foundation of modern operations automation.

  • TypeScript

    Typed superset of JavaScript adding optional static types, improving developer productivity, error detection, and code maintainability.

  • AWS CloudFormation

    AWS native service for defining and provisioning infrastructure as code using YAML or JSON templates, with state management and automatic rollback.

Concepts