Skip to content
Ayhan Sipahi Ayhan Sipahi

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 rarely earns that price back, and licensing cost on its own is a weak reason to move.

This six-part series covers the complete migration process:

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 licensing (for teams using Pro features):

  • Per-deployment pricing model
  • Scaling costs with team growth
  • Additional features behind paid tiers

CDK approach:

  • No licensing fees (part of AWS CLI)
  • Infrastructure as standard application code
  • Native AWS service support

Hidden Operational Costs

Both tools carry ongoing costs that never appear on an invoice:

  • YAML maintenance: Configuration syntax can become complex
  • Plugin dependencies: Third-party plugin compatibility issues
  • Cross-service references: String-based references vs. typed objects
  • Debugging: Runtime vs. compile-time error detection

Key Technical Differences

Three differences show up in day-to-day work and shape the decision:

1. Configuration vs. Code

Serverless Framework approach (YAML configuration):

# serverless.yml
provider:
  environment:
    STRIPE_API_KEY: ${env:STRIPE_API_KEY}
    STRIPE_WEBHOOK_SECRET: ${env:STRIPE_WEBHOOK_SECRET}

CDK approach (TypeScript code):

// 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');
}

Key insight: Configuration typos can reach production with YAML, while TypeScript catches issues at compile time.

2. Plugin Dependencies vs. Native Integration

Serverless Framework relies on community plugins for advanced features:

plugins:
  - serverless-webpack
  - serverless-offline
  - serverless-step-functions

custom:
  webpack:
    webpackConfig: webpack.config.js

CDK provides native constructs for AWS services:

// Native bundling and service integration
const bundling = {
  target: 'node20',
  minify: true,
  sourceMap: true,
};

Learning: Plugin compatibility can become a maintenance burden during Node.js upgrades.

3. Cross-Stack References

Serverless Framework uses CloudFormation exports and string interpolation:

# auth-service/serverless.yml
provider:
  environment:
    USER_TABLE_ARN: ${cf:database-stack-${opt:stage}.UserTableArn}

CDK enables type-safe object references:

// Direct object references with compile-time validation
const authStack = new AuthStack(this, 'AuthStack', {
  userTable: databaseStack.userTable, // TypeScript ensures this exists
});

Benefit: Refactoring becomes safer when dependencies are explicit and type-checked.

The TypeScript Infrastructure Advantage

Moving from YAML configuration to TypeScript code brings several advantages:

Serverless Framework (YAML configuration):

# 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 (TypeScript 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)
);

Benefits include:

  • Compile-time error detection
  • IDE autocompletion
  • Refactoring support
  • Type-safe environment variables

Native AWS Service Integration

Serverless Framework requires plugins for advanced AWS services:

plugins:
  - serverless-step-functions
  - serverless-appsync-plugin
  - serverless-plugin-aws-alerts

custom:
  alerts:
    stages:
      - production
    topics:
      alarm:
        topic: ${self:service}-${opt:stage}-alerts

CDK provides native constructs for all AWS services:

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,
});

Infrastructure Composition and Reusability

Serverless Framework uses includes and variables:

# serverless.yml
custom:
  userTableConfig: ${file(./config/tables.yml):userTable}

resources:
  Resources:
    UserTable: ${self:custom.userTableConfig}

CDK enables true object-oriented infrastructure:

// 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,
});

Testing Infrastructure

Serverless Framework testing typically involves:

  • Mocking framework behavior
  • Testing deployed resources
  • Limited unit testing options

CDK enables comprehensive infrastructure testing:

// 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

Choose CDK When You Need:

  1. Complex AWS service integration - Step Functions, EventBridge, AppSync
  2. Shared infrastructure patterns - Reusable constructs across teams
  3. Fine-grained control - Custom CloudFormation resources
  4. Strong typing - TypeScript throughout your stack
  5. Infrastructure testing - Unit and integration tests for IaC
  6. Large team coordination - Explicit dependencies and interfaces

Stay with Serverless Framework When:

  1. Simple Lambda + API Gateway - Basic CRUD APIs
  2. Existing plugin ecosystem - Heavy reliance on community plugins
  3. Team YAML preference - Developers uncomfortable with TypeScript
  4. Quick prototypes - Rapid proof-of-concepts
  5. Small applications - Minimal infrastructure complexity

Migration Complexity Assessment

Before migrating, size the work against your current setup. Five signals drive most of the effort:

SignalLow effortHigh effort
Function countTens of functions in one serviceHundreds across several services
Custom resourcesNone, or plain CloudFormationCustom resources and macros
PluginsOnly local-dev and bundler pluginsPlugins with no CDK equivalent
EnvironmentsOne or two stagesPer-developer and per-region stages
CI/CDA single deploy pipelineA pipeline coupled to Serverless Framework CLI output

Plugins without a CDK equivalent are the item most likely to stretch a schedule: replacing one means writing the construct yourself.

Migration Decision Framework

Based on experience with both tools, here’s a practical framework for evaluating migration:

Technical Assessment

Current infrastructure complexity:

  • Number of Lambda functions and services
  • Custom resources and CloudFormation usage
  • Cross-service dependencies
  • Plugin dependencies and maintenance overhead

Team Readiness

Skill evaluation:

  • TypeScript experience level
  • Infrastructure as code familiarity
  • Available learning time
  • Comfort with programmatic infrastructure

Migration Planning

Risk mitigation strategies:

  • Gradual migration vs. full cutover
  • Rollback procedures and testing
  • Parallel infrastructure during transition
  • Team training and knowledge transfer

Expected Benefits

Realistic outcome expectations:

  • Improved developer experience with IDE support
  • Better error detection at compile time
  • Simplified cross-service references
  • Enhanced testing capabilities for infrastructure

Migration Readiness Checklist

Before starting a migration, consider these factors:

Technical Readiness

  • Team has TypeScript experience or learning time
  • Current infrastructure is well-documented
  • Plugin dependencies are understood and replaceable
  • Testing strategy exists for infrastructure changes

Organizational Readiness

  • Migration timeline aligns with business goals
  • Rollback procedures are defined
  • Knowledge transfer plan exists
  • Migration complexity is appropriate for team size

When NOT to Migrate

Stick with Serverless Framework if:

  1. Limited TypeScript experience - The learning curve may impact delivery
  2. Simple, stable applications - Migration overhead may not be justified
  3. Heavy plugin dependencies - Ensure CDK alternatives exist
  4. Time constraints - Migration requires dedicated focus and time

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.

The technical migration is usually easier than the process around it. Coordinating team efforts, maintaining development velocity, and keeping production stable during the transition all need planning.

References

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.

Progress 1/6 posts completed

Related posts