March 2, 2025

Migrating from Serverless Framework to AWS CDK: Part 1 - Why Make the Switch?

Explore the motivations behind migrating from Serverless Framework to AWS CDK, including licensing changes, architectural advantages, and when CDK becomes the better choice for your serverless applications.

Last November, our CTO dropped a bombshell in the all-hands meeting: "We're spending

$2,400 per month on Serverless Framework licenses, and that's about to triple with our growth projections. We need to make a call."

I'd been using Serverless Framework for four years. It was comfortable, familiar, and our team of 12 engineers knew it inside-out. But staring at a projected $7,200/month bill for deployment tooling - more than we spent on our entire AWS infrastructure - I realized we had a problem.

This is the story of why we migrated our 47 Lambda functions, 12 DynamoDB tables, and $2.8M ARR platform from Serverless Framework to AWS CDK. Not a theoretical comparison, but the real decision-making process, financial analysis, and technical trade-offs we faced when our infrastructure tooling costs threatened to exceed our actual AWS spend.

This six-part series chronicles the complete 4-month migration journey:

The $2.4M Decision Framework#

When you're responsible for infrastructure serving $2.8M ARR, every decision needs ROI analysis. Here's the spreadsheet that convinced our board to approve the migration:

The Real Cost Analysis (November 2024)#

Current Serverless Framework costs:

  • 4 environments × 12 services = 48 deployments/day
  • 1,440 deployments/month at $2/credit = $2,880/month
  • Projected growth: 150% by Q2 2025 = $7,200/month

Hidden costs we discovered:

  • Plugin maintenance: 15 hours/month ($1,250 engineering time)
  • Debugging YAML issues: 8 hours/month ($800 engineering time)
  • Cross-team dependency blockers: 12 hours/month ($1,200 engineering time)
  • Total monthly cost: $12,450

CDK alternative:

  • AWS CDK: Free (part of AWS CLI)
  • Migration cost: $45,000 (3 months × 3 engineers × $5K/month)
  • Ongoing maintenance: 5 hours/month ($750 engineering time)
  • Break-even point: 4.2 months

The numbers were clear, but numbers don't tell the whole story. Let me share what really drove this decision.

The Three Production Incidents That Sealed the Deal#

While the financial case was compelling, three specific production incidents made the technical case for migration:

Incident #1: The YAML Typo That Cost $12K (September 2024)#

Our senior engineer deployed what looked like a routine configuration change:

YAML
# serverless.yml - The $12K typo
provider:
  environment:
    STRIPE_API_KEY: ${env:STRIPE_API_KEY}
    STRIPE_WEBHOOK_SECRET: ${env:STRIPE_WEBHOOK_SECRE}  # Missing 'T'

The typo caused our payment webhooks to fail silently for 6 hours. By the time we caught it, we'd lost $12,000 in failed subscription renewals and spent another 8 hours reconciling payment state.

The CDK equivalent would have caught this at compile time:

TypeScript
// This would fail to compile
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRE; // Error: undefined

,200 engineering time)

  • Cross-team dependency blockers: 12 hours/month ($1,800 engineering time)
  • Total monthly cost: $12,450

CDK alternative:

  • AWS CDK: Free (part of AWS CLI)
  • Migration cost: $45,000 (3 months × 3 engineers × $5K/month)
  • Ongoing maintenance: 5 hours/month ($750 engineering time)
  • Break-even point: 4.2 months

The numbers were clear, but numbers don't tell the whole story. Let me share what really drove this decision.

The Three Production Incidents That Sealed the Deal#

While the financial case was compelling, three specific production incidents made the technical case for migration:

Incident #1: The YAML Typo That Cost $12K (September 2024)#

Our senior engineer deployed what looked like a routine configuration change:

YAML
# serverless.yml - The $12K typo
provider:
  environment:
    STRIPE_API_KEY: ${env:STRIPE_API_KEY}
    STRIPE_WEBHOOK_SECRET: ${env:STRIPE_WEBHOOK_SECRE}  # Missing 'T'

The typo caused our payment webhooks to fail silently for 6 hours. By the time we caught it, we'd lost $12,000 in failed subscription renewals and spent another 8 hours reconciling payment state.

The CDK equivalent would have caught this at compile time:

TypeScript
// This would fail to compile
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRE; // Error: undefined

Incident #2: The Plugin Dependency Hell (October 2024)#

A simple Node.js version upgrade from 18 to 20 broke our deployment pipeline. The issue? Our serverless-webpack plugin wasn't compatible with the new Node version, but the error message was cryptic:

Bash
Serverless Error: Cannot read property 'compilation' of undefined

It took our team 16 hours across 3 days to track down the root cause. During this time, we couldn't deploy any fixes or features. Our velocity dropped to zero.

With CDK, we control the entire toolchain:

TypeScript
// No plugin dependencies, just standard Node.js
const bundling = {
  target: 'node20',
  minify: true,
  sourceMap: true,
};

Incident #3: The Cross-Stack Reference Nightmare (November 2024)#

Our authentication service needed to reference outputs from our database stack. The Serverless Framework approach required this brittle setup:

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

When we renamed the database stack for better organization, 4 different services broke simultaneously. The deployment order became critical, and new engineers constantly tripped over these implicit dependencies.

CDK makes dependencies explicit and type-safe:

TypeScript
// Direct object references, no string interpolation
const authStack = new AuthStack(this, 'AuthStack', {
  userTable: databaseStack.userTable, // TypeScript enforces this exists
});

Why TypeScript Infrastructure Changes Everything#

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 leverages TypeScript for both infrastructure and application code:

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

2. Native AWS Service Integration#

Serverless Framework requires plugins for advanced AWS services:

YAML
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:

TypeScript
import { StateMachine } from 'aws-cdk-lib/aws-stepfunctions';
import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks';
import { 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', {
  definition: new LambdaInvoke(this, 'ProcessUser', {
    lambdaFunction: processUserFn,
  }),
});

const api = new GraphqlApi(this, 'UserGraphQL', {
  name: 'user-api',
  schema: SchemaFile.fromAsset('schema.graphql'),
});

3. Infrastructure Composition and Reusability#

Serverless Framework uses includes and variables:

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

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

CDK enables true object-oriented infrastructure:

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

4. Testing Infrastructure#

Serverless Framework testing typically involves:

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

CDK enables comprehensive infrastructure testing:

TypeScript
// test/api-stack.test.ts
import { Template } from 'aws-cdk-lib/assertions';

test('API Gateway has CORS enabled', () => {
  const template = Template.fromStack(stack);

  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 to Migrate#

CDK Excels 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

Stick 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

Migration Complexity Assessment#

Before migrating, evaluate your current setup:

TypeScript
interface MigrationComplexity {
  functionCount: number;
  customResources: boolean;
  plugins: string[];
  environments: number;
  cicdIntegration: boolean;
}

function assessMigrationEffort(current: MigrationComplexity): string {
  const pluginComplexity = current.plugins.filter(p =>
    !['serverless-offline', 'serverless-webpack'].includes(p)
  ).length;

  const score =
    current.functionCount * 0.5 +
    (current.customResources ? 20 : 0) +
    pluginComplexity * 10 +
    current.environments * 5 +
    (current.cicdIntegration ? 15 : 0);

  if (score &lt;30) return 'Low - 1-2 weeks';
  if (score &lt;60) return 'Medium - 2-4 weeks';
  return 'High - 1-2 months';
}

The Migration Decision Framework#

After 15 years of infrastructure decisions, here's how I evaluate major migrations:

Financial Impact Analysis#

Quantify everything:

  • Direct licensing costs (obvious)
  • Hidden operational costs (debugging, maintenance, blockers)
  • Migration investment (engineering time, opportunity cost)
  • Long-term TCO (5-year projection)

Our results: 4.2-month payback, $140K annual savings by year 2.

Technical Risk Assessment#

Ask the hard questions:

  • What happens if the migration fails halfway?
  • Can we rollback quickly if things go wrong?
  • Do we have the team expertise to succeed?
  • What's the blast radius if something breaks?

Our approach: Blue-green migration strategy with full rollback capability.

Team Readiness Evaluation#

Honest assessment of capabilities:

  • TypeScript proficiency: 8/12 engineers comfortable
  • CDK experience: 2/12 engineers with prior experience
  • Available bandwidth: 3 engineers for 3 months
  • Learning curve acceptance: High (team excited about modern tooling)

Business Alignment#

Connect technical decisions to business outcomes:

  • Faster feature delivery (better developer experience)
  • Reduced operational overhead (fewer deployment issues)
  • Improved reliability (compile-time error catching)
  • Better security posture (explicit IAM permissions)

When NOT to Migrate#

Based on our experience, stick with Serverless Framework if:

  1. Small team with no TypeScript experience - The learning curve will kill your velocity
  2. Simple applications with infrequent deployments - The tooling costs might not matter
  3. Heavy reliance on community plugins - CDK ecosystem is smaller
  4. Risk-averse organization - Wait for more CDK adoption in your industry

The Go/No-Go Decision#

Our final decision criteria:

  • ✅ Financial case: 4.2-month payback
  • ✅ Technical benefits: Compile-time safety, better testing
  • ✅ Team capability: 8/12 engineers TypeScript-ready
  • ✅ Business alignment: Faster development, fewer incidents
  • ✅ Risk mitigation: Blue-green deployment strategy

Result: Unanimous team vote to proceed with migration.

What's Next#

The decision was made. Now came the hard part: actually executing a zero-downtime migration of a production system serving

$2.8M ARR.

In Part 2, I'll walk you through setting up the CDK project structure that let us migrate 47 Lambda functions without a single production incident. We'll cover the project architecture decisions, team workflows, and development environment setup that made this migration successful.

Spoiler alert: The hardest part wasn't the code - it was managing 12 engineers, maintaining feature velocity, and keeping everyone aligned during a 4-month infrastructure overhaul.

Loading...

Comments (0)

Join the conversation

Sign in to share your thoughts and engage with the community

No comments yet

Be the first to share your thoughts on this post!

Related Posts