
Aws Cdk
- 1.3k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
aws-cdk is an agent skill that provides AWS CDK TypeScript patterns for stacks, constructs, and synth-diff-deploy validation workflows.
About
The aws-cdk skill provides TypeScript patterns for defining, validating, and deploying AWS infrastructure as code with the CDK. It covers cdk init app scaffolding, stack and reusable construct design, choosing L1 L2 and L3 constructs, and validation-first workflows using cdk synth, tests, cdk diff, and cdk deploy. Guidance includes IAM least privilege defaults, encryption settings, VPC and serverless architectures, multi-stack apps, and environment-aware configuration. Agents help refactor existing CDK apps, wire cross-stack references, and review diffs before deployment. Triggers include aws cdk typescript, create cdk app, cdk stack, cdk construct, cdk deploy, and cdk test requests inside developer-kit workflows.
- TypeScript CDK patterns for apps, stacks, and reusable constructs.
- Validation loop with cdk synth, tests, diff, and deploy steps.
- Covers L1 L2 L3 construct selection and multi-stack wiring.
- Emphasizes IAM, encryption, and environment-aware defaults.
- Supports serverless and VPC-focused infrastructure modeling.
Aws Cdk by the numbers
- 1,259 all-time installs (skills.sh)
- +55 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #296 of 1,041 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
aws-cdk capabilities & compatibility
- Capabilities
- cdk app and stack scaffolding · reusable construct patterns · synth diff deploy validation · iam and encryption defaults · multi stack environment configuration
- Works with
- aws
- Use cases
- devops · api development
What aws-cdk says it does
Provides AWS CDK TypeScript patterns for defining, validating, and deploying AWS infrastructure as code.
npx cdk init app --language typescript
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill aws-cdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How should I structure and validate an AWS CDK TypeScript app before deploying infrastructure changes?
Build and validate AWS CDK TypeScript apps, stacks, constructs, synth, diff, and deploy loops.
Who is it for?
Developers creating or refactoring AWS CDK TypeScript infrastructure with validation-first delivery.
Skip if: Skip for raw CloudFormation YAML editing without CDK or non-AWS infrastructure tools.
When should I use this skill?
User creates CDK apps, constructs, stacks, or runs cdk synth, diff, and deploy tasks.
What you get
CDK app or stack changes with synth-tested constructs, reviewed diffs, and safer IAM and encryption defaults.
- CDK stack source code
- Synthesized CloudFormation templates
By the numbers
- Covers 8 core CDK concept sections in the developer-kit readme
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 },
});
}
});
});Related skills
Forks & variants (1)
Aws Cdk has 1 known copy in the catalog totaling 2 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 2 installs
How it compares
Pick aws-cdk over generic CloudFormation skills when you want typed constructs, aspect hooks, and the full CDK synthesis pipeline in TypeScript or Python.
FAQ
What validation loop does aws-cdk recommend?
cdk synth, tests, cdk diff, then cdk deploy after reviewing infrastructure changes.
Which construct levels does it cover?
Guidance for choosing L1, L2, and L3 constructs based on reuse and control needs.
What project layout does cdk init create?
bin entry, lib stack, test folder, cdk.json, and tsconfig for a TypeScript CDK app.
Is Aws Cdk safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.