AWS SAM vs CDK: Choosing the Right IaC Tool for Serverless in 2025
7 min read
"Which one should I use: SAM or CDK?"
I've been asked this question approximately 847 times (give or take). The answer? It depends. But not in the frustrating consultant way-in the "each tool solves different problems" way.
Let's settle this once and for all.
The Landscape
Both AWS SAM (Serverless Application Model) and CDK (Cloud Development Kit) help you define infrastructure as code. Both generate CloudFormation templates. Both are maintained by AWS. But they serve different masters.
AWS SAM: Declarative YAML for serverless applications AWS CDK: Programmatic TypeScript/Python/Java for any AWS infrastructure
Think SAM as SQL, CDK as Python. One is domain-specific and optimized. The other is general-purpose and flexible.
Side-by-Side Comparison
Hello World API: SAM vs CDK
SAM (template.yaml):
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: python3.12
Timeout: 30
Environment:
Variables:
TABLE_NAME: !Ref UsersTable
Resources:
HelloWorldApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Auth:
DefaultAuthorizer: MyCognitoAuthorizer
Authorizers:
MyCognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: hello-world
CodeUri: src/
Handler: app.lambda_handler
Events:
GetUsers:
Type: Api
Properties:
RestApiId: !Ref HelloWorldApi
Path: /users
Method: get
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UsersTable
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: my-app-users
AutoVerifiedAttributes:
- email
CDK (TypeScript):
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as cognito from 'aws-cdk-lib/aws-cognito';
export class HelloWorldStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// DynamoDB table
const usersTable = new dynamodb.Table(this, 'UsersTable', {
tableName: 'users',
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
// Cognito User Pool
const userPool = new cognito.UserPool(this, 'UserPool', {
userPoolName: 'my-app-users',
autoVerify: { email: true },
});
// Lambda function
const helloFunction = new lambda.Function(this, 'HelloWorldFunction', {
functionName: 'hello-world',
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'app.lambda_handler',
code: lambda.Code.fromAsset('src'),
timeout: cdk.Duration.seconds(30),
environment: {
TABLE_NAME: usersTable.tableName,
},
});
// Grant DynamoDB read permissions
usersTable.grantReadData(helloFunction);
// API Gateway with Cognito authorizer
const api = new apigateway.RestApi(this, 'HelloWorldApi', {
restApiName: 'hello-world-api',
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
},
});
const auth = new apigateway.CognitoUserPoolsAuthorizer(this, 'Authorizer', {
cognitoUserPools: [userPool],
});
const users = api.root.addResource('users');
users.addMethod('GET', new apigateway.LambdaIntegration(helloFunction), {
authorizer: auth,
authorizationType: apigateway.AuthorizationType.COGNITO,
});
// Outputs
new cdk.CfnOutput(this, 'ApiUrl', {
value: api.url,
description: 'API Gateway URL',
});
}
}
Line count: SAM = 51 lines, CDK = 56 lines. Nearly identical for simple cases.
Feature Comparison Matrix
| Feature | SAM | CDK | Winner |
| Learning Curve | Minimal (YAML) | Moderate (Programming) | SAM |
| Local Testing | sam local built-in | Requires SAM CLI | SAM |
| Type Safety | None | Full TypeScript support | CDK |
| Reusability | Limited (nested stacks) | High (constructs) | CDK |
| Multi-Service | Serverless-focused | All AWS services | CDK |
| IDE Support | Basic | Excellent autocomplete | CDK |
| Custom Logic | Difficult | Easy (native code) | CDK |
| Deployment Speed | Fast | Slower (synth step) | SAM |
| Debugging | Straightforward | Requires understanding | SAM |
| Community | Large, serverless-focused | Growing, polyglot | Tie |
When to Use SAM
✅ Perfect For:
1. Pure Serverless Applications
# SAM excels at this
Resources:
Function:
Type: AWS::Serverless::Function
Properties:
Events:
Api:
Type: Api
Schedule:
Type: Schedule
Properties:
Schedule: rate(5 minutes)
DynamoDB:
Type: DynamoDB
Properties:
Stream: !GetAtt Table.StreamArn
2. Quick Prototypes
sam init --runtime python3.12 --app-template hello-world
cd sam-app
sam build && sam deploy --guided
# Done in 5 minutes
3. Teams New to IaC YAML is familiar, readable, and doesn't require programming knowledge.
4. Local Development
sam local start-api # API Gateway locally
sam local invoke MyFunction # Test single function
sam local start-lambda # Lambda endpoint for testing
When to Use CDK
✅ Perfect For:
1. Complex Infrastructure
// Easy to build complex patterns
for (let i = 0; i < 10; i++) {
new lambda.Function(this, `Function${i}`, {
functionName: `processor-${i}`,
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
PARTITION: i.toString(),
TABLE_NAME: tables[i % 5].tableName, // Distribute across tables
},
});
}
2. Multi-Stack Applications
// Cross-stack references are trivial
export class NetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: cdk.App, id: string) {
super(scope, id);
this.vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 });
}
}
export class ApplicationStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, vpc: ec2.Vpc) {
super(scope, id);
const func = new lambda.Function(this, 'Function', {
vpc, // Reference from another stack
// ...
});
}
}
// App
const networkStack = new NetworkStack(app, 'Network');
new ApplicationStack(app, 'App', networkStack.vpc);
3. Custom Abstractions
// Build reusable constructs
export class SecureLambdaFunction extends cdk.Construct {
public readonly function: lambda.Function;
constructor(scope: cdk.Construct, id: string, props: SecureLambdaProps) {
super(scope, id);
// Apply security best practices automatically
this.function = new lambda.Function(this, 'Function', {
...props,
environment: {
...props.environment,
NODE_OPTIONS: '--enable-source-maps',
},
tracing: lambda.Tracing.ACTIVE, // Always enable X-Ray
logRetention: logs.RetentionDays.ONE_WEEK,
deadLetterQueueEnabled: true,
reservedConcurrentExecutions: props.maxConcurrency || 10,
});
// Add standard alarms
this.function.metricErrors().createAlarm(this, 'ErrorAlarm', {
threshold: 10,
evaluationPeriods: 1,
});
}
}
// Usage
new SecureLambdaFunction(this, 'MyFunc', {
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_18_X,
});
4. Testing Infrastructure
// test/stack.test.ts
import { Template } from 'aws-cdk-lib/assertions';
import { HelloWorldStack } from '../lib/stack';
test('Lambda has correct runtime', () => {
const app = new cdk.App();
const stack = new HelloWorldStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'python3.12',
Timeout: 30,
});
});
test('DynamoDB has billing mode PAY_PER_REQUEST', () => {
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::DynamoDB::Table', {
BillingMode: 'PAY_PER_REQUEST',
});
});
Hybrid Approach: Best of Both Worlds
You don't have to choose exclusively:
Use CDK with SAM CLI
// Deploy with CDK
cdk synth
cdk deploy
// But test locally with SAM
sam local start-api -t ./cdk.out/MyStack.template.json
Embed SAM Resources in CDK
import * as sam from 'aws-cdk-lib/aws-sam';
// Use SAM's shorthand where it helps
const samFunction = new sam.CfnFunction(this, 'SAMFunction', {
codeUri: 's3://mybucket/code.zip',
handler: 'index.handler',
runtime: 'python3.12',
events: {
Api: {
type: 'Api',
properties: {
path: '/hello',
method: 'get',
},
},
},
});
Migration Guide: SAM to CDK
Step 1: Analyze Current SAM Template
# Identify resources
aws cloudformation describe-stack-resources --stack-name my-sam-stack
Step 2: Create CDK App
mkdir my-cdk-app && cd my-cdk-app
cdk init app --language typescript
npm install
Step 3: Convert Resource by Resource
SAM Function:
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: app.handler
Events:
Api:
Type: Api
Properties:
Path: /users
Method: get
→ CDK Equivalent:
const api = new apigw.RestApi(this, 'Api');
const myFunction = new lambda.Function(this, 'MyFunction', {
code: lambda.Code.fromAsset('src'),
handler: 'app.handler',
runtime: lambda.Runtime.PYTHON_3_12,
});
api.root.addResource('users').addMethod('GET', new apigw.LambdaIntegration(myFunction));
Step 4: Deploy Side-by-Side
# Keep SAM stack running
sam deploy
# Deploy CDK stack with different name
cdk deploy MyApp-Migrated
# Test CDK stack
# Cut over traffic
# Delete SAM stack
Decision Framework
START
│
│─ Is it pure serverless? (Lambda, API Gateway, DynamoDB)
│ └─ YES → SAM
│
│─ Do you need multiple AWS services? (VPC, RDS, ECS, etc.)
│ └─ YES → CDK
│
│─ Do you need complex logic or loops?
│ └─ YES → CDK
│
│─ Is team new to infrastructure?
│ └─ YES → SAM
│
│─ Do you want type safety and IDE support?
│ └─ YES → CDK
│
│─ Do you need extensive local testing?
└─ YES → SAM (or CDK + SAM CLI)
Real-World Recommendations
Startup (0-10 engineers): Start with SAM. Simpler, faster iteration.
Scale-up (10-50 engineers): Migrate to CDK. Build reusable constructs.
Enterprise (50+ engineers): CDK with custom construct library. Enforce standards.
Solo developer: SAM for prototypes, CDK for production.
Performance & Costs
Both generate CloudFormation, so runtime performance is identical. Deployment differences:
| Metric | SAM | CDK |
| Initial deploy | ~2 min | ~3 min (synth + deploy) |
| Update deploy | ~1 min | ~2 min |
| Local test startup | <5 sec | <5 sec (with SAM CLI) |
| Build complexity | Low | Medium |
Conclusion
SAM: Faster to learn, perfect for pure serverless, excellent local testing.
CDK: More powerful, type-safe, reusable, better for complex architectures.
My recommendation? Start with SAM. When you hit its limitations (complex logic, multi-service, reusability needs), migrate to CDK.
Don't overthink it. Both are excellent tools. Pick one, build something, ship it.
What's your experience with SAM vs CDK? Share your migration stories!