
Aws Cdk
- 2 installs
- 318 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit-claude-code
This is a copy of aws-cdk by giuseppe-trisciuoglio - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
aws-cdk is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- aws-cdk
- AI & Agent Building
- AI-coding skill
Aws Cdk by the numbers
- 2 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit-claude-code --skill aws-cdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 318 |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit-claude-code ↗ |
What it does
Helps with ai & agent building tasks.
Files
AWS CDK TypeScript
Overview
Use this skill to build AWS infrastructure in TypeScript with reusable constructs, safe defaults, and a validation-first delivery loop.
When to Use
Use this skill when:
- Creating or refactoring a CDK app, stack, or reusable construct in TypeScript
- Choosing between L1, L2, and L3 constructs
- Building serverless, networking, or security-focused AWS infrastructure
- Wiring multi-stack applications and environment-aware deployments
- Validating infrastructure changes with
cdk synth, tests,cdk diff, andcdk deploy
Instructions
1. Project Initialization
# Create a new CDK app
npx cdk init app --language typescript
# Project structure
my-cdk-app/
├── bin/
│ └── my-cdk-app.ts # App entry point (instantiates stacks)
├── lib/
│ └── my-cdk-app-stack.ts # Stack definition
├── test/
│ └── my-cdk-app.test.ts # Tests
├── cdk.json # CDK configuration
├── tsconfig.json
└── package.json2. Core Architecture
import { App, Stack, StackProps, CfnOutput, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
// Define a reusable stack
class StorageStack extends Stack {
public readonly bucketArn: string;
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, 'DataBucket', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
removalPolicy: RemovalPolicy.RETAIN,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
this.bucketArn = bucket.bucketArn;
new CfnOutput(this, 'BucketName', { value: bucket.bucketName });
}
}
// App entry point
const app = new App();
new StorageStack(app, 'DevStorage', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
tags: { Environment: 'dev' },
});
new StorageStack(app, 'ProdStorage', {
env: { account: '123456789012', region: 'eu-west-1' },
tags: { Environment: 'prod' },
terminationProtection: true,
});
app.synth();3. Construct Levels
| Level | Description | Use When |
|---|---|---|
L1 (Cfn*) | Direct CloudFormation mapping, full control | Need properties not exposed by L2 |
| L2 | Curated with sensible defaults and helper methods | Standard resource provisioning (recommended) |
| L3 (Patterns) | Multi-resource architectures | Common patterns like LambdaRestApi |
// L1 — Raw CloudFormation
new s3.CfnBucket(this, 'L1Bucket', { bucketName: 'my-l1-bucket' });
// L2 — Sensible defaults + grant helpers
const bucket = new s3.Bucket(this, 'L2Bucket', { versioned: true });
bucket.grantRead(myLambda);
// L3 — Multi-resource pattern
new apigateway.LambdaRestApi(this, 'Api', { handler: myLambda });4. CDK Lifecycle Commands
cdk synth # Synthesize CloudFormation template
cdk diff # Compare deployed vs local changes
cdk deploy # Deploy stack(s) to AWS
cdk deploy --all # Deploy all stacks
cdk destroy # Tear down stack(s)
cdk ls # List all stacks in the app
cdk doctor # Check environment setup5. Recommended Delivery Loop
1. Model the stack
- Start with L2 constructs and extract repeated logic into custom constructs.
2. Run `cdk synth`
- Checkpoint: synthesis succeeds with no missing imports, invalid props, missing context, or unresolved references.
- If it fails: fix the construct configuration or context values, then rerun
cdk synth.
3. Run infrastructure tests
- Checkpoint: assertions cover IAM scope, stateful resources, and critical outputs.
- If tests fail: update the stack or test expectations, then rerun the test suite.
4. Run `cdk diff`
- Checkpoint: review IAM broadening, resource replacement, export changes, and deletes on stateful resources.
- If the diff is risky: adjust names, dependencies, or
RemovalPolicy, then reruncdk diff.
5. Run `cdk deploy`
- Checkpoint: the stack reaches
CREATE_COMPLETEorUPDATE_COMPLETE. - If deploy fails: inspect CloudFormation events, fix quotas, permissions, export conflicts, or bootstrap issues, then retry
cdk deploy.
6. Verify runtime outcomes
- Confirm stack outputs, endpoints, alarms, and integrations behave as expected before moving on.
6. Cross-Stack References
// Stack A exports a value
class NetworkStack extends Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2 });
}
}
// Stack B imports it via props
interface AppStackProps extends StackProps {
vpc: ec2.Vpc;
}
class AppStack extends Stack {
constructor(scope: Construct, id: string, props: AppStackProps) {
super(scope, id, props);
new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
vpc: props.vpc,
});
}
}
// Wire them together
const network = new NetworkStack(app, 'Network');
new AppStack(app, 'App', { vpc: network.vpc });Examples
Example 1: Serverless API
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';
class ServerlessApiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const table = new dynamodb.Table(this, 'Items', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const fn = new lambda.Function(this, 'Handler', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: { TABLE_NAME: table.tableName },
});
table.grantReadWriteData(fn);
new apigateway.LambdaRestApi(this, 'Api', { handler: fn });
}
}Example 2: CDK Assertion Test
import { Template } from 'aws-cdk-lib/assertions';
import { App } from 'aws-cdk-lib';
import { ServerlessApiStack } from '../lib/serverless-api-stack';
test('creates DynamoDB table with PAY_PER_REQUEST', () => {
const app = new App();
const stack = new ServerlessApiStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::DynamoDB::Table', {
BillingMode: 'PAY_PER_REQUEST',
});
template.resourceCountIs('AWS::Lambda::Function', 1);
});Best Practices
1. One concern per stack — Separate network, compute, storage, and monitoring. 2. Prefer L2 constructs — Drop to Cfn* only when you need unsupported properties. 3. Set explicit environments — Pass env with account and region; avoid implicit production targets. 4. Use grant helpers — Prefer .grant*() over handwritten IAM where possible. 5. Review the diff before deploy — Treat IAM expansion, replacement, and deletes as mandatory checkpoints. 6. Test infrastructure — Cover critical resources with fine-grained assertions. 7. Avoid hardcoded values — Use context, parameters, or environment variables. 8. Use the right `RemovalPolicy` — RETAIN for production data, DESTROY only for disposable environments.
Constraints and Warnings
- CloudFormation limits — Max 500 resources per stack; split large apps into multiple stacks
- Synthesis is not deployment —
cdk synthonly generates templates;cdk deployapplies changes - Cross-stack references create CloudFormation exports; removing them requires careful ordering
- Stateful resources (RDS, DynamoDB, S3 with data) — Always set
removalPolicy: RETAINin production - Bootstrap required — Run
cdk bootstraponce per account/region before first deploy - Asset bundling — Lambda code and Docker images are uploaded to the CDK bootstrap bucket
References
Detailed implementation guides are available in the references/ directory:
- Core Concepts — App lifecycle, stacks, constructs, environments, assets
- Serverless Patterns — Lambda, API Gateway, DynamoDB, S3 events, Step Functions
- Networking & VPC — VPC design, subnets, NAT, security groups, VPC endpoints
- Security Hardening — IAM, KMS, Secrets Manager, WAF, compliance
- Testing Strategies — Assertions, snapshots, integration tests, CDK Nag
AWS CDK Core Concepts
Table of Contents
---
App Lifecycle
The CDK app lifecycle follows a predictable flow from code to deployed infrastructure:
Code → Construct Tree → Synthesis → CloudFormation Template → DeploymentPhases
| Phase | Description | CLI Command |
|---|---|---|
| Construction | Instantiate App, Stacks, Constructs | — |
| Preparation | Mutate construct tree (Aspects run here) | — |
| Validation | Validate construct configurations | — |
| Synthesis | Generate CloudFormation templates + assets | cdk synth |
| Deployment | Create/update CloudFormation stacks | cdk deploy |
import { App } from 'aws-cdk-lib';
const app = new App();
// Construction phase: define stacks and constructs
new MyStack(app, 'MyStack', {
env: { account: '123456789012', region: 'us-east-1' },
});
// Synthesis phase: generates cdk.out/ directory
app.synth();Output Directory
After cdk synth, the cdk.out/ directory contains:
cdk.out/
├── MyStack.template.json # CloudFormation template
├── manifest.json # Assembly manifest
├── tree.json # Construct tree
└── asset.* # Bundled assets (Lambda code, Docker images)---
Stacks
A Stack is the unit of deployment — it maps 1:1 to a CloudFormation stack.
Single Stack
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class MyAppStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Define resources here
}
}Multi-Stack Architecture
const app = new App();
const networkStack = new NetworkStack(app, 'Network', {
env: { account: '123456789012', region: 'us-east-1' },
});
const appStack = new AppStack(app, 'App', {
vpc: networkStack.vpc,
env: { account: '123456789012', region: 'us-east-1' },
});
// Explicit dependency ensures correct deploy order
appStack.addDependency(networkStack);Stack Best Practices
- Keep stacks under 500 resources (CloudFormation limit)
- Group resources by lifecycle (network rarely changes, app changes often)
- Use
terminationProtection: truefor production stacks - Export shared resources as public properties for cross-stack references
---
Constructs
Constructs are the building blocks of CDK apps. They form a tree hierarchy: App → Stack → Constructs.
L1 Constructs (CloudFormation Resources)
Direct 1:1 mapping to CloudFormation resource types. Prefixed with Cfn.
import * as s3 from 'aws-cdk-lib/aws-s3';
// Full control, no defaults
new s3.CfnBucket(this, 'RawBucket', {
bucketName: 'my-raw-bucket',
versioningConfiguration: { status: 'Enabled' },
bucketEncryption: {
serverSideEncryptionConfiguration: [{
serverSideEncryptionByDefault: { sseAlgorithm: 'AES256' },
}],
},
});L2 Constructs (Curated)
Higher-level abstractions with sensible defaults, helper methods, and grant patterns.
import * as s3 from 'aws-cdk-lib/aws-s3';
// Sensible defaults + helper methods
const bucket = new s3.Bucket(this, 'DataBucket', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
// Grant pattern — generates least-privilege IAM automatically
bucket.grantRead(myLambdaFunction);L3 Constructs (Patterns)
Combine multiple L2 constructs into reusable architectural patterns.
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
// Creates API Gateway + Lambda integration + IAM permissions
new apigateway.LambdaRestApi(this, 'MyApi', {
handler: myLambdaFunction,
proxy: false,
});Custom Constructs
Create reusable constructs for your organization:
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export interface ProcessingPipelineProps {
readonly bucketName?: string;
readonly lambdaMemory?: number;
}
export class ProcessingPipeline extends Construct {
public readonly bucket: s3.Bucket;
public readonly processor: lambda.Function;
constructor(scope: Construct, id: string, props: ProcessingPipelineProps = {}) {
super(scope, id);
this.bucket = new s3.Bucket(this, 'InputBucket', {
bucketName: props.bucketName,
versioned: true,
});
this.processor = new lambda.Function(this, 'Processor', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/processor'),
memorySize: props.lambdaMemory ?? 256,
environment: { BUCKET_NAME: this.bucket.bucketName },
});
this.bucket.grantRead(this.processor);
}
}---
Environments
Environments specify the target AWS account and region for a stack.
Explicit Environment
new MyStack(app, 'ProdStack', {
env: { account: '123456789012', region: 'eu-west-1' },
});Default Environment (from CLI profile)
new MyStack(app, 'DevStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});Environment-Agnostic Stacks
Omitting env creates environment-agnostic stacks. Some features (like Vpc.fromLookup) require explicit environments.
Multi-Environment Pattern
interface EnvironmentConfig {
account: string;
region: string;
isProd: boolean;
}
const environments: Record<string, EnvironmentConfig> = {
dev: { account: '111111111111', region: 'us-east-1', isProd: false },
prod: { account: '222222222222', region: 'eu-west-1', isProd: true },
};
for (const [name, config] of Object.entries(environments)) {
new MyStack(app, `${name}-Stack`, {
env: { account: config.account, region: config.region },
terminationProtection: config.isProd,
});
}---
Context
Context values are key-value pairs available at synthesis time. They configure behavior without changing code.
Sources (in priority order)
1. --context CLI flag 2. cdk.json file 3. ~/.cdk.json (global) 4. construct.node.setContext() in code
Usage
// Read context value in stack
const stage = this.node.tryGetContext('stage') || 'dev';
const vpcId = this.node.tryGetContext('vpcId');
// Use in resource configuration
const isProd = stage === 'prod';// cdk.json
{
"context": {
"stage": "dev",
"vpcId": "vpc-0123456789abcdef0"
}
}# Override via CLI
cdk deploy --context stage=prod --context vpcId=vpc-abc123---
Assets
Assets are local files or Docker images that CDK uploads to S3/ECR during deployment.
File Assets (Lambda Code)
// Directory asset — bundled and uploaded to S3
new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/my-function'),
});Docker Assets (Container Images)
import * as ecs from 'aws-cdk-lib/aws-ecs';
// Build Docker image and push to ECR
const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef');
taskDef.addContainer('App', {
image: ecs.ContainerImage.fromAsset('./docker'),
memoryLimitMiB: 512,
});Bundled Assets (esbuild)
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
// Automatically bundles TypeScript with esbuild
new lambdaNode.NodejsFunction(this, 'BundledFn', {
entry: 'lambda/handler.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_20_X,
bundling: {
minify: true,
sourceMap: true,
externalModules: ['@aws-sdk/*'],
},
});---
Tokens and Lazy Values
Tokens are placeholders for values not known until deploy time (e.g., ARNs, names).
const bucket = new s3.Bucket(this, 'Bucket');
// bucket.bucketName is a Token (resolved at deploy time)
new lambda.Function(this, 'Fn', {
environment: {
BUCKET_NAME: bucket.bucketName, // Token — resolves to actual name
},
});
// Check if a value is a token
import { Token } from 'aws-cdk-lib';
Token.isUnresolved(bucket.bucketName); // true---
Aspects
Aspects apply operations to every construct in a scope (e.g., enforce tagging, compliance).
import { IAspect, Tags, Aspects, Annotations } from 'aws-cdk-lib';
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
import { IConstruct } from 'constructs';
class BucketVersioningChecker implements IAspect {
visit(node: IConstruct): void {
if (node instanceof CfnBucket) {
if (!node.versioningConfiguration) {
Annotations.of(node).addWarning('Bucket versioning is not enabled');
}
}
}
}
// Apply aspect to entire stack
Aspects.of(myStack).add(new BucketVersioningChecker());
// Apply tags to all resources in a scope
Tags.of(app).add('Project', 'MyProject');
Tags.of(app).add('ManagedBy', 'CDK');AWS CDK Networking & VPC
Table of Contents
- VPC Design
- Subnets
- NAT Gateways
- Security Groups
- Network ACLs
- VPC Endpoints
- VPC Peering
- Importing Existing VPCs
- Complete VPC Pattern
---
VPC Design
Basic VPC
import * as ec2 from 'aws-cdk-lib/aws-ec2';
const vpc = new ec2.Vpc(this, 'AppVpc', {
maxAzs: 2,
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});Production VPC with Custom Subnets
const vpc = new ec2.Vpc(this, 'ProdVpc', {
maxAzs: 3,
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
subnetConfiguration: [
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
{
cidrMask: 28,
name: 'Isolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});---
Subnets
Subnet Types
| Type | Internet Access | NAT Gateway | Use Case |
|---|---|---|---|
PUBLIC | Direct (IGW) | No | Load balancers, bastion hosts |
PRIVATE_WITH_EGRESS | Outbound only (NAT) | Yes | Application servers, Lambda |
PRIVATE_ISOLATED | None | No | Databases, internal services |
Selecting Subnets
// Select specific subnet types for resource placement
const publicSubnets = vpc.selectSubnets({
subnetType: ec2.SubnetType.PUBLIC,
});
const privateSubnets = vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
});
// Place Lambda in private subnets
new lambda.Function(this, 'PrivateFn', {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
});---
NAT Gateways
Cost Optimization
// One NAT Gateway per AZ (high availability, higher cost)
const prodVpc = new ec2.Vpc(this, 'ProdVpc', {
natGateways: 3, // One per AZ
maxAzs: 3,
});
// Single NAT Gateway (lower cost, single AZ risk)
const devVpc = new ec2.Vpc(this, 'DevVpc', {
natGateways: 1,
maxAzs: 2,
});
// No NAT Gateways (isolated or public-only architectures)
const isolatedVpc = new ec2.Vpc(this, 'IsolatedVpc', {
natGateways: 0,
subnetConfiguration: [
{ cidrMask: 24, name: 'Public', subnetType: ec2.SubnetType.PUBLIC },
{ cidrMask: 24, name: 'Isolated', subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
],
});---
Security Groups
Basic Security Group
const webSg = new ec2.SecurityGroup(this, 'WebSG', {
vpc,
description: 'Security group for web servers',
allowAllOutbound: true,
});
webSg.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS traffic',
);
webSg.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP traffic',
);Security Group Chaining (Multi-Tier)
const albSg = new ec2.SecurityGroup(this, 'AlbSG', {
vpc, description: 'ALB security group',
});
const appSg = new ec2.SecurityGroup(this, 'AppSG', {
vpc, description: 'Application security group',
});
const dbSg = new ec2.SecurityGroup(this, 'DbSG', {
vpc, description: 'Database security group',
});
// ALB accepts HTTPS from internet
albSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
// App accepts traffic only from ALB
appSg.addIngressRule(albSg, ec2.Port.tcp(8080), 'From ALB');
// DB accepts traffic only from App
dbSg.addIngressRule(appSg, ec2.Port.tcp(5432), 'From App');Lambda Security Groups
const lambdaSg = new ec2.SecurityGroup(this, 'LambdaSG', {
vpc,
description: 'Lambda function security group',
allowAllOutbound: true,
});
// Allow Lambda to reach RDS
dbSg.addIngressRule(lambdaSg, ec2.Port.tcp(5432), 'Lambda to RDS');---
Network ACLs
const nacl = new ec2.NetworkAcl(this, 'CustomNacl', {
vpc,
subnetSelection: { subnetType: ec2.SubnetType.PUBLIC },
});
nacl.addEntry('AllowHTTPS', {
cidr: ec2.AclCidr.anyIpv4(),
ruleNumber: 100,
traffic: ec2.AclTraffic.tcpPort(443),
direction: ec2.TrafficDirection.INGRESS,
ruleAction: ec2.Action.ALLOW,
});
nacl.addEntry('AllowEphemeral', {
cidr: ec2.AclCidr.anyIpv4(),
ruleNumber: 200,
traffic: ec2.AclTraffic.tcpPortRange(1024, 65535),
direction: ec2.TrafficDirection.EGRESS,
ruleAction: ec2.Action.ALLOW,
});---
VPC Endpoints
Eliminate NAT costs for AWS service traffic by using VPC endpoints.
Gateway Endpoints (S3, DynamoDB)
// Free — no hourly charge
vpc.addGatewayEndpoint('S3Endpoint', {
service: ec2.GatewayVpcEndpointAwsService.S3,
});
vpc.addGatewayEndpoint('DynamoEndpoint', {
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
});Interface Endpoints (Other AWS Services)
// Per-hour charge — add only services you use
vpc.addInterfaceEndpoint('SecretsManagerEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
vpc.addInterfaceEndpoint('SqsEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.SQS,
});
vpc.addInterfaceEndpoint('LambdaEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.LAMBDA,
});---
VPC Peering
const vpcA = new ec2.Vpc(this, 'VpcA', {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});
const vpcB = new ec2.Vpc(this, 'VpcB', {
ipAddresses: ec2.IpAddresses.cidr('10.1.0.0/16'),
});
const peering = new ec2.CfnVPCPeeringConnection(this, 'Peering', {
vpcId: vpcA.vpcId,
peerVpcId: vpcB.vpcId,
});
// Add routes in both directions
vpcA.privateSubnets.forEach((subnet, i) => {
new ec2.CfnRoute(this, `AtoB${i}`, {
routeTableId: subnet.routeTable.routeTableId,
destinationCidrBlock: '10.1.0.0/16',
vpcPeeringConnectionId: peering.ref,
});
});---
Importing Existing VPCs
// Look up by VPC ID (requires explicit env)
const existingVpc = ec2.Vpc.fromLookup(this, 'ImportedVpc', {
vpcId: 'vpc-0123456789abcdef0',
});
// Look up by tags
const taggedVpc = ec2.Vpc.fromLookup(this, 'TaggedVpc', {
tags: { Environment: 'production' },
});
// Import by attributes (no context lookup needed)
const importedVpc = ec2.Vpc.fromVpcAttributes(this, 'AttrVpc', {
vpcId: 'vpc-abc123',
availabilityZones: ['us-east-1a', 'us-east-1b'],
publicSubnetIds: ['subnet-pub1', 'subnet-pub2'],
privateSubnetIds: ['subnet-priv1', 'subnet-priv2'],
});---
Complete VPC Pattern
Production-ready three-tier VPC:
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
export class NetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, 'MainVpc', {
maxAzs: 3,
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
natGateways: 2,
subnetConfiguration: [
{ cidrMask: 22, name: 'Public', subnetType: ec2.SubnetType.PUBLIC },
{ cidrMask: 22, name: 'Private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
{ cidrMask: 24, name: 'Isolated', subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
],
flowLogs: {
default: {
destination: ec2.FlowLogDestination.toCloudWatchLogs(),
trafficType: ec2.FlowLogTrafficType.REJECT,
},
},
});
// Gateway endpoints (free)
this.vpc.addGatewayEndpoint('S3', {
service: ec2.GatewayVpcEndpointAwsService.S3,
});
this.vpc.addGatewayEndpoint('DynamoDB', {
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
});
// Outputs
new cdk.CfnOutput(this, 'VpcId', { value: this.vpc.vpcId });
new cdk.CfnOutput(this, 'PublicSubnets', {
value: this.vpc.publicSubnets.map(s => s.subnetId).join(','),
});
new cdk.CfnOutput(this, 'PrivateSubnets', {
value: this.vpc.privateSubnets.map(s => s.subnetId).join(','),
});
}
}AWS CDK Security Hardening
Table of Contents
- IAM Least Privilege
- KMS Encryption
- Secrets Manager
- Resource Policies
- WAF Integration
- Security Compliance Patterns
- Secure Defaults Checklist
---
IAM Least Privilege
Grant Helpers (Recommended)
CDK L2 constructs provide .grant*() methods that generate least-privilege IAM policies automatically.
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
const bucket = new s3.Bucket(this, 'Bucket');
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
});
const fn = new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
});
// Least-privilege grants — only necessary actions
bucket.grantRead(fn); // s3:GetObject, s3:GetBucket*
table.grantReadWriteData(fn); // dynamodb:GetItem, PutItem, UpdateItem, DeleteItem, Query, ScanCustom IAM Policies
When grant helpers are insufficient, create targeted policies:
import * as iam from 'aws-cdk-lib/aws-iam';
fn.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['ses:SendEmail', 'ses:SendRawEmail'],
resources: [`arn:aws:ses:${this.region}:${this.account}:identity/*`],
conditions: {
StringEquals: { 'ses:FromAddress': 'noreply@myapp.com' },
},
}));Service Roles
const role = new iam.Role(this, 'LambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
],
description: 'Role for order processing Lambda',
});
// Add permissions boundary
const boundary = iam.ManagedPolicy.fromAwsManagedPolicyName('PowerUserAccess');
iam.PermissionsBoundary.of(role).apply(boundary);Cross-Account Access
const crossAccountRole = new iam.Role(this, 'CrossAccountRole', {
assumedBy: new iam.AccountPrincipal('999999999999'),
externalIds: ['unique-external-id'],
});
bucket.grantRead(crossAccountRole);---
KMS Encryption
Customer-Managed Key
import { RemovalPolicy } from 'aws-cdk-lib';
import * as kms from 'aws-cdk-lib/aws-kms';
const key = new kms.Key(this, 'AppKey', {
alias: 'my-app-key',
description: 'Encryption key for application data',
enableKeyRotation: true,
removalPolicy: RemovalPolicy.RETAIN,
});
// Encrypt S3 bucket
const encryptedBucket = new s3.Bucket(this, 'EncryptedBucket', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: key,
bucketKeyEnabled: true, // Reduces KMS API calls
});
// Encrypt DynamoDB table
const encryptedTable = new dynamodb.Table(this, 'EncryptedTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED,
encryptionKey: key,
});
// Grant decrypt to Lambda
key.grantDecrypt(fn);SQS Encryption
const encryptedQueue = new sqs.Queue(this, 'EncryptedQueue', {
encryption: sqs.QueueEncryption.KMS,
encryptionMasterKey: key,
});---
Secrets Manager
Creating and Using Secrets
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
// Create a secret with auto-generated password
const dbSecret = new secretsmanager.Secret(this, 'DbSecret', {
secretName: 'my-app/db-credentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'admin' }),
generateStringKey: 'password',
excludePunctuation: true,
passwordLength: 32,
},
});
// Use in RDS
import * as rds from 'aws-cdk-lib/aws-rds';
const database = new rds.DatabaseInstance(this, 'Database', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_16,
}),
vpc,
credentials: rds.Credentials.fromSecret(dbSecret),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
});
// Grant Lambda read access to the secret
dbSecret.grantRead(fn);Referencing Secrets in Lambda
const apiKeySecret = new secretsmanager.Secret(this, 'ApiKey', {
secretName: 'my-app/api-key',
});
const fn = new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
SECRET_ARN: apiKeySecret.secretArn,
},
});
apiKeySecret.grantRead(fn);Secret Rotation
import { Duration } from 'aws-cdk-lib';
dbSecret.addRotationSchedule('Rotation', {
automaticallyAfter: Duration.days(30),
hostedRotation: secretsmanager.HostedRotation.postgreSqlSingleUser({
vpc,
excludeCharacters: '"@/\\',
}),
});---
Resource Policies
S3 Bucket Policy
const bucket = new s3.Bucket(this, 'SecureBucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
});
bucket.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.DENY,
principals: [new iam.AnyPrincipal()],
actions: ['s3:*'],
resources: [bucket.bucketArn, `${bucket.bucketArn}/*`],
conditions: {
Bool: { 'aws:SecureTransport': 'false' },
},
}));SQS Queue Policy
queue.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('sns.amazonaws.com')],
actions: ['sqs:SendMessage'],
resources: [queue.queueArn],
conditions: {
ArnEquals: { 'aws:SourceArn': topic.topicArn },
},
}));---
WAF Integration
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
const webAcl = new wafv2.CfnWebACL(this, 'WebAcl', {
defaultAction: { allow: {} },
scope: 'REGIONAL',
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'MyApiWAF',
},
rules: [
{
name: 'AWSManagedRulesCommonRuleSet',
priority: 1,
overrideAction: { none: {} },
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'CommonRules',
},
},
{
name: 'RateLimitRule',
priority: 2,
action: { block: {} },
statement: {
rateBasedStatement: {
limit: 2000,
aggregateKeyType: 'IP',
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'RateLimit',
},
},
],
});
// Associate WAF with API Gateway
new wafv2.CfnWebACLAssociation(this, 'WafAssociation', {
resourceArn: api.deploymentStage.stageArn,
webAclArn: webAcl.attrArn,
});---
Security Compliance Patterns
Enforce Tags with Aspects
import { IAspect, Annotations, Tags } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
import { CfnResource } from 'aws-cdk-lib';
class RequiredTagsAspect implements IAspect {
constructor(private requiredTags: string[]) {}
visit(node: IConstruct): void {
if (CfnResource.isCfnResource(node)) {
for (const tag of this.requiredTags) {
if (!Tags.of(node).tagValues()[tag]) {
Annotations.of(node).addError(`Missing required tag: ${tag}`);
}
}
}
}
}
Aspects.of(app).add(new RequiredTagsAspect(['Environment', 'Owner', 'CostCenter']));Suppress Specific CDK Nag Rules
import { NagSuppressions } from 'cdk-nag';
NagSuppressions.addResourceSuppressions(bucket, [
{
id: 'AwsSolutions-S1',
reason: 'Access logging not required for this dev bucket',
},
]);---
Secure Defaults Checklist
| Resource | Security Setting | CDK Property |
|---|---|---|
| S3 Bucket | Block public access | blockPublicAccess: BLOCK_ALL |
| S3 Bucket | Enforce SSL | enforceSSL: true |
| S3 Bucket | Enable encryption | encryption: BucketEncryption.S3_MANAGED |
| S3 Bucket | Enable versioning | versioned: true |
| DynamoDB | Enable PITR | pointInTimeRecovery: true |
| DynamoDB | Encryption | encryption: TableEncryption.AWS_MANAGED |
| Lambda | Timeout | timeout: Duration.seconds(30) |
| Lambda | Reserved concurrency | reservedConcurrentExecutions: N |
| RDS | Storage encryption | storageEncrypted: true |
| RDS | Multi-AZ | multiAz: true |
| RDS | Deletion protection | deletionProtection: true |
| API Gateway | Throttling | deployOptions: { throttlingRateLimit } |
| SQS | Encryption | encryption: QueueEncryption.KMS |
| SQS | Dead letter queue | deadLetterQueue: { queue, maxReceiveCount } |
AWS CDK Serverless Patterns
Table of Contents
- Lambda Functions
- API Gateway Integration
- DynamoDB Tables
- S3 Event Processing
- Step Functions
- EventBridge Rules
- SQS and SNS
- Complete Serverless API Pattern
---
Lambda Functions
Basic Lambda Function
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Duration } from 'aws-cdk-lib';
const fn = new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/my-function'),
timeout: Duration.seconds(30),
memorySize: 256,
environment: {
NODE_ENV: 'production',
},
});NodejsFunction (TypeScript with esbuild)
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
const fn = new lambdaNode.NodejsFunction(this, 'TsFunction', {
entry: 'lambda/handler.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_20_X,
timeout: Duration.seconds(30),
memorySize: 256,
bundling: {
minify: true,
sourceMap: true,
externalModules: ['@aws-sdk/*'],
format: lambdaNode.OutputFormat.ESM,
},
environment: {
NODE_OPTIONS: '--enable-source-maps',
},
});Lambda Layers
const layer = new lambda.LayerVersion(this, 'SharedLayer', {
code: lambda.Code.fromAsset('layers/shared'),
compatibleRuntimes: [lambda.Runtime.NODEJS_20_X],
description: 'Shared utilities and dependencies',
});
const fn = new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/handler'),
layers: [layer],
});Lambda with Dead Letter Queue
import * as sqs from 'aws-cdk-lib/aws-sqs';
const dlq = new sqs.Queue(this, 'DLQ', {
retentionPeriod: Duration.days(14),
});
const fn = new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
deadLetterQueue: dlq,
retryAttempts: 2,
});---
API Gateway Integration
REST API with Lambda
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
const api = new apigateway.RestApi(this, 'ItemsApi', {
restApiName: 'Items Service',
description: 'CRUD API for items',
deployOptions: {
stageName: 'prod',
throttlingBurstLimit: 100,
throttlingRateLimit: 50,
},
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
},
});
const items = api.root.addResource('items');
items.addMethod('GET', new apigateway.LambdaIntegration(listFn));
items.addMethod('POST', new apigateway.LambdaIntegration(createFn));
const item = items.addResource('{id}');
item.addMethod('GET', new apigateway.LambdaIntegration(getFn));
item.addMethod('PUT', new apigateway.LambdaIntegration(updateFn));
item.addMethod('DELETE', new apigateway.LambdaIntegration(deleteFn));HTTP API (API Gateway v2)
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
const httpApi = new apigatewayv2.HttpApi(this, 'HttpApi', {
apiName: 'My HTTP API',
corsPreflight: {
allowOrigins: ['https://myapp.com'],
allowMethods: [apigatewayv2.CorsHttpMethod.GET, apigatewayv2.CorsHttpMethod.POST],
},
});
httpApi.addRoutes({
path: '/items',
methods: [apigatewayv2.HttpMethod.GET],
integration: new integrations.HttpLambdaIntegration('ListItems', listFn),
});LambdaRestApi (L3 Pattern)
// Shortcut: creates API Gateway + proxy integration
const api = new apigateway.LambdaRestApi(this, 'QuickApi', {
handler: handlerFn,
proxy: true, // All requests forwarded to Lambda
});---
DynamoDB Tables
Single Table Design
import { RemovalPolicy } from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
const table = new dynamodb.Table(this, 'MainTable', {
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: true,
removalPolicy: RemovalPolicy.RETAIN,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
});
// Global Secondary Index
table.addGlobalSecondaryIndex({
indexName: 'GSI1',
partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
// Grant permissions to Lambda
table.grantReadWriteData(handlerFn);DynamoDB Stream Processing
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
const streamProcessor = new lambda.Function(this, 'StreamProcessor', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'stream.handler',
code: lambda.Code.fromAsset('lambda/stream'),
});
streamProcessor.addEventSource(new lambdaEventSources.DynamoEventSource(table, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
batchSize: 100,
retryAttempts: 3,
bisectBatchOnError: true,
}));---
S3 Event Processing
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
const uploadBucket = new s3.Bucket(this, 'UploadBucket', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
});
const processor = new lambda.Function(this, 'ImageProcessor', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/image-processor'),
timeout: Duration.minutes(5),
memorySize: 1024,
});
uploadBucket.grantRead(processor);
uploadBucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(processor),
{ prefix: 'uploads/', suffix: '.jpg' },
);---
Step Functions
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
const validateOrder = new tasks.LambdaInvoke(this, 'ValidateOrder', {
lambdaFunction: validateFn,
outputPath: '$.Payload',
});
const processPayment = new tasks.LambdaInvoke(this, 'ProcessPayment', {
lambdaFunction: paymentFn,
outputPath: '$.Payload',
});
const sendConfirmation = new tasks.LambdaInvoke(this, 'SendConfirmation', {
lambdaFunction: confirmFn,
});
const handleFailure = new tasks.LambdaInvoke(this, 'HandleFailure', {
lambdaFunction: failureFn,
});
const definition = validateOrder
.next(new sfn.Choice(this, 'IsValid?')
.when(sfn.Condition.booleanEquals('$.isValid', true),
processPayment.next(sendConfirmation))
.otherwise(handleFailure));
new sfn.StateMachine(this, 'OrderWorkflow', {
definitionBody: sfn.DefinitionBody.fromChainable(definition),
timeout: Duration.minutes(5),
tracingEnabled: true,
});---
EventBridge Rules
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
// Scheduled rule
new events.Rule(this, 'DailyCleanup', {
schedule: events.Schedule.cron({ hour: '2', minute: '0' }),
targets: [new targets.LambdaFunction(cleanupFn)],
});
// Custom event pattern
new events.Rule(this, 'OrderCreated', {
eventPattern: {
source: ['my-app.orders'],
detailType: ['OrderCreated'],
},
targets: [
new targets.LambdaFunction(processFn),
new targets.SqsQueue(auditQueue),
],
});---
SQS and SNS
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as snsSubscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
// SNS Topic
const topic = new sns.Topic(this, 'OrderTopic', {
displayName: 'Order Notifications',
});
// SQS Queue with DLQ
const dlq = new sqs.Queue(this, 'DLQ');
const queue = new sqs.Queue(this, 'OrderQueue', {
visibilityTimeout: Duration.seconds(300),
deadLetterQueue: { queue: dlq, maxReceiveCount: 3 },
});
// Subscribe SQS to SNS
topic.addSubscription(new snsSubscriptions.SqsSubscription(queue));
// Lambda consumes from SQS
const consumer = new lambda.Function(this, 'Consumer', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/consumer'),
});
consumer.addEventSource(new lambdaEventSources.SqsEventSource(queue, {
batchSize: 10,
maxBatchingWindow: Duration.seconds(5),
}));---
Complete Serverless API Pattern
Full example combining Lambda, API Gateway, DynamoDB, and proper IAM:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as logs from 'aws-cdk-lib/aws-logs';
export class CrudApiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// DynamoDB Table
const table = new dynamodb.Table(this, 'ItemsTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Shared Lambda configuration
const sharedProps: Partial<lambdaNode.NodejsFunctionProps> = {
runtime: lambda.Runtime.NODEJS_20_X,
timeout: cdk.Duration.seconds(10),
memorySize: 256,
environment: { TABLE_NAME: table.tableName },
logRetention: logs.RetentionDays.ONE_WEEK,
bundling: { minify: true, sourceMap: true },
};
// CRUD Functions
const createFn = new lambdaNode.NodejsFunction(this, 'CreateFn', {
...sharedProps, entry: 'lambda/create.ts',
});
const listFn = new lambdaNode.NodejsFunction(this, 'ListFn', {
...sharedProps, entry: 'lambda/list.ts',
});
const getFn = new lambdaNode.NodejsFunction(this, 'GetFn', {
...sharedProps, entry: 'lambda/get.ts',
});
const deleteFn = new lambdaNode.NodejsFunction(this, 'DeleteFn', {
...sharedProps, entry: 'lambda/delete.ts',
});
// Least-privilege permissions
table.grantWriteData(createFn);
table.grantReadData(listFn);
table.grantReadData(getFn);
table.grantWriteData(deleteFn);
// API Gateway
const api = new apigateway.RestApi(this, 'ItemsApi', {
restApiName: 'Items CRUD',
deployOptions: { stageName: 'v1' },
});
const items = api.root.addResource('items');
items.addMethod('POST', new apigateway.LambdaIntegration(createFn));
items.addMethod('GET', new apigateway.LambdaIntegration(listFn));
const item = items.addResource('{id}');
item.addMethod('GET', new apigateway.LambdaIntegration(getFn));
item.addMethod('DELETE', new apigateway.LambdaIntegration(deleteFn));
// Outputs
new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
new cdk.CfnOutput(this, 'TableName', { value: table.tableName });
}
}AWS CDK Testing Strategies
Table of Contents
- Testing Overview
- Fine-Grained Assertions
- Snapshot Testing
- Validation Testing
- Integration Testing
- CDK Nag Compliance
- Testing Patterns
---
Testing Overview
CDK provides the assertions module for testing synthesized CloudFormation templates without deploying.
| Test Type | Purpose | Speed | Maintenance |
|---|---|---|---|
| Fine-grained assertions | Verify specific resource properties | Fast | Low |
| Snapshot tests | Detect unintended template changes | Fast | Medium |
| Validation tests | Test custom construct validation logic | Fast | Low |
| Integration tests | Deploy and verify real AWS resources | Slow | High |
| CDK Nag | Compliance and best-practice checks | Fast | Low |
Setup
npm install --save-dev jest @types/jest ts-jest// jest.config.js
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: { '^.+\\.tsx?$': 'ts-jest' },
};---
Fine-Grained Assertions
Basic Resource Assertions
import { App } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { MyStack } from '../lib/my-stack';
describe('MyStack', () => {
let template: Template;
beforeEach(() => {
const app = new App();
const stack = new MyStack(app, 'TestStack');
template = Template.fromStack(stack);
});
test('creates a DynamoDB table with PAY_PER_REQUEST', () => {
template.hasResourceProperties('AWS::DynamoDB::Table', {
BillingMode: 'PAY_PER_REQUEST',
});
});
test('creates a Lambda function with correct runtime', () => {
template.hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'nodejs20.x',
Timeout: 30,
});
});
test('creates exactly 2 Lambda functions', () => {
template.resourceCountIs('AWS::Lambda::Function', 2);
});
});Match Helpers
import { Match } from 'aws-cdk-lib/assertions';
// Match any value
template.hasResourceProperties('AWS::Lambda::Function', {
Handler: Match.anyValue(),
Runtime: 'nodejs20.x',
});
// Match object with specific keys (ignoring others)
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: Match.objectLike({
TABLE_NAME: Match.anyValue(),
}),
},
});
// Match absent property
template.hasResourceProperties('AWS::S3::Bucket', {
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
},
WebsiteConfiguration: Match.absent(),
});
// Match array containing specific elements
template.hasResourceProperties('AWS::IAM::Role', {
ManagedPolicyArns: Match.arrayWith([
Match.objectLike({
'Fn::Join': Match.anyValue(),
}),
]),
});Testing Resource Counts
test('creates expected number of resources', () => {
template.resourceCountIs('AWS::Lambda::Function', 3);
template.resourceCountIs('AWS::DynamoDB::Table', 1);
template.resourceCountIs('AWS::SQS::Queue', 2); // main + DLQ
template.resourceCountIs('AWS::ApiGateway::RestApi', 1);
});Testing Outputs
test('exports API URL', () => {
template.hasOutput('ApiUrl', {
Value: Match.anyValue(),
});
});---
Snapshot Testing
Snapshot tests capture the entire synthesized template and detect any change.
Basic Snapshot
test('matches snapshot', () => {
const app = new App();
const stack = new MyStack(app, 'TestStack');
const template = Template.fromStack(stack);
expect(template.toJSON()).toMatchSnapshot();
});Updating Snapshots
# Regenerate snapshots after intentional changes
npx jest --updateSnapshotWhen to Use Snapshots
| Scenario | Recommended |
|---|---|
| Detect accidental changes | ✅ Yes |
| Verify specific properties | ❌ Use fine-grained assertions |
| Stable, rarely-changing stacks | ✅ Yes |
| Rapidly iterating stacks | ❌ Too many snapshot updates |
---
Validation Testing
Test custom validation logic in your constructs.
import { App, Stack } from 'aws-cdk-lib';
// Custom construct with validation
class MyConstruct extends Construct {
constructor(scope: Construct, id: string, props: { maxRetries: number }) {
super(scope, id);
if (props.maxRetries < 0 || props.maxRetries > 10) {
throw new Error('maxRetries must be between 0 and 10');
}
}
}
// Test validation
test('throws on invalid maxRetries', () => {
const app = new App();
const stack = new Stack(app, 'TestStack');
expect(() => {
new MyConstruct(stack, 'Bad', { maxRetries: -1 });
}).toThrow('maxRetries must be between 0 and 10');
expect(() => {
new MyConstruct(stack, 'Also Bad', { maxRetries: 99 });
}).toThrow('maxRetries must be between 0 and 10');
});
test('accepts valid maxRetries', () => {
const app = new App();
const stack = new Stack(app, 'TestStack');
expect(() => {
new MyConstruct(stack, 'Good', { maxRetries: 3 });
}).not.toThrow();
});---
Integration Testing
CDK integ-tests Module
import { IntegTest } from '@aws-cdk/integ-tests-alpha';
import { App } from 'aws-cdk-lib';
const app = new App();
const stack = new MyStack(app, 'IntegTestStack');
const integ = new IntegTest(app, 'MyIntegTest', {
testCases: [stack],
diffAssets: true,
stackUpdateWorkflow: true,
});
// Assert deployed resources
integ.assertions
.httpApiCall('https://my-api.execute-api.us-east-1.amazonaws.com/items')
.expect(ExpectedResult.objectLike({ statusCode: 200 }));Running Integration Tests
# Deploy and test
npx integ-runner --directory test/integ --parallel-regions us-east-1
# Update snapshots after successful deploy
npx integ-runner --directory test/integ --update-on-failed---
CDK Nag Compliance
CDK Nag checks your stacks against best-practice rule packs.
Setup
npm install cdk-nagimport { Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks, HIPAASecurityChecks } from 'cdk-nag';
const app = new App();
const stack = new MyStack(app, 'ProdStack');
// Add compliance checks
Aspects.of(stack).add(new AwsSolutionsChecks({ verbose: true }));
// Or HIPAA compliance
// Aspects.of(stack).add(new HIPAASecurityChecks());Available Rule Packs
| Pack | Description |
|---|---|
AwsSolutionsChecks | AWS Solutions Library best practices |
HIPAASecurityChecks | HIPAA compliance rules |
NIST80053R4Checks | NIST 800-53 Rev 4 |
NIST80053R5Checks | NIST 800-53 Rev 5 |
PCI321Checks | PCI DSS 3.2.1 |
Suppressing Rules
import { NagSuppressions } from 'cdk-nag';
// Suppress at resource level
NagSuppressions.addResourceSuppressions(myBucket, [
{ id: 'AwsSolutions-S1', reason: 'Access logging not needed for dev' },
]);
// Suppress at stack level
NagSuppressions.addStackSuppressions(stack, [
{ id: 'AwsSolutions-IAM4', reason: 'Using AWS managed policies is acceptable here' },
]);Testing with CDK Nag
import { Annotations } from 'aws-cdk-lib/assertions';
import { AwsSolutionsChecks } from 'cdk-nag';
test('passes AWS Solutions checks', () => {
const app = new App();
const stack = new MyStack(app, 'TestStack');
Aspects.of(stack).add(new AwsSolutionsChecks());
const annotations = Annotations.fromStack(stack);
// No errors
annotations.hasNoError('*', Match.anyValue());
// Optionally check no warnings
annotations.hasNoWarning('*', Match.anyValue());
});---
Testing Patterns
Pattern: Test Environment-Specific Behavior
test('production stack has termination protection', () => {
const app = new App();
const stack = new MyStack(app, 'ProdStack', {
env: { account: '123456789012', region: 'us-east-1' },
terminationProtection: true,
});
expect(stack.terminationProtection).toBe(true);
});
test('dev stack allows removal', () => {
const app = new App();
const stack = new MyStack(app, 'DevStack', {
terminationProtection: false,
});
const template = Template.fromStack(stack);
template.hasResource('AWS::DynamoDB::Table', {
DeletionPolicy: 'Delete',
});
});Pattern: Test IAM Permissions
test('Lambda has read-only access to S3', () => {
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: Match.arrayWith([
Match.objectLike({
Action: Match.arrayWith(['s3:GetObject*', 's3:GetBucket*']),
Effect: 'Allow',
}),
]),
},
});
});Pattern: Test Cross-Stack References
test('app stack uses VPC from network stack', () => {
const app = new App();
const networkStack = new NetworkStack(app, 'Network');
const appStack = new AppStack(app, 'App', { vpc: networkStack.vpc });
const template = Template.fromStack(appStack);
template.hasResourceProperties('AWS::Lambda::Function', {
VpcConfig: {
SubnetIds: Match.anyValue(),
SecurityGroupIds: Match.anyValue(),
},
});
});Pattern: Parameterized Tests
describe.each([
['dev', false, 'PAY_PER_REQUEST'],
['prod', true, 'PAY_PER_REQUEST'],
])('environment: %s', (env, pitr, billing) => {
test(`DynamoDB has PITR=${pitr}`, () => {
const app = new App({ context: { stage: env } });
const stack = new MyStack(app, 'Stack');
const template = Template.fromStack(stack);
if (pitr) {
template.hasResourceProperties('AWS::DynamoDB::Table', {
PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
});
}
});
});