Migrating from Serverless Framework to AWS CDK: Part 6 - Migration Strategies and Best Practices
Plan the Serverless Framework to AWS CDK cut-over: blue-green deployment, rollback procedures, infrastructure testing, and performance tuning.
A CDK migration’s final phase is not about the correctness of the generated infrastructure. It is about operational readiness during cut-over: a plan for taking traffic, a plan for reverting if the migration degrades production, and a plan for reconciling drift that appears under live load. For a migration covering dozens of Lambda functions and multiple DynamoDB tables, the cut-over plan is larger than the migration code itself.
Blue-green deployment with an alarm-driven rollback trigger is the default worth reaching for. It costs a duplicate stack for the length of the cut-over, and it buys a switch that reverses in seconds without waiting on a CloudFormation rollback. The rest of the work is unglamorous: health checks that can decide whether a stack is safe, tests that exercise service limits rather than business logic, and a rollback path fired at least once before production traffic depends on it.
Series Navigation:
- Part 1: Why Make the Switch?
- Part 2: Setting Up Your CDK Environment
- Part 3: Migrating Lambda Functions and API Gateway
- Part 4: Database and Environment Management
- Part 5: Authentication, Authorization, and IAM
- Part 6: Migration Strategies and Best Practices (this post)
Choosing a Cut-Over Pattern
Three patterns dominate serverless migrations, and each one fails in a different place.
Approach #1: Big Bang Migration
Implementation: Deploy all CDK infrastructure inside a single maintenance window.
Where it breaks: CloudFormation deployment time for a large stack is hard to predict, so the window is the first thing to overrun. A failed API Gateway stage deployment or a partial DynamoDB import leaves you rolling back while the service is already down, and the rollback path is as long as the deployment path.
Verdict: Workable for a demo application or an internal tool with a genuine maintenance window. Not workable for a production system with cross-service dependencies.
Approach #2: Strangler Fig Pattern
Implementation: Migrate functions in batches, splitting traffic between the old and new APIs until nothing is left on the old stack.
Where it breaks: Both systems have to share authentication and session state for the whole transition. Cross-stack calls add a network hop the single-stack version did not have. The transition is measured in weeks, and each of those weeks means running two deployment pipelines side by side.
Verdict: The right choice when the application is too large to move at once, provided the dependency map is drawn first and the auth layer is shared rather than duplicated.
Approach #3: Blue-Green Deployment
Implementation: Stand up the full CDK stack in parallel, verify it against production-shaped traffic, then switch DNS or the API Gateway stage.
Where it breaks: It costs a second copy of every stateless resource, and it says nothing about state. Shared tables have to stay readable by both stacks, which constrains schema changes during the cut-over.
Verdict: The default. Rollback is a second DNS or stage change rather than a CloudFormation operation, and that is what makes the switch reversible under pressure.
Production-Ready Migration Strategies
1. Blue-Green Deployment Stack
The blue-green stack carries its own health check, its own alarms, and the function that performs the switch:
// lib/stacks/production-blue-green-stack.ts
import { Stack, StackProps, Tags, CfnOutput, Duration, TreatMissingData } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { RestApi, MethodLoggingLevel, LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
import { Alarm, ComparisonOperator } from 'aws-cdk-lib/aws-cloudwatch';
import { LambdaAction } from 'aws-cdk-lib/aws-cloudwatch-actions';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export interface BlueGreenStackProps extends StackProps {
stage: string;
environment: 'blue' | 'green';
monitoringConfig: {
errorThreshold: number;
latencyThreshold: number;
rollbackFunction: NodejsFunction;
};
}
export class ProductionBlueGreenStack extends Stack {
public readonly api: RestApi;
public readonly healthCheckEndpoint: string;
public readonly switchOverFunction: NodejsFunction;
constructor(scope: Construct, id: string, props: BlueGreenStackProps) {
super(scope, id, props);
// Create the complete CDK infrastructure
this.api = new RestApi(this, 'Api', {
restApiName: `my-service-${props.stage}-${props.environment}`,
description: `Production API - ${props.environment.toUpperCase()} environment`,
deployOptions: {
stageName: props.environment,
// Aggressive throttling during migration for safety
throttlingRateLimit: props.environment === 'green' ? 500 : 1000,
throttlingBurstLimit: props.environment === 'green' ? 1000 : 2000,
// Enhanced monitoring during migration
metricsEnabled: true,
loggingLevel: MethodLoggingLevel.INFO,
dataTraceEnabled: true,
tracingEnabled: true,
},
});
// Deploy all Lambda functions and wire the routes.
// createLambdaFunctions and setupApiRoutes are private helpers on this
// class, omitted here for brevity.
const functions = this.createLambdaFunctions(props);
this.setupApiRoutes(functions);
// Create health check endpoint for monitoring
const healthCheckFn = new NodejsFunction(this, 'HealthCheckFunction', {
entry: 'src/health/health-check.ts',
handler: 'handler',
environment: {
ENVIRONMENT: props.environment,
API_VERSION: process.env.API_VERSION || 'v1',
DEPLOYMENT_TIME: new Date().toISOString(),
},
});
const healthResource = this.api.root.addResource('health');
healthResource.addMethod('GET', new LambdaIntegration(healthCheckFn));
this.healthCheckEndpoint = `${this.api.url}health`;
// Create production monitoring alarms
this.createProductionAlarms(props);
// Traffic switching function
this.switchOverFunction = this.createSwitchOverFunction(props);
// Tag all resources for identification
Tags.of(this).add('Environment', props.environment);
Tags.of(this).add('MigrationPhase', 'cdk-migration');
Tags.of(this).add('DeploymentTime', new Date().toISOString());
Tags.of(this).add('Version', process.env.COMMIT_SHA || 'latest');
// Export critical information
new CfnOutput(this, 'ApiEndpoint', {
value: this.api.url,
exportName: `${this.stackName}-api-endpoint`,
description: `API endpoint for ${props.environment} environment`,
});
new CfnOutput(this, 'HealthCheckUrl', {
value: this.healthCheckEndpoint,
exportName: `${this.stackName}-health-check`,
description: 'Health check endpoint for monitoring',
});
}
private createProductionAlarms(props: BlueGreenStackProps) {
// Error rate alarm - triggers rollback
const errorAlarm = new Alarm(this, 'HighErrorRateAlarm', {
metric: this.api.metricServerError({
period: Duration.minutes(2),
statistic: 'Sum',
}),
threshold: props.monitoringConfig.errorThreshold,
evaluationPeriods: 2,
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
alarmDescription: `High error rate detected in ${props.environment} environment`,
treatMissingData: TreatMissingData.NOT_BREACHING,
});
// Latency alarm - triggers investigation
const latencyAlarm = new Alarm(this, 'HighLatencyAlarm', {
metric: this.api.metricLatency({
period: Duration.minutes(5),
statistic: 'Average',
}),
threshold: props.monitoringConfig.latencyThreshold,
evaluationPeriods: 3,
alarmDescription: `High latency detected in ${props.environment} environment`,
});
// Connect alarms to automated rollback
errorAlarm.addAlarmAction(
new LambdaAction(props.monitoringConfig.rollbackFunction)
);
// Export alarm ARNs for external monitoring
new CfnOutput(this, 'ErrorAlarmArn', {
value: errorAlarm.alarmArn,
exportName: `${this.stackName}-error-alarm`,
});
}
private createSwitchOverFunction(props: BlueGreenStackProps) {
return new NodejsFunction(this, 'TrafficSwitchFunction', {
entry: 'src/deployment/traffic-switch.ts',
handler: 'handler',
timeout: Duration.minutes(5),
environment: {
CURRENT_ENVIRONMENT: props.environment,
TARGET_ENVIRONMENT: props.environment === 'blue' ? 'green' : 'blue',
HOSTED_ZONE_ID: process.env.HOSTED_ZONE_ID!,
DOMAIN_NAME: process.env.API_DOMAIN!,
SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL!,
},
initialPolicy: [
new PolicyStatement({
actions: ['route53:ChangeResourceRecordSets', 'route53:GetChange'],
resources: ['*'],
}),
],
});
}
}
// src/health/health-check.ts - Comprehensive health validation
import { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';
const dynamoDB = new DynamoDBClient({});
export const handler: APIGatewayProxyHandler = async () => {
const startTime = Date.now();
const checks = [];
try {
// Database connectivity check
const tableCheck = await dynamoDB.send(new DescribeTableCommand({
TableName: process.env.USERS_TABLE!,
}));
checks.push({
name: 'database',
status: tableCheck.Table?.TableStatus === 'ACTIVE' ? 'healthy' : 'unhealthy',
responseTime: Date.now() - startTime,
});
// Memory usage check
const memoryUsed = process.memoryUsage();
checks.push({
name: 'memory',
status: memoryUsed.heapUsed < 100 * 1024 * 1024 ? 'healthy' : 'warning', // 100MB threshold
details: {
heapUsed: Math.round(memoryUsed.heapUsed / 1024 / 1024) + 'MB',
heapTotal: Math.round(memoryUsed.heapTotal / 1024 / 1024) + 'MB',
},
});
const overallStatus = checks.every(check => check.status === 'healthy') ? 'healthy' : 'degraded';
return {
statusCode: overallStatus === 'healthy' ? 200 : 503,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
},
body: JSON.stringify({
status: overallStatus,
environment: process.env.ENVIRONMENT,
version: process.env.API_VERSION,
deploymentTime: process.env.DEPLOYMENT_TIME,
timestamp: new Date().toISOString(),
responseTime: Date.now() - startTime,
checks,
}),
};
} catch (error) {
return {
statusCode: 503,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: 'unhealthy',
error: (error as Error).message,
timestamp: new Date().toISOString(),
}),
};
}
};
2. Strangler Fig Pattern
When to use: Large applications requiring zero-downtime migration.
// lib/constructs/migration/traffic-splitter.ts
import { Construct } from 'constructs';
import { RestApi, Deployment, Stage } from 'aws-cdk-lib/aws-apigateway';
import { Alarm } from 'aws-cdk-lib/aws-cloudwatch';
export class TrafficSplitter extends Construct {
constructor(scope: Construct, id: string, props: {
legacyApiId: string;
newApi: RestApi;
trafficPercentageToNew: number;
}) {
super(scope, id);
// Create canary deployment
const deployment = new Deployment(this, 'CanaryDeployment', {
api: props.newApi,
description: `Canary deployment ${new Date().toISOString()}`,
});
const stage = new Stage(this, 'CanaryStage', {
deployment,
stageName: 'canary',
canarySettings: {
percentTraffic: props.trafficPercentageToNew,
useStageCache: false,
},
});
// CloudWatch alarms for monitoring.
// Scope the metric to the canary stage, not the whole API.
new Alarm(this, 'CanaryErrorAlarm', {
metric: stage.metricServerError(),
threshold: 5,
evaluationPeriods: 2,
});
}
}
3. DNS-Level Traffic Switching
When to use: When the switch has to happen outside CloudFormation, so that a rollback is one record change rather than a stack update.
// lib/stacks/blue-green-stack.ts
export class BlueGreenStack extends Stack {
constructor(scope: Construct, id: string, props: {
stage: string;
version: 'blue' | 'green';
}) {
super(scope, id);
const api = new RestApi(this, 'Api', {
restApiName: `my-service-${props.stage}-${props.version}`,
deployOptions: {
stageName: props.version,
},
});
// Tag resources for easy identification
Tags.of(this).add('Deployment', props.version);
Tags.of(this).add('Version', process.env.COMMIT_SHA || 'latest');
// Export API endpoint
new CfnOutput(this, 'ApiEndpoint', {
value: api.url,
exportName: `${this.stackName}-endpoint`,
});
}
}
// deployment-scripts/blue-green-switch.ts
import { Route53Client, ChangeResourceRecordSetsCommand } from '@aws-sdk/client-route53';
export async function switchTraffic(targetVersion: 'blue' | 'green') {
const route53 = new Route53Client({});
await route53.send(new ChangeResourceRecordSetsCommand({
HostedZoneId: process.env.HOSTED_ZONE_ID,
ChangeBatch: {
Changes: [{
Action: 'UPSERT',
ResourceRecordSet: {
Name: 'api.example.com',
Type: 'CNAME',
TTL: 60,
ResourceRecords: [{
Value: `api-${targetVersion}.execute-api.region.amazonaws.com`,
}],
},
}],
},
}));
}
Effective Testing Strategy for Production
A suite that passes every unit and integration test can still miss the failures that surface at cut-over. Those failures are usually about limits, not logic.
What Standard Tests Miss
What the usual suite covers: Unit tests, integration tests, and a load test, all green.
What it leaves uncovered:
- CloudFormation template size passing the per-stack limit as constructs accumulate
- API Gateway integration timeouts conflicting with Lambda timeout settings
- DynamoDB throttling under peak traffic
- JWT validation cost growing with request volume
Production-Oriented Testing Strategy
Assertions against the synthesized template catch limit and configuration problems before a deploy:
// test/infrastructure/api-stack.test.ts
import { Template, Match } from 'aws-cdk-lib/assertions';
import { App } from 'aws-cdk-lib';
import { ApiStack } from '../../lib/stacks/api-stack';
describe('ApiStack', () => {
let template: Template;
beforeAll(() => {
const app = new App();
const stack = new ApiStack(app, 'TestStack', {
config: testConfig,
});
template = Template.fromStack(stack);
});
test('Lambda functions have correct runtime', () => {
template.allResourcesProperties('AWS::Lambda::Function', {
Runtime: 'nodejs20.x',
});
});
test('API Gateway has throttling enabled', () => {
template.hasResourceProperties('AWS::ApiGateway::Stage', {
ThrottlingRateLimit: Match.anyValue(),
ThrottlingBurstLimit: Match.anyValue(),
});
});
test('DynamoDB tables have point-in-time recovery', () => {
template.allResourcesProperties('AWS::DynamoDB::Table', {
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true,
},
});
});
});
Integration Testing
// test/integration/api.test.ts
import { CloudFormationClient, ListExportsCommand } from '@aws-sdk/client-cloudformation';
import axios from 'axios';
describe('API Integration Tests', () => {
let apiEndpoint: string;
let authToken: string;
beforeAll(async () => {
// Get deployed API endpoint
const cf = new CloudFormationClient({});
const exports = await cf.send(new ListExportsCommand({}));
apiEndpoint = exports.Exports?.find(
e => e.Name === 'ApiStack-endpoint'
)?.Value!;
// Get auth token
authToken = await getTestAuthToken();
});
test('Health check endpoint', async () => {
const response = await axios.get(`${apiEndpoint}/health`);
expect(response.status).toBe(200);
expect(response.data).toEqual({ status: 'healthy' });
});
test('Create and retrieve user', async () => {
// Create user
const createResponse = await axios.post(
`${apiEndpoint}/users`,
{ name: 'Test User', email: '[email protected]' },
{ headers: { Authorization: `Bearer ${authToken}` } }
);
expect(createResponse.status).toBe(201);
// Retrieve user
const userId = createResponse.data.userId;
const getResponse = await axios.get(
`${apiEndpoint}/users/${userId}`,
{ headers: { Authorization: `Bearer ${authToken}` } }
);
expect(getResponse.data.name).toBe('Test User');
});
});
Load Testing
// test/load/k6-script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const errorRate = new Rate('errors');
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up
{ duration: '5m', target: 100 }, // Sustain
{ duration: '2m', target: 200 }, // Spike
{ duration: '5m', target: 200 }, // Sustain spike
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
errors: ['rate<0.01'], // Error rate under 1%
},
};
export default function() {
const response = http.get(`${__ENV.API_URL}/users`);
const success = check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
errorRate.add(!success);
sleep(1);
}
Rollback Procedures
Automated Rollback
// lib/constructs/deployment/safe-deployment.ts
import { Construct } from 'constructs';
import { Alarm, TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch';
import { RestApi } from 'aws-cdk-lib/aws-apigateway';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { Topic } from 'aws-cdk-lib/aws-sns';
import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions';
import { LambdaSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';
import { CfnOutput } from 'aws-cdk-lib';
export class SafeDeployment extends Construct {
constructor(scope: Construct, id: string, props: {
api: RestApi;
alarmThreshold: number;
rollbackFunction: IFunction;
}) {
super(scope, id);
// Create CloudWatch alarm
const alarm = new Alarm(this, 'DeploymentAlarm', {
metric: props.api.metricServerError(),
threshold: props.alarmThreshold,
evaluationPeriods: 2,
treatMissingData: TreatMissingData.NOT_BREACHING,
});
// SNS topic for notifications
const topic = new Topic(this, 'RollbackTopic');
alarm.addAlarmAction(new SnsAction(topic));
// Lambda for automated rollback
topic.addSubscription(
new LambdaSubscription(props.rollbackFunction)
);
// Manual rollback command
new CfnOutput(this, 'RollbackCommand', {
value: `aws lambda invoke --function-name ${props.rollbackFunction.functionName} --payload '{"action":"rollback"}' response.json`,
});
}
}
// src/deployment/rollback-handler.ts
import { SNSEvent } from 'aws-lambda';
import { CodeDeployClient, StopDeploymentCommand } from '@aws-sdk/client-codedeploy';
// Helper functions
async function switchTraffic(version: string): Promise<void> {
// Implementation for traffic switching
}
async function notifySlack(message: { channel: string; message: string }): Promise<void> {
// Implementation for Slack notification
}
export const handler = async (event: SNSEvent) => {
console.log('Initiating rollback:', JSON.stringify(event, null, 2));
const codedeploy = new CodeDeployClient({});
// Stop current deployment
await codedeploy.send(new StopDeploymentCommand({
deploymentId: process.env.CURRENT_DEPLOYMENT_ID,
autoRollbackEnabled: true,
}));
// Revert traffic to previous version
await switchTraffic('blue'); // Assuming green was failing
// Notify team
await notifySlack({
channel: '#alerts',
message: 'Automatic rollback initiated due to high error rate',
});
};
Performance Optimization
Lambda Performance Tuning
// lib/constructs/performance/optimized-function.ts
import { Construct } from 'constructs';
import { Duration, Stack } from 'aws-cdk-lib';
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Architecture, CfnFunction, CfnAlias } from 'aws-cdk-lib/aws-lambda';
// Base function interface
interface ServerlessFunctionProps extends NodejsFunctionProps {
config: {
stage: string;
};
}
export class OptimizedFunction extends NodejsFunction {
constructor(scope: Construct, id: string, props: ServerlessFunctionProps & {
enableProvisioning?: boolean;
enableSnapStart?: boolean;
}) {
super(scope, id, {
...props,
memorySize: props.memorySize || 1024,
architecture: Architecture.ARM_64, // Better price/performance
environment: {
...props.environment,
NODE_OPTIONS: '--enable-source-maps --max-old-space-size=896',
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
},
});
// Provisioned concurrency for critical functions
if (props.enableProvisioning && props.config.stage === 'prod') {
const version = this.currentVersion;
new CfnAlias(this, 'ProvisionedAlias', {
functionName: this.functionName,
functionVersion: version.version,
name: 'provisioned',
provisionedConcurrencyConfig: {
provisionedConcurrentExecutions: 5,
},
});
}
// SnapStart for Java functions
if (props.enableSnapStart) {
const cfnFunction = this.node.defaultChild as CfnFunction;
cfnFunction.snapStart = {
applyOn: 'PublishedVersions',
};
}
}
}
API Gateway Optimization
// lib/constructs/performance/cached-api.ts
import { Construct } from 'constructs';
import { Duration } from 'aws-cdk-lib';
import { RestApi, RestApiProps } from 'aws-cdk-lib/aws-apigateway';
export class CachedApi extends RestApi {
constructor(scope: Construct, id: string, props: RestApiProps & {
cacheConfig?: {
ttlMinutes: number;
encrypted: boolean;
clusterSize: string;
};
}) {
super(scope, id, {
...props,
deployOptions: {
...props.deployOptions,
cachingEnabled: true,
cacheClusterEnabled: true,
cacheClusterSize: props.cacheConfig?.clusterSize || '0.5',
cacheDataEncrypted: props.cacheConfig?.encrypted ?? true,
cacheTtl: Duration.minutes(props.cacheConfig?.ttlMinutes || 5),
methodOptions: {
'/*/*': {
cachingEnabled: true,
cacheKeyParameters: [
'method.request.path.proxy',
'method.request.querystring.page',
],
},
},
},
});
}
}
Monitoring and Observability
Comprehensive Monitoring Stack
// lib/stacks/monitoring-stack.ts
import { Stack, StackProps, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Dashboard, GraphWidget, Alarm } from 'aws-cdk-lib/aws-cloudwatch';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
// Interface for ApiStack
interface ApiStack extends Stack {
api: any; // RestApi from aws-apigateway
functions: NodejsFunction[];
}
export class MonitoringStack extends Stack {
constructor(scope: Construct, id: string, props: {
apiStack: ApiStack;
stage: string;
}) {
super(scope, id);
// Create dashboard
const dashboard = new Dashboard(this, 'ServiceDashboard', {
dashboardName: `my-service-${props.stage}`,
});
// API metrics
dashboard.addWidgets(
new GraphWidget({
title: 'API Requests',
left: [props.apiStack.api.metricCount()],
right: [props.apiStack.api.metricLatency()],
}),
new GraphWidget({
title: 'API Errors',
left: [
props.apiStack.api.metric4XXError(),
props.apiStack.api.metric5XXError(),
],
})
);
// Lambda metrics
const lambdaWidgets = props.apiStack.functions.map(fn =>
new GraphWidget({
title: `${fn.functionName} Performance`,
left: [fn.metricInvocations()],
right: [fn.metricDuration()],
})
);
dashboard.addWidgets(...lambdaWidgets);
// Alarms
this.createAlarms(props.apiStack);
}
private createAlarms(apiStack: ApiStack) {
// API Gateway alarms
new Alarm(this, 'HighErrorRate', {
metric: apiStack.api.metric5XXError({
period: Duration.minutes(5),
statistic: 'Sum',
}),
threshold: 10,
evaluationPeriods: 2,
});
// Lambda alarms
apiStack.functions.forEach(fn => {
new Alarm(this, `${fn.node.id}Throttles`, {
metric: fn.metricThrottles(),
threshold: 5,
evaluationPeriods: 2,
});
new Alarm(this, `${fn.node.id}Errors`, {
metric: fn.metricErrors(),
threshold: 10,
evaluationPeriods: 2,
});
});
}
}
Distributed Tracing
// lib/constructs/observability/tracing.ts
import { Construct } from 'constructs';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Tracing } from 'aws-cdk-lib/aws-lambda';
import { OptimizedFunction } from '../performance/optimized-function';
// Base function interface
interface ServerlessFunctionProps {
environment?: Record<string, string>;
}
export class TracedFunction extends OptimizedFunction {
constructor(scope: Construct, id: string, props: ServerlessFunctionProps) {
super(scope, id, {
...props,
tracing: Tracing.ACTIVE,
environment: {
...props.environment,
// _X_AMZN_TRACE_ID is reserved by Lambda and injected per invocation;
// setting it here is rejected at deploy time.
AWS_XRAY_CONTEXT_MISSING: 'LOG_ERROR',
AWS_XRAY_LOG_LEVEL: 'error',
},
});
// Add X-Ray permissions
this.addToRolePolicy(new PolicyStatement({
actions: [
'xray:PutTraceSegments',
'xray:PutTelemetryRecords',
],
resources: ['*'],
}));
}
}
// src/libs/tracing.ts
import { Tracer } from '@aws-lambda-powertools/tracer';
const tracer = new Tracer({
serviceName: process.env.SERVICE_NAME || 'my-service',
});
export function traceMethod(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const segment = tracer.getSegment();
const subsegment = segment?.addNewSubsegment(propertyKey);
try {
const result = await originalMethod.apply(this, args);
subsegment?.close();
return result;
} catch (error) {
subsegment?.addError(error as Error);
subsegment?.close();
throw error;
}
};
return descriptor;
}
Migration Checklist
Pre-Migration
-
Inventory current resources
- Document all Lambda functions
- List API Gateway endpoints
- Map DynamoDB tables and indexes
- Identify custom resources
- Note all environment variables and secrets
-
Assess dependencies
- Review Serverless plugins in use
- Check for custom CloudFormation resources
- Identify external service integrations
- Document IAM roles and policies
-
Plan migration strategy
- Choose migration pattern (big bang, strangler fig, blue-green)
- Define rollback procedures
- Set success criteria
- Schedule maintenance windows if needed
During Migration
-
Set up CDK project
- Initialize repository with CDK
- Configure environments
- Set up CI/CD pipelines
- Implement infrastructure tests
-
Migrate components
- Start with stateless resources
- Import existing stateful resources
- Migrate Lambda functions
- Set up API Gateway
- Configure authentication
-
Testing
- Run unit tests
- Execute integration tests
- Perform load testing
- Validate security configurations
Post-Migration
-
Monitor and optimize
- Set up comprehensive monitoring
- Configure alerts
- Review performance metrics
- Optimize cold starts
-
Documentation
- Update runbooks
- Document new deployment procedures
- Create architecture diagrams
- Train team on CDK
-
Cleanup
- Remove old Serverless Framework resources
- Delete unused IAM roles
- Clean up S3 deployment buckets
- Update DNS records
Common Pitfalls and Solutions
1. Resource Naming Conflicts
// Avoid hardcoded names
// Bad
const table = new Table(this, 'Table', {
tableName: 'users-table', // Will conflict if exists
});
// Good
const table = new Table(this, 'Table', {
tableName: `${props.serviceName}-${props.stage}-users`,
});
2. State Management
// Separate stateful and stateless resources
import { App } from 'aws-cdk-lib';
const app = new App();
// Stateful resources in separate stack
const dataStack = new DataStack(app, 'DataStack', {
terminationProtection: true,
});
// Stateless resources can be updated freely
const apiStack = new ApiStack(app, 'ApiStack', {
tables: dataStack.tables,
});
3. Environment Variable Migration
// Map Serverless variables to CDK
import { Stack, Fn } from 'aws-cdk-lib';
const legacyMappings: Record<string, string> = {
'${self:service}': props.serviceName,
'${opt:stage}': props.stage,
'${opt:region}': Stack.of(this).region,
'${cf:OtherStack.Output}': Fn.importValue('OtherStack-Output'),
};
When to Stay with Serverless Framework
Certain scenarios benefit more from Serverless Framework than CDK:
- Simple CRUD applications with minimal customization needs
- Proof-of-concept projects that need rapid prototyping
- Teams without TypeScript experience and no bandwidth for training
- Applications with heavy plugin dependencies that don’t exist in CDK
- Organizations with YAML-only infrastructure policies
Where the Default Holds
Blue-green with an alarm-driven rollback is the default for a production cut-over. Override it when the application is small enough that a maintenance window is honest about its cost, or when it is large enough that a strangler fig transition is the only way to keep the auth layer coherent. The rest of the plan survives either choice: import stateful resources instead of recreating them, assert against the synthesized template, and fire the rollback once in a staging account before production traffic depends on it.
Moving from YAML configuration to TypeScript brings compilation, testing, and code review to infrastructure. That benefit arrives only if the tests run in CI and the rollback path has been exercised. Without those two, CDK is a more expensive way to write CloudFormation.
References
- AWS CDK v2 Developer Guide - Official documentation covering constructs, stacks, environments, and best practices for CDK v2.
- CDK Pipelines: Continuous Delivery for AWS CDK Applications - Guide to building automated multi-environment CI/CD pipelines with CDK Pipelines.
- Blue/Green Deployments Using AWS CDK Pipelines and AWS CodeDeploy - AWS DevOps blog post on zero-downtime blue/green deployment patterns with CDK.
- AWS Lambda Best Practices - Official guidance on function performance, memory, timeouts, and initialization code.
- Serverless Framework AWS Lambda Functions Guide - Reference for Serverless Framework function configuration used as the migration source.
- AWS CDK Best Practices - Recommended patterns for developing and deploying cloud infrastructure with CDK.
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
Multi-environment deployment, performance optimization at scale, cost management, and monitoring with solid incident response patterns.
Step-by-step guide to adding Sentry to a React Native Expo app: SDK setup, Expo Router instrumentation, session replay, and source maps for EAS.
Strategies to prevent and handle DynamoDB throttling in Single Table Design: partition key design, write sharding, capacity modes, DAX, and retry patterns.
A technical guide to choosing and implementing AWS edge computing for global apps, with practical examples and cost optimization strategies.
Lessons from running LangChain in production: the anti-patterns that cause failures, the patterns that work, with code examples and cost optimization strategies.