
Mastering Aws Cdk
- 2 installs
- Updated January 12, 2026
- spillwavesolutions/mastering-aws-cdk-plugin
Guides AWS CDK v2 infrastructure-as-code in TypeScript with stack patterns, deploy troubleshooting, and GitHub Actions OIDC setup.
About
Provides AWS CDK v2 TypeScript patterns for building stacks, debugging synth/diff/deploy failures, and integrating services like Lambda, ECS, and DynamoDB. A developer uses it when creating or refactoring CDK stacks or debugging CloudFormation errors.
- Prefers L2 constructs, least-privilege IAM, and stacks under 500 resources
- Covers cdk synth/diff/deploy workflow and GitHub Actions OIDC deployments
Mastering Aws Cdk by the numbers
- 2 all-time installs (skills.sh)
- Ranked #917 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/mastering-aws-cdk-plugin --skill mastering-aws-cdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| Last updated | January 12, 2026 |
| Repository | spillwavesolutions/mastering-aws-cdk-plugin ↗ |
What it does
Guides AWS CDK v2 infrastructure-as-code in TypeScript with stack patterns, deploy troubleshooting, and GitHub Actions OIDC setup.
Files
Mastering AWS CDK v2 (TypeScript)
Focused guidance for building, deploying, and troubleshooting AWS CDK v2 infrastructure in TypeScript.
Contents
- Use This Skill When
- Trigger Terms
- Quick Start
- Workflow
- Reference Map
- Guardrails
- Debugging Checklist
- When Not to Use
- Reference Files
Use This Skill When
- Building new CDK apps or stacks in TypeScript
- Refactoring or splitting stacks to manage limits
- Debugging synth/diff/deploy failures or CloudFormation rollbacks
- Importing existing resources into CDK management
- Driving stacks from JSON/YAML configuration files
- Setting up GitHub Actions OIDC deployments
- Implementing service patterns across AWS managed services
- Writing CDK tests and running security checks
Trigger Terms
Use for queries mentioning: cdk, cdk deploy, cdk diff, cdk synth, cdk import, cdk watch, cdk refactor, cdk bootstrap, cdk-nag, hotswap, CloudFormation, stack rollback, cdk.context.json, cdk.json, SSM Parameter Store, hnb659fds, or OIDC GitHub Actions.
Quick Start
1. Confirm target account, region, and environment (dev/stage/prod). 2. Run cdk synth then cdk diff to validate changes. 3. Deploy with cdk deploy --require-approval=never in CI.
Workflow
1) Intake
Collect:
- account and region
- environment name and stage
- target services and integrations
- existing resources to import or avoid replacement
2) Stack Design
- Keep stacks under 500 resources (split or use nested stacks)
- Pass outputs via props or explicit exports
- Set removal policies for stateful resources (retain by default)
3) Implement
- Prefer L2 constructs; use L1 only for gaps
- Apply least-privilege IAM grants
- Keep resource names deterministic
4) Validate
cdk synthto inspect the templatecdk diffto review changescdk doctorfor environment issues
5) Deploy
- Ensure bootstrap completed for the account/region
- Review CloudFormation events on failure
- Use
--require-approval=neveronly for CI
6) Observability
- Add log retention, alarms, and dashboards early
- Use X-Ray where distributed tracing matters
- See observability.md
Reference Map
| Task | Reference |
|---|---|
| Troubleshooting errors | troubleshooting.md |
| CI/CD with GitHub Actions | cicd-github.md |
| Service-specific patterns | services.md |
| Observability setup | observability.md |
| Architecture and operations | architecture-ops.md |
| Testing and security | testing-security.md |
| Latest features | latest-features.md |
Guardrails
- Do not modify CloudFormation-managed resources in the console
- Avoid dynamic values (Date.now, random) in resource definitions
- Use
env: { account, region }for lookups (VPC/AZ/AMI) - Use stable IDs when generating constructs from config data
- Use
cdk import(adopt) for existing resources; usefromXxxonly for read-only references - Do not use hotswap in production pipelines
Debugging Checklist
Copy and track progress:
Debugging Progress:
- [ ] Check CloudFormation events (Console -> Stack -> Events)
- [ ] Re-run with verbose output: `cdk deploy --progress events`
- [ ] Inspect template: `cdk synth > template.yaml`
- [ ] Run diff: `cdk diff`
- [ ] Check service logs (Lambda: CloudWatch, ECS: task events)
- [ ] Run `cdk doctor`When Not to Use
- Terraform/Pulumi or raw CloudFormation templates
- Manual console-driven resource management
- CDK in Python/Java/Go/C# (TypeScript only)
Reference Files
- references/troubleshooting.md -- Error messages and fixes
- references/cicd-github.md -- GitHub Actions OIDC setup
- references/services.md -- Lambda, ECS, MSK, DynamoDB, Aurora, S3, EventBridge patterns
- references/observability.md -- CloudWatch, X-Ray, dashboards, alarms
- references/architecture-ops.md -- Determinism, configuration-driven patterns, imports, drift, and operational workflows
- references/testing-security.md -- CDK testing, cdk-nag, and compliance checks
- references/latest-features.md -- New constructs, CLI capabilities, and recent patterns
Architecture and Operations for AWS CDK
Contents
- Imperative to Declarative Model
- Construct Levels and Abstractions
- Determinism and Context
- Project Structure and Dependency Hygiene
- Configuration-Driven Infrastructure
- Importing Existing Resources
- Cross-Stack References
- Drift Detection and Reconciliation
- Rapid Iteration Tools
- CDK Pipelines and Self-Mutation
- Advanced Debugging
---
Imperative to Declarative Model
CDK code compiles into CloudFormation templates. Debugging often requires checking both:
- CDK logic (imperative code)
- CloudFormation output (declarative template)
Use cdk synth to inspect cdk.out/<Stack>.template.json when resources are missing or misconfigured.
Construct Levels and Abstractions
Construct levels determine how much abstraction you get:
- L1 (Cfn*): one-to-one with CloudFormation resources, fully explicit
- L2: opinionated defaults and helper logic
- L3: higher-level patterns composed of multiple resources
Prefer L2 or L3 unless you need a missing property or edge behavior from L1. Solutions Constructs provide vetted L3 patterns and factory helpers for common integrations.
Determinism and Context
Synthesis must be deterministic. Avoid volatile values in constructs:
Date.now()or random values- network lookups without cached context
Use cdk.context.json for environment lookups (VPCs, AMIs). Commit it to version control and reset with:
cdk context --reset KEYContext is not application configuration. Use external JSON/YAML for app intent, not context files.
Project Structure and Dependency Hygiene
Keep CDK dependencies in sync:
aws-cdk-libandconstructsversions must match- Commit lock files (
package-lock.jsonoryarn.lock)
For multi-app setups:
- Prefer monorepo tooling to enforce consistent CDK versions
- Use semantic versioning for shared construct libraries
- Group CDK updates together in Renovate or Dependabot
Optional tooling:
- Projen can generate and manage project configuration files from a single
.projenrcsource.
Logical Units for Large Apps
Split by lifecycle and blast radius:
- Stateful stacks (databases, storage) with termination protection
- Stateless stacks (compute, APIs) that can be replaced safely
- Infrastructure stacks (networking, shared security, observability)
Configuration-Driven Infrastructure
When generating constructs from JSON/YAML:
- Load config at synth time
- Use a stable, unique ID from the config as the construct ID
- Never use array indices as construct IDs
Example config:
{
"services": {
"payment-gateway": { "memory": 1024, "replicas": 2 },
"user-auth": { "memory": 512, "replicas": 1 }
}
}Importing Existing Resources
To adopt existing resources without re-creating them: 1. Model the resource in CDK with matching properties and physical names. 2. Generate a mapping file:
cdk import --record-resource-mapping mapping.json3. Review and edit mapping.json for correct logical-to-physical IDs. 4. Import:
cdk import --resource-mapping mapping.jsonIf the modeled resource diverges from the real resource, CloudFormation may update it after import.
Cross-Stack References
Prefer SSM Parameter Store for cross-account or cross-region references:
- Avoids tight deployment ordering
- Works across accounts and regions
Use CloudFormation exports only for simple, same-account cases.
Drift Detection and Reconciliation
Use CloudFormation drift detection to check divergence between live resources and the stack template. Reconcile by:
- Overwriting drift (deploy to restore desired state)
- Adopting drift (update CDK code to match reality)
Avoid manual console changes to prevent drift.
Rapid Iteration Tools
Development-only tools:
cdk watchfor rapid rebuild/deploy cyclescdk deploy --hotswapfor fast code updates on supported resources
Hotswap introduces drift. Never use it for production pipelines.
CDK Pipelines and Self-Mutation
CDK Pipelines can update themselves when the pipeline definition changes:
- The synth step detects pipeline changes
- A self-mutate step updates the pipeline before deploying app stacks
This keeps delivery infrastructure aligned with the app definition.
Advanced Debugging
Synthesis debugging:
- Use
cdk synthand inspectcdk.out/* - Run with a debugger in your IDE for complex logic
Deployment debugging:
- Use
cdk diffto spot replacements - Use
cdk deploy --no-rollbackto preserve failed resources for inspection
Custom resource issues:
- Check the
AWSCDK-CustomResource-*Lambda logs - Timeouts often indicate missing NAT or VPC endpoints
Logical ID changes:
- Use
cdk refactorto rename or move constructs safely
CI/CD with GitHub Actions and OIDC
Contents
---
OIDC Authentication Setup
Use OIDC instead of storing AWS access keys in GitHub secrets.
Step 1: Create OIDC Provider in AWS
In IAM Console → Identity Providers → Add Provider:
- Provider type: OpenID Connect
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
Step 2: Create IAM Role for GitHub Actions
Trust policy (restrict to your repo and branch):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/heads/main"
}
}
}]
}Trust Policy Guardrails
- Lock
subto a specific repo and branch (avoid wildcards) - Require
audto bests.amazonaws.com - Use
StringEqualswherever possible
Step 3: Attach Permissions
Minimum permissions for CDK deployment:
sts:AssumeRoleon CDK bootstrap roles- Or use
AdministratorAccessfor simplicity (scope down for production)
Least-Privilege Notes
- The GitHub Actions role should only deploy via CloudFormation
- CloudFormation uses its own execution role for resource creation
- Avoid granting broad service permissions directly to the GitHub Actions role
For least privilege, allow assuming CDK bootstrap roles:
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::*:role/cdk-hnb659fds-deploy-role-*",
"arn:aws:iam::*:role/cdk-hnb659fds-file-publishing-role-*",
"arn:aws:iam::*:role/cdk-hnb659fds-image-publishing-role-*",
"arn:aws:iam::*:role/cdk-hnb659fds-lookup-role-*"
]
}---
GitHub Actions Workflow
Basic CDK Deploy Workflow
name: CDK Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GHActionsCDKRole
aws-region: us-east-1
- name: CDK Synth
run: npx cdk synth
- name: CDK Diff
if: github.event_name == 'pull_request'
run: npx cdk diff
- name: CDK Deploy
if: github.ref == 'refs/heads/main'
run: npx cdk deploy --all --require-approval=never
### Synth Once, Deploy Many
Build `cdk.out` once and promote the artifact:
- Build stage: `npx cdk synth`
- Deploy stages: `npx cdk deploy --app cdk.out`
This ensures the same template is promoted across environments.
### Permissions Recap
Required GitHub workflow permissions:permissions: id-token: write contents: read
With Caching and Bootstrap Check
- name: Check bootstrap
run: |
aws cloudformation describe-stacks \
--stack-name CDKToolkit \
--query 'Stacks[0].StackStatus' \
--output text || echo "Not bootstrapped"
- name: CDK Bootstrap (if needed)
run: npx cdk bootstrap aws://${{ secrets.AWS_ACCOUNT_ID }}/${{ env.AWS_REGION }}
env:
AWS_REGION: us-east-1---
Multi-Account Deployments
Cross-Account Trust
When a pipeline runs in a tooling account, bootstrap targets with trust:
cdk bootstrap --trust TOOLING_ACCOUNT_IDSeparate Roles per Account
jobs:
deploy-dev:
environment: development
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::DEV_ACCOUNT:role/GHActionsRole
aws-region: us-east-1
- run: npx cdk deploy --all
deploy-prod:
needs: deploy-dev
environment: production
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::PROD_ACCOUNT:role/GHActionsRole
aws-region: us-east-1
- run: npx cdk deploy --allCDK Pipelines (Self-Mutating)
For complex multi-account setups, consider CDK Pipelines:
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
const pipeline = new CodePipeline(this, 'Pipeline', {
pipelineName: 'MyPipeline',
synth: new ShellStep('Synth', {
input: CodePipelineSource.gitHub('org/repo', 'main'),
commands: ['npm ci', 'npx cdk synth'],
}),
});
pipeline.addStage(new DevStage(this, 'Dev'));
pipeline.addStage(new ProdStage(this, 'Prod'), {
pre: [new ManualApprovalStep('PromoteToProd')],
});Multi-Region Waves
Use Waves to deploy multiple regions in parallel:
const wave = pipeline.addWave('ProdWave');
wave.addStage(new ProdStage(this, 'ProdUsEast1', { env: { account, region: 'us-east-1' } }));
wave.addStage(new ProdStage(this, 'ProdEuWest1', { env: { account, region: 'eu-west-1' } }));---
Security Best Practices
Scope OIDC Trust Policy
- Restrict to specific repository:
repo:ORG/REPO:* - Restrict to specific branch:
repo:ORG/REPO:ref:refs/heads/main - Restrict to specific environment:
repo:ORG/REPO:environment:production
Least Privilege Permissions
- Use CDK bootstrap role assumption instead of direct permissions
- Review CloudTrail logs for actual permissions used
- Create custom policies based on actual needs
Pipeline Security
- Use GitHub Environments with protection rules for production
- Require approvals for production deployments
- Enable branch protection on main branch
Avoid in CI/CD
- Don't commit
cdk.outor generated templates - Don't store AWS keys in GitHub secrets (use OIDC)
- Don't skip
--require-approvalinteractively in CI
Latest Features and Recent Patterns
Contents
- CDK v2 Consolidation
- Construct Lifecycle Phases
- Alpha Modules and Opt-In APIs
- Feature Flags
- Recent Service Additions
---
CDK v2 Consolidation
CDK v2 ships as a single package (aws-cdk-lib) to avoid version drift across modules.
Construct Lifecycle Phases
CDK apps move through: 1. Construction 2. Preparation 3. Validation 4. Synthesis 5. Deployment
Use this model when debugging issues that appear only at synth or deploy time.
Alpha Modules and Opt-In APIs
Experimental modules are explicitly labeled -alpha and may introduce breaking changes:
- Example:
aws-cdk-lib/aws-pipes-alpha
Only use alpha modules when you can tolerate API changes.
Feature Flags
New defaults are gated by cdk.json feature flags. Review and enable new flags periodically to adopt security fixes and behavior improvements.
Recent Service Additions
Keep an eye on new L2 constructs as AWS adds services:
- Data Firehose
- AppSync Events
- EKS with auto mode
Observability with CDK
Contents
- CloudWatch Logs
- CloudWatch Metrics and Alarms
- CloudWatch Dashboards
- X-Ray Tracing
- Service-Specific Observability
- Embedded Metric Format (EMF)
- Log Subscriptions
---
CloudWatch Logs
Log Group with Retention
const logGroup = new logs.LogGroup(this, 'AppLogs', {
logGroupName: '/myapp/service',
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});Lambda Logging
// Explicit log group (recommended for control)
const fn = new lambda.Function(this, 'Fn', {
// ...
logGroup: new logs.LogGroup(this, 'FnLogs', {
retention: logs.RetentionDays.TWO_WEEKS,
}),
});
// Or use logRetention shorthand
const fn2 = new lambda.Function(this, 'Fn2', {
// ...
logRetention: logs.RetentionDays.ONE_WEEK,
});ECS Container Logging
taskDef.addContainer('App', {
image,
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'myapp',
logGroup: new logs.LogGroup(this, 'EcsLogs', {
retention: logs.RetentionDays.ONE_MONTH,
}),
}),
});Metric Filters
new logs.MetricFilter(this, 'ErrorFilter', {
logGroup,
metricNamespace: 'MyApp',
metricName: 'ErrorCount',
filterPattern: logs.FilterPattern.literal('ERROR'),
metricValue: '1',
});---
CloudWatch Metrics and Alarms
Service Metrics
// Lambda
fn.metricErrors({ period: Duration.minutes(5) });
fn.metricDuration({ statistic: 'p99' });
fn.metricInvocations();
// DynamoDB
table.metricThrottledRequests();
table.metricConsumedReadCapacityUnits();
// SQS
queue.metricApproximateNumberOfMessagesVisible();
queue.metricApproximateAgeOfOldestMessage();
// S3
bucket.metric5xxErrors();
bucket.metricGetRequests();Creating Alarms
new cloudwatch.Alarm(this, 'LambdaErrors', {
metric: fn.metricErrors({ period: Duration.minutes(5) }),
threshold: 1,
evaluationPeriods: 1,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
alarmDescription: 'Lambda function errors detected',
});
// Alarm with SNS notification
const topic = new sns.Topic(this, 'AlertTopic');
alarm.addAlarmAction(new cw_actions.SnsAction(topic));Composite Alarms
const composite = new cloudwatch.CompositeAlarm(this, 'ServiceHealth', {
alarmRule: cloudwatch.AlarmRule.anyOf(
errorAlarm,
latencyAlarm,
throttleAlarm
),
});Custom Metrics
const customMetric = new cloudwatch.Metric({
namespace: 'MyApp',
metricName: 'OrdersProcessed',
dimensionsMap: { Environment: 'prod' },
statistic: 'Sum',
period: Duration.minutes(1),
});---
CloudWatch Dashboards
Basic Dashboard
const dashboard = new cloudwatch.Dashboard(this, 'Dashboard', {
dashboardName: 'MyAppDashboard',
});
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'Lambda Invocations',
left: [fn.metricInvocations()],
right: [fn.metricErrors()],
width: 12,
}),
new cloudwatch.SingleValueWidget({
title: 'DynamoDB Reads',
metrics: [table.metricConsumedReadCapacityUnits()],
width: 6,
}),
);Multi-Row Dashboard
dashboard.addWidgets(
// Row 1: Lambda
new cloudwatch.GraphWidget({
title: 'Lambda Performance',
left: [fn.metricDuration({ statistic: 'p50' })],
right: [fn.metricDuration({ statistic: 'p99' })],
}),
);
dashboard.addWidgets(
// Row 2: DynamoDB
new cloudwatch.GraphWidget({
title: 'DynamoDB Capacity',
left: [
table.metricConsumedReadCapacityUnits(),
table.metricConsumedWriteCapacityUnits(),
],
}),
);Alarm Status Widget
dashboard.addWidgets(
new cloudwatch.AlarmStatusWidget({
title: 'Service Alarms',
alarms: [errorAlarm, latencyAlarm, throttleAlarm],
width: 24,
}),
);---
X-Ray Tracing
Lambda Tracing
const fn = new lambda.Function(this, 'Fn', {
// ...
tracing: lambda.Tracing.ACTIVE,
});API Gateway Tracing
const api = new apigateway.RestApi(this, 'Api', {
deployOptions: {
tracingEnabled: true,
dataTraceEnabled: true, // Log request/response
loggingLevel: apigateway.MethodLoggingLevel.INFO,
},
});Step Functions Tracing
const stateMachine = new sfn.StateMachine(this, 'SM', {
// ...
tracingEnabled: true,
});ECS X-Ray Daemon
// Add X-Ray daemon sidecar
taskDef.addContainer('xray', {
image: ecs.ContainerImage.fromRegistry('amazon/aws-xray-daemon'),
cpu: 32,
memoryReservationMiB: 256,
essential: false,
portMappings: [{ containerPort: 2000, protocol: ecs.Protocol.UDP }],
});---
Service-Specific Observability
Lambda Insights
const fn = new lambda.Function(this, 'Fn', {
// ...
insightsVersion: lambda.LambdaInsightsVersion.VERSION_1_0_143_0,
});Provides: CPU, memory, cold starts, network metrics.
Container Insights
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
containerInsights: true,
});Provides: Task/service metrics, CPU/memory per container.
RDS Performance Insights
const cluster = new rds.DatabaseCluster(this, 'DB', {
// ...
enablePerformanceInsights: true,
performanceInsightRetention: rds.PerformanceInsightRetention.DEFAULT,
});Key Metrics to Monitor
| Service | Critical Metrics |
|---|---|
| Lambda | Errors, Duration (p99), ConcurrentExecutions, Throttles |
| DynamoDB | ThrottledRequests, ConsumedCapacity, SystemErrors |
| ECS | CPUUtilization, MemoryUtilization, RunningTaskCount |
| SQS | ApproximateAgeOfOldestMessage, NumberOfMessagesDeleted |
| API Gateway | 4XXError, 5XXError, Latency, Count |
| RDS | CPUUtilization, FreeableMemory, DatabaseConnections |
Structured Logging Pattern
// In Lambda code - log JSON for CloudWatch Insights queries
console.log(JSON.stringify({
level: 'INFO',
message: 'Order processed',
orderId: '123',
userId: 'user-456',
duration: 150,
}));
// Query in CloudWatch Logs Insights:
// fields @timestamp, orderId, duration
// | filter level = 'ERROR'
// | sort @timestamp descBest Practices
1. Set log retention on all log groups (avoid infinite growth) 2. Use structured JSON logs for queryability 3. Enable X-Ray for distributed tracing across services 4. Create dashboards per application/service boundary 5. Set alarms on business metrics, not just technical 6. Use anomaly detection for unpredictable patterns 7. Tag all resources for cost attribution
Embedded Metric Format (EMF)
EMF allows high-cardinality custom metrics via logs:
console.log(JSON.stringify({
_aws: {
Timestamp: Date.now(),
CloudWatchMetrics: [{
Namespace: 'MyApp/Orders',
Dimensions: [['Service', 'Status']],
Metrics: [{ Name: 'OrderCount', Unit: 'Count' }],
}],
},
Service: 'OrderProcessor',
Status: 'Success',
OrderCount: 1,
}));Log Subscriptions
Stream logs to centralized systems:
new logs.SubscriptionFilter(this, 'LogsToFirehose', {
logGroup,
destination: new destinations.KinesisFirehoseDestination(firehose),
filterPattern: logs.FilterPattern.allEvents(),
});AWS Service Patterns with CDK
Contents
---
Lambda
Basic Function
const fn = new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_18_X,
architecture: lambda.Architecture.ARM_64, // 20% cheaper
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
memorySize: 1024,
timeout: Duration.seconds(30),
environment: { STAGE: 'prod' },
tracing: lambda.Tracing.ACTIVE, // X-Ray
});Event Sources
// SQS trigger
fn.addEventSource(new SqsEventSource(queue, { batchSize: 10 }));
// DynamoDB Streams
fn.addEventSource(new DynamoEventSource(table, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
batchSize: 100,
bisectBatchOnError: true,
}));
// S3 trigger
bucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(fn),
{ prefix: 'uploads/' }
);Cold Start Mitigation
const alias = new lambda.Alias(this, 'Alias', {
aliasName: 'live',
version: fn.currentVersion,
});
alias.addAutoScaling({ minCapacity: 1, maxCapacity: 10 })
.scaleOnUtilization({ utilizationTarget: 0.7 });Best Practices
- Use ARM64 for cost savings (compatible with most runtimes)
- Set appropriate
timeout(default 3s often too low) - Configure
reservedConcurrentExecutionsto protect downstream - Use Secrets Manager for sensitive config (not env vars)
- Set
logRetentionexplicitly to avoid infinite log growth - Consider layers for shared code and dependencies
NodejsFunction (Bundled TypeScript)
const fn = new nodejs.NodejsFunction(this, 'Fn', {
entry: 'lambda/handler.ts',
runtime: lambda.Runtime.NODEJS_20_X,
bundling: { minify: true },
});Container Image Lambda
new lambda.DockerImageFunction(this, 'ImageFn', {
code: lambda.DockerImageCode.fromImageAsset('lambda-image'),
});Reserved Concurrency
const fn = new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
reservedConcurrentExecutions: 10,
});---
DynamoDB
Table with GSI
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
});
table.addGlobalSecondaryIndex({
indexName: 'gsi1',
partitionKey: { name: 'gsi1pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'gsi1sk', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});Auto Scaling (Provisioned)
const scaling = table.autoScaleReadCapacity({ minCapacity: 5, maxCapacity: 100 });
scaling.scaleOnUtilization({ targetUtilizationPercent: 75 });Best Practices
- Enable PITR (
pointInTimeRecovery: true) for production - Use PAY_PER_REQUEST for variable workloads
- Enable streams for CDC patterns
- Design partition keys for even distribution
- Use TTL for expiring data
- Enable
contributorInsightsEnabledto detect hot partitions - Enable deletion protection for production tables
Global Tables (Multi-Region)
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
replicationRegions: ['us-east-1', 'eu-west-1'],
});TTL and Contributor Insights
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
timeToLiveAttribute: 'expiresAt',
contributorInsightsEnabled: true,
});---
S3
Secure Bucket
const bucket = new s3.Bucket(this, 'Bucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
versioned: true,
lifecycleRules: [{
expiration: Duration.days(365),
transitions: [{
storageClass: s3.StorageClass.INTELLIGENT_TIERING,
transitionAfter: Duration.days(30),
}],
}],
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true, // Dev only
});Additional S3 Guidance
- Use bucket keys with KMS to reduce SSE-KMS cost
- Prefer object ownership = bucket owner enforced
- Keep access logs in a separate, retained log bucket
- Avoid ACLs; keep
blockPublicAccessenabled - For high-scale notifications, route via SQS or EventBridge
S3 Deployment and Invalidation
new s3deploy.BucketDeployment(this, 'Deploy', {
sources: [s3deploy.Source.asset('./site')],
destinationBucket: bucket,
distribution,
distributionPaths: ['/*'],
prune: true,
});CORS Configuration
bucket.addCorsRule({
allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.PUT],
allowedOrigins: ['https://example.com'],
allowedHeaders: ['*'],
});Monitoring
const alarm = new cloudwatch.Alarm(this, 'S3Errors', {
metric: bucket.metric5xxErrors({ period: Duration.minutes(5) }),
threshold: 1,
evaluationPeriods: 1,
});S3 to EventBridge
const bucket = new s3.Bucket(this, 'Bucket', {
eventBridgeEnabled: true,
});CloudFront + S3 + WAF
const distribution = new cloudfront.Distribution(this, 'Dist', {
defaultBehavior: { origin: new origins.S3Origin(bucket) },
webAclId: webAcl.attrArn,
});---
ECS/Fargate
ALB Fargate Service (High-Level)
const service = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'Svc', {
cluster,
taskImageOptions: {
image: ecs.ContainerImage.fromEcrRepository(repo),
environment: { NODE_ENV: 'production' },
secrets: { DB_PASS: ecs.Secret.fromSecretsManager(secret) },
},
memoryLimitMiB: 1024,
cpu: 512,
desiredCount: 2,
publicLoadBalancer: true,
});Manual Task Definition
const taskDef = new ecs.FargateTaskDefinition(this, 'Task', {
memoryLimitMiB: 1024,
cpu: 512,
});
const container = taskDef.addContainer('App', {
image: ecs.ContainerImage.fromRegistry('nginx'),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'app',
logGroup: new logs.LogGroup(this, 'Logs', {
retention: logs.RetentionDays.ONE_WEEK,
}),
}),
});
container.addPortMappings({ containerPort: 80 });Auto Scaling
const scaling = service.service.autoScaleTaskCount({ minCapacity: 2, maxCapacity: 10 });
scaling.scaleOnCpuUtilization('CpuScaling', { targetUtilizationPercent: 50 });Networking
- Fargate uses
awsvpcmode (each task gets ENI) - Create separate SGs for ALB and service
- ALB SG allows 80/443 from internet
- Service SG allows container port from ALB SG only
---
Aurora
Serverless v2 Cluster
const cluster = new rds.DatabaseCluster(this, 'Aurora', {
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_15_4,
}),
serverlessV2MinCapacity: 0.5,
serverlessV2MaxCapacity: 16,
writer: rds.ClusterInstance.serverlessV2('writer'),
readers: [rds.ClusterInstance.serverlessV2('reader', { scaleWithWriter: true })],
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
credentials: rds.Credentials.fromGeneratedSecret('admin'),
backup: { retention: Duration.days(7) },
});Best Practices
- Use Secrets Manager for credentials (
fromGeneratedSecret) - Enable Performance Insights for query analysis
- Place in private subnets, allow only from app SGs
- Enable deletion protection for production
- Prefer
DatabaseClusterover deprecated serverless constructs - Set backup retention and maintenance windows explicitly
Credential Rotation
cluster.addRotationSingleUser({
automaticallyAfter: Duration.days(30),
});---
MSK (Kafka)
Cluster Setup
const cluster = new msk.Cluster(this, 'Kafka', {
clusterName: 'my-cluster',
kafkaVersion: msk.KafkaVersion.V3_4_0,
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
numberOfBrokerNodes: 3,
instanceType: new ec2.InstanceType('kafka.m5.large'),
encryptionInTransit: { clientBroker: msk.ClientBrokerEncryption.TLS },
clientAuthentication: msk.ClientAuthentication.sasl({ iam: true }),
});Lambda Consumer
new lambda.EventSourceMapping(this, 'MskSource', {
eventSourceArn: cluster.clusterArn,
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
kafkaTopic: 'my-topic',
batchSize: 100,
target: consumerFn,
});Best Practices
- MSK provisioning takes 15+ minutes (don't interrupt)
- Enable TLS for client-broker encryption
- Use IAM auth for serverless consumers
- Monitor broker storage (auto-expand not automatic)
- Enable broker logs in CloudWatch for diagnostics
- Keep brokers in private subnets with controlled egress
MSK Serverless (L1/L2 Alpha)
new msk.CfnServerlessCluster(this, 'Serverless', {
clientAuthentication: { sasl: { iam: { enabled: true } } },
vpcConfigs: [{ subnetIds: ['subnet-1', 'subnet-2'] }],
});MSK Authentication Modes
- IAM (recommended for AWS-native integrations)
- mTLS for legacy or cross-cloud clients
- SCRAM for username/password workflows
---
EventBridge
Event Rule
const rule = new events.Rule(this, 'Rule', {
eventPattern: {
source: ['myapp.orders'],
detailType: ['OrderCreated'],
detail: { priority: [{ numeric: ['>=', 5] }] },
},
targets: [new targets.LambdaFunction(processFn)],
});Scheduled Rule
new events.Rule(this, 'ScheduleRule', {
schedule: events.Schedule.rate(Duration.hours(1)),
targets: [new targets.LambdaFunction(cleanupFn)],
});Cross-Account
// Send to another account's bus
const targetBus = events.EventBus.fromEventBusArn(
this, 'TargetBus',
'arn:aws:events:us-east-1:OTHER_ACCOUNT:event-bus/default'
);
new events.Rule(this, 'CrossAccountRule', {
eventPattern: { source: ['myapp'] },
targets: [new targets.EventBus(targetBus)],
});EventBridge Pipes
new pipes.Pipe(this, 'Pipe', {
source: new SqsSource(queue),
target: new EventBridgeTarget(bus),
filter: new pipes.Filter([
pipes.FilterPattern.fromObject({ body: { type: ['ORDER'] } }),
]),
});Best Practices
- Use Pipes for filter/enrich/route without Lambda glue
- Set DLQs and retry policies on targets when supported
- Version event schemas via
detailTypechanges
Custom Event Bus
const bus = new events.EventBus(this, 'Bus', {
eventBusName: 'orders-bus',
});---
API Gateway
REST API with Lambda
const api = new apigateway.RestApi(this, 'Api', {
restApiName: 'MyAPI',
deployOptions: { stageName: 'prod' },
});
api.root.addResource('items').addMethod(
'GET',
new apigateway.LambdaIntegration(listFn)
);HTTP API (Lighter Weight)
const httpApi = new apigwv2.HttpApi(this, 'HttpApi');
httpApi.addRoutes({
path: '/items',
methods: [apigwv2.HttpMethod.GET],
integration: new HttpLambdaIntegration('ListIntegration', listFn),
});Best Practices
- Use HTTP API for simple Lambda proxies (cheaper, faster)
- Use REST API for transformations, request validation, usage plans
- Enable CloudWatch logging for debugging
- Use custom domain with ACM certificate
API Gateway to SQS (Storage First)
const queue = new sqs.Queue(this, 'Queue');
api.root.addMethod('POST', new apigateway.AwsIntegration({
service: 'sqs',
path: `${cdk.Aws.ACCOUNT_ID}/${queue.queueName}`,
integrationHttpMethod: 'POST',
options: { requestTemplates: { 'application/json': 'Action=SendMessage&MessageBody=$input.body' } },
}));---
ECS/Fargate (Operational Add-ons)
Circuit Breaker
new ecs.FargateService(this, 'Service', {
cluster,
taskDefinition,
circuitBreaker: { rollback: true },
});Guardrails
- Enable Container Insights on the cluster
- Use separate SGs for ALB and tasks
- Set autoscaling on CPU and memory
Service Connect
service.enableServiceConnect({
services: [{ portMappingName: 'app', discoveryName: 'api' }],
});Task Role vs Execution Role
- Execution role pulls images and writes logs
- Task role grants application access to AWS services
Testing and Security for CDK
Contents
- Unit Tests with Assertions
- Snapshot Tests for Refactors
- Integration Tests
- Security Checks with cdk-nag
- CI Integration
- Testing Pyramid
---
Unit Tests with Assertions
Use aws-cdk-lib/assertions for fine-grained checks:
import { Template } from 'aws-cdk-lib/assertions';
const app = new cdk.App();
const stack = new MyStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::S3::Bucket', {
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256' },
}],
},
});Snapshot Tests for Refactors
Snapshot tests help detect unintended template changes during refactors:
const template = JSON.stringify(
app.synth().getStackByName('SnapshotStack').template,
null,
2
);Use snapshots for refactor safety, not as the only regression check.
Integration Tests
Use @aws-cdk/integ-tests-alpha to validate deployed behavior:
const integ = new IntegTest(app, 'IntegTest', { testCases: [stack] });Integration tests are slower and incur real AWS costs. Use them sparingly.
Security Checks with cdk-nag
Run compliance checks with cdk-nag:
import { AwsSolutionsChecks } from 'cdk-nag';
import { Aspects } from 'aws-cdk-lib';
Aspects.of(app).add(new AwsSolutionsChecks({ reports: true }));Use suppressions sparingly and document reasons.
Aspects can also enforce org-wide tagging or policy rules across the construct tree.
CI Integration
Fail builds on violations:
if (report.violations?.length) {
process.exit(1);
}Testing Pyramid
Balance coverage:
- Unit assertions for specific properties
- Snapshot tests to guard refactors
- Integration tests for critical end-to-end paths
Troubleshooting AWS CDK
Contents
- Version and Bootstrap Errors
- Deployment Failures
- Resource Limits and Quotas
- Deletion and Update Issues
- Context and Sync Problems
- Cross-Stack and Dependency Errors
- Custom Resource Failures
- Logical ID and Refactor Issues
---
Version and Bootstrap Errors
"CDK CLI is not compatible with the CDK library"
Cause: Global CDK CLI older than project libraries. Fix:
npm install -g aws-cdk@latest
cdk --version # Verify upgrade"NoSuchBucket" on deploy
Cause: CDK bootstrap not run for account/region. Fix:
cdk bootstrap aws://ACCOUNT_ID/REGION"/cdk-bootstrap/hnb659fds/version not found"
Cause: Target account/region not bootstrapped with the modern CDK v2 template. Fix:
cdk bootstrap aws://ACCOUNT_ID/REGIONNote: Use --qualifier only if the environment is intentionally customized.
"Forbidden: null" on deploy
Cause: Credentials lack permission to CDK bootstrap bucket. Fix: Ensure deploying role has access to bootstrap resources (S3 bucket, ECR repo). Re-bootstrap with correct trust policy if needed.
"--app is required"
Cause: CDK cannot find application entry point. Fix:
- Run from project root (where
cdk.jsonexists) - Verify
cdk.jsonhas correctappfield:"app": "npx ts-node bin/app.ts"
---
Deployment Failures
"Resource already exists"
Cause: Resource with same name exists (often S3 bucket from previous stack). Fix:
- Delete orphaned resource manually, or
- Change resource name in CDK code, or
- Import existing resource with
cdk import
IAM permission denied during resource creation
Cause: Deploy role lacks permissions for specific resource. Fix:
- Add required IAM policies to deploy role
- For CDK Pipelines: check pipeline's CloudFormation execution role
CloudFormation rollback
Cause: Resource creation failed partway through. Fix:
- Check CloudFormation Events for specific failure reason
- Fix issue in CDK code
- Delete stack if in
ROLLBACK_COMPLETEstate, then redeploy
"Internal Failure"
Cause: Custom resource provider failed during deployment. Fix:
- Locate the provider Lambda logs (often
*Provider*) - Fix permissions or network access (NAT/VPC endpoints)
"Unable to assume role"
Cause: Cross-account role trust or execution role misconfigured. Fix:
- Verify trust policy on target account roles
- Check CDK bootstrap roles exist:
cdk-hnb659fds-deploy-role-*
---
Resource Limits and Quotas
"CloudFormation resource limit exceeded" (500 resources)
Cause: Stack exceeds 500 resource limit. Fix:
// Option 1: Split into multiple stacks
const dbStack = new DatabaseStack(app, 'DB');
const apiStack = new ApiStack(app, 'API', { db: dbStack.table });
// Option 2: Use nested stacks
import { NestedStack } from 'aws-cdk-lib';
class MyNestedStack extends NestedStack { ... }"Specified 3 AZs but only 2 were used"
Cause: Environment-agnostic synthesis doesn't know actual AZs. Fix:
new MyStack(app, 'Stack', {
env: { account: '123456789012', region: 'us-east-1' }
});---
Deletion and Update Issues
Resource not deleted on cdk destroy
Cause: RemovalPolicy.RETAIN (default for stateful resources). Fix:
// For dev/test only
new s3.Bucket(this, 'Bucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true, // For S3 specifically
});
new dynamodb.Table(this, 'Table', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});Note: Manually delete retained resources that block redeployment.
Update causes resource replacement
Cause: Changed immutable property (Lambda function name, RDS identifier). Fix:
- Let CDK-generated names be used (avoid explicit naming)
- Use
cdk diffbefore deploy to spot replacements ([~]vs[-] [+])
Drift detected / out-of-band changes
Cause: Manual changes in AWS console. Fix:
- Re-sync by importing changes into CDK code
- Use CloudFormation drift detection to identify differences
- Avoid manual changes to CDK-managed resources
Resource replacement surprises
Cause: Immutable property changed (name, identifier, or encryption settings). Fix:
- Review
cdk difffor replacements - Avoid explicit names for stateful resources
- Use snapshots or migration steps when replacements are required
---
Context and Sync Problems
Stale context values (VPC, AMI)
Cause: Cached lookup values in cdk.context.json. Fix:
cdk context # List cached values
cdk context --reset KEY # Clear specific entry
cdk synth # Re-fetch on next synthContext mismatch after environment changes
Cause: Underlying resources changed but cached context still used. Fix:
cdk context --clearNon-deterministic synthesis
Cause: Using Date.now(), random values, or environment-dependent values in resource definitions. Fix: Use fixed identifiers or derive from stack parameters.
Cross-stack reference failure
Cause: Dependent stack deleted or output changed. Fix:
- Use
exportValue()for explicit exports - Prefer passing constructs directly between stacks in same app
- Avoid circular dependencies
Cross-Stack and Dependency Errors
"Export EXPORT_NAME cannot be updated as it is in use by STACK_NAME"
Cause: Stack B imports an output from Stack A that is being removed or changed. Fix:
- Remove the import in Stack B (temporary value if needed)
- Deploy Stack B
- Remove the export in Stack A
- Deploy Stack A
"Circular dependency found"
Cause: Two constructs reference each other implicitly (often via grants or triggers). Fix:
- Break the cycle with explicit roles or permissions
- Use lazy values to defer resolution at synth time
Custom Resource Failures
Deployment hangs then fails after ~1 hour
Cause: Custom resource Lambda cannot reach AWS APIs (private subnets, no NAT, missing endpoints). Fix:
- Check
AWSCDK-CustomResource-*Lambda logs - Add NAT Gateway or VPC endpoints for required services
Need to preserve failed resources for debugging
Cause: Default rollback deletes evidence. Fix:
cdk deploy --no-rollbackLogical ID and Refactor Issues
Unintended resource replacement after refactor
Cause: Construct path changes alter logical IDs. Fix:
- Use
cdk refactorto move or rename constructs safely - Use explicit physical names when required for stability