Serverless Framework vs AWS CDK: Should You Migrate? (Part 1)
Why migrate from Serverless Framework to AWS CDK: licensing changes, architectural advantages, and when CDK becomes the better choice for your apps.
Serverless Framework and AWS CDK solve overlapping problems with different philosophies. Serverless Framework is a YAML-driven, provider-neutral abstraction over a Lambda-centric deployment unit; CDK is a typed, AWS-native synthesis layer over CloudFormation. When Serverless Framework introduced paid licensing, staying put stopped being the cost-free default, and the comparison became worth making again.
For a team already committed to AWS and running more than a handful of functions, CDK is the better default: typed cross-stack references, native constructs for services that Serverless Framework reaches through plugins, and infrastructure you can unit test. The price is deeper AWS lock-in and a TypeScript learning curve. A small, stable Lambda-plus-API-Gateway application, where licensing cost is the only complaint, rarely earns that price back.
This six-part series covers the complete migration process:
- Part 1: Why migrate? Understanding the trade-offs (this post)
- Part 2: Setting up your CDK environment and project structure
- Part 3: Migrating Lambda functions and API Gateway
- Part 4: Database resources and environment management
- Part 5: Authentication, authorization, and IAM
- Part 6: Migration strategies and best practices
Licensing Cost Versus Operational Cost#
Licensing fees are the visible cost. The operational costs underneath them usually weigh more in the decision. Both layers push teams toward CDK:
Direct Cost Considerations#
Serverless Framework’s Pro tier prices per deployment, and that cost scales with team growth as more features move behind paid plans. CDK carries no separate licensing fee: it ships as part of the AWS CLI, treats infrastructure as standard application code, and covers AWS services natively.
Hidden Operational Costs#
Both tools carry ongoing costs that never appear on an invoice. YAML configuration syntax gets harder to maintain as it grows, third-party plugins bring their own compatibility problems, cross-service references rely on strings vs. typed objects, and debugging happens at runtime vs. compile time.
Compile-Time Safety in Practice#
These differences show up in day-to-day work and shape the decision:
Configuration Errors and Plugin Overhead#
YAML lets configuration typos reach production. TypeScript stops them at compile time. Serverless Framework reads the webhook secret straight from the environment:
# serverless.yml
provider:
environment:
STRIPE_API_KEY: ${env:STRIPE_API_KEY}
STRIPE_WEBHOOK_SECRET: ${env:STRIPE_WEBHOOK_SECRET}
CDK reads STRIPE_WEBHOOK_SECRET in application code and throws before deploy if it’s missing:
// Environment variables are validated at compile time
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
throw new Error('STRIPE_WEBHOOK_SECRET environment variable required');
}
Advanced features push Serverless Framework toward community plugins, and each plugin adds its own compatibility surface to track through Node.js upgrades:
plugins:
- serverless-webpack
- serverless-offline
- serverless-step-functions
custom:
webpack:
webpackConfig: webpack.config.js
CDK needs none of that:
// Native bundling and service integration
const bundling = {
target: 'node20',
minify: true,
sourceMap: true,
};
Cross-Stack Type Safety#
CDK replaces CloudFormation exports with type-safe object references. Dependencies stay explicit and type-checked, so refactors are safer:
// Direct object references with compile-time validation
const authStack = new AuthStack(this, 'AuthStack', {
userTable: databaseStack.userTable, // TypeScript ensures this exists
});
In Serverless Framework, CloudFormation exports and string interpolation wire the dependency:
# auth-service/serverless.yml
provider:
environment:
USER_TABLE_ARN: ${cf:database-stack-${opt:stage}.UserTableArn}
Scaling Beyond a Single Function#
The gap between the two approaches widens once an application grows past a single endpoint. A createUser route shows the shift in practice. Serverless Framework declares it in YAML:
# serverless.yml
provider:
name: aws
runtime: nodejs20.x
environment:
TABLE_NAME: ${self:service}-${opt:stage}-users
functions:
createUser:
handler: src/handlers/users.create
events:
- http:
path: users
method: post
cors: true
CDK connects the function and its route directly in code:
// lib/api-stack.ts
import { RestApi, LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
const createUserFn = new NodejsFunction(this, 'CreateUserFunction', {
entry: 'src/handlers/users.ts',
handler: 'create',
environment: {
TABLE_NAME: userTable.tableName,
},
});
// Type-safe integration
const api = new RestApi(this, 'UserApi');
api.root.addResource('users').addMethod('POST',
new LambdaIntegration(createUserFn)
);
Step Functions and AppSync Without Plugins#
CDK reaches Step Functions, AppSync, and CloudWatch alarms through its own constructs, without an extra dependency:
import { DefinitionBody, StateMachine } from 'aws-cdk-lib/aws-stepfunctions';
import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks';
import { Definition, GraphqlApi } from 'aws-cdk-lib/aws-appsync';
import { Alarm } from 'aws-cdk-lib/aws-cloudwatch';
// Direct service integration without plugins
const workflow = new StateMachine(this, 'UserWorkflow', {
definitionBody: DefinitionBody.fromChainable(
new LambdaInvoke(this, 'ProcessUser', {
lambdaFunction: processUserFn,
})
),
});
const api = new GraphqlApi(this, 'UserGraphQL', {
name: 'user-api',
definition: Definition.fromFile('schema.graphql'),
});
new Alarm(this, 'ProcessUserErrors', {
metric: processUserFn.metricErrors(),
threshold: 1,
evaluationPeriods: 1,
});
Serverless Framework needs a separate plugin for each of the three:
plugins:
- serverless-step-functions
- serverless-appsync-plugin
- serverless-plugin-aws-alerts
custom:
alerts:
stages:
- production
topics:
alarm:
topic: ${self:service}-${opt:stage}-alerts
A Reusable API Construct#
Wrapping the API construct in a reusable class brings true object-oriented infrastructure to CDK:
// lib/constructs/serverless-api.ts
export class ServerlessApi extends Construct {
public readonly api: RestApi;
public readonly functions: Map<string, NodejsFunction>;
constructor(scope: Construct, id: string, props: ServerlessApiProps) {
super(scope, id);
// Encapsulated, reusable infrastructure patterns
this.api = new RestApi(this, 'Api', {
restApiName: props.apiName,
deployOptions: this.createDeployOptions(props.stage),
});
this.functions = this.createFunctions(props.routes);
this.setupRoutes(props.routes);
this.setupAlarms(props.monitoring);
}
}
// Usage across multiple stacks
new ServerlessApi(this, 'UserApi', {
apiName: 'users',
routes: userRoutes,
monitoring: productionMonitoring,
});
Serverless Framework approximates this with includes and shared variable files:
# serverless.yml
custom:
userTableConfig: ${file(./config/tables.yml):userTable}
resources:
Resources:
UserTable: ${self:custom.userTableConfig}
Unit-Testing the Stack#
Testing Serverless Framework infrastructure typically means:
- Mocking framework behavior
- Testing deployed resources
- Limited unit testing options
Against the synthesized template itself, CDK infrastructure tests run directly:
// test/api-stack.test.ts
import { Match, Template } from 'aws-cdk-lib/assertions';
const template = Template.fromStack(stack);
test('API Gateway has CORS enabled', () => {
template.hasResourceProperties('AWS::ApiGateway::Method', {
Integration: {
IntegrationResponses: [{
ResponseParameters: {
'method.response.header.Access-Control-Allow-Origin': "'*'",
},
}],
},
});
});
test('Lambda has correct environment variables', () => {
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Variables: {
TABLE_NAME: { Ref: Match.anyValue() },
STAGE: 'production',
},
},
});
});
When Each Tool Excels#
Where CDK Pulls Ahead#
CDK pulls ahead once the AWS surface gets more complex: Step Functions, EventBridge, and AppSync integrations; infrastructure patterns that need to stay reusable across teams; fine-grained control over custom CloudFormation resources; TypeScript typing throughout the stack; unit and integration tests for the infrastructure itself; and explicit dependencies once more than one team touches the code.
Serverless Framework still wins in a few cases:
- A simple Lambda-plus-API-Gateway CRUD API
- A team already deep in its plugin ecosystem
- Developers who prefer YAML over TypeScript
- Fast prototypes
- Applications too small for infrastructure complexity to matter
Scoping the Migration#
Before migrating, size the work against your current setup. Five signals drive most of the effort:
| Signal | Low effort | High effort |
|---|---|---|
| Function count | Tens of functions in one service | Hundreds across several services |
| Custom resources | None, or plain CloudFormation | Custom resources and macros |
| Plugins | Only local-dev and bundler plugins | Plugins with no CDK equivalent |
| Environments | One or two stages | Per-developer and per-region stages |
| CI/CD | A single deploy pipeline | A pipeline coupled to Serverless Framework CLI output |
Technical Assessment#
- Number of Lambda functions and services
- Custom resources and CloudFormation usage
- Cross-service dependencies
- Plugin dependencies and maintenance overhead
Team and Rollout Readiness#
Skill matters as much as infrastructure size: current TypeScript experience level, familiarity with infrastructure as code, how much learning time is actually available, and comfort with programmatic infrastructure over declarative YAML. The rollout plan is a separate decision: a gradual migration or a full cutover, rollback procedures and testing, parallel infrastructure during the transition, and time budgeted for team training and knowledge transfer.
What’s Next#
Once you’ve decided to migrate, the real work begins: setting up a CDK project structure that supports safe, gradual migration.
Part 2 covers those setup steps: project architecture patterns, development workflows, and the environment configuration that keeps the transition manageable.
References#
- What is the AWS CDK? - AWS CDK v2 (opens in new tab) - Introduction to CDK’s programmatic, type-safe approach to defining AWS infrastructure
- Serverless Framework Documentation (opens in new tab) - Official Serverless Framework reference covering YAML configuration, providers, and plugins
- AWS CDK Constructs (opens in new tab) - Explanation of the L1/L2/L3 construct hierarchy that replaces Serverless Framework resource definitions
- Building Lambda functions with TypeScript - AWS Lambda (opens in new tab) - How Lambda runs TypeScript, relevant to evaluating CDK’s NodejsFunction construct
- Serverless Applications Lens - AWS Well-Architected Framework (opens in new tab) - Architectural guidance that informs migration decisions for serverless workloads
- Getting started with the AWS CDK (opens in new tab) - Prerequisites, installation, and first CDK app for teams evaluating the migration
Migrating from Serverless Framework to AWS CDK
A comprehensive 6-part guide covering the complete migration process from Serverless Framework to AWS CDK, including setup, implementation patterns, and best practices.
All posts in this series
Related posts
Migrate Lambda functions, API Gateway, request validation, and error handling from Serverless Framework to AWS CDK with practical examples.
api-gateway · aws · aws-cdk +2
Master DynamoDB migrations, environment variable management, secrets handling, and VPC configurations when moving from Serverless Framework to AWS CDK.
aws · aws-cdk · dynamodb +4
A Cognito passkey migration guide for SaaS teams on email, password, and SMS OTP: configuration, enrollment, and the four-tier recovery ladder that replaces SMS.
authentication · security · aws +2
Before building an internal service layer, decide whether you need one: what it costs per call, the volume where VPC Lattice wins, and when direct invoke still beats it.
aws · aws-cdk · lambda +4
A private REST API structurally cannot carry gRPC, and every AWS surface that speaks gRPC excludes Lambda targets. What to keep from gRPC, and what to drop.
aws · aws-cdk · lambda +4