Serverless Framework'ten AWS CDK'ya Geçiş: Bölüm 3 - DynamoDB ve S3
DynamoDB tabloları ve S3 bucket'larını CDK'ya taşıma. Data migration stratejileri ve en iyi uygulamalar.
CDK migration'ımızın 4. haftası. Proje yapımızı ve tooling'i başarıyla migrate etmiştik. Şimdi gerçek test anı gelmişti: 2.8M$ ARR platformumuzu destekleyen 47 production Lambda fonksiyonunu migrate etmek.
"Ne kadar zor olabilir ki?" diye sordu team lead'im. "Sadece handler'ları YAML'dan TypeScript'e taşımak, değil mi?"
Üç hafta sonra, cold start regresyonları, bundling kabusuyla ve yanlış API Gateway timeout'ları nedeniyle production incident'la uğraştıktan sonra çok farklı bir cevabım vardı. Bu, 376 Lambda fonksiyonunu (evet, 47'den çok daha fazlasını keşfettik) migrate etme, cold start'larda 40% tasarruf sağlayan performans optimizasyonları ve şimdi tüm API katmanımızı destekleyen production-grade pattern'lerin hikayesi.
Seri Navigasyonu:
- Bölüm 1: Neden Geçiş Yapalım?
- Bölüm 2: CDK Environment Kurulumu
- Bölüm 3: Lambda Fonksiyonları ve API Gateway Migration (bu yazı)
- Bölüm 4: Database Kaynakları ve Environment Yönetimi
- Bölüm 5: Authentication, Authorization ve IAM
- Bölüm 6: Migration Stratejileri ve Best Practice'ler
376 Fonksiyon Keşfi (5. Hafta)#
Migration audit'imiz sırasında şok edici bir gerçeği ortaya çıkardık: "47 Lambda fonksiyonumuz" aslında tüm environment'lar ve servisler boyunca 376 fonksiyondu. Yıllarca süren hızlı büyüme, tek kişinin tam olarak anlayamayacağı geniş bir serverless ekosistem yaratmıştı.
Dağılım:
- Core API fonksiyonları: 47 (sahip olduğumuzu düşündüğümüz)
- Background job processor'lar: 156
- Webhook handler'lar: 89
- Scheduled fonksiyonlar: 84
Her biri farklı memory ayarları, timeout konfigürasyonları ve deployment pattern'leri ile. Bu basit bir YAML-to-TypeScript dönüşümü olmayacaktı.
Production Lambda Construct (Gizli Silahımız)#
İlk 20 fonksiyonu manuel olarak migrate ettikten ve performans sorunlarıyla karşılaştıktan sonra, 376 fonksiyonun tamamının temelini oluşturan standartlaştırılmış bir construct geliştirdik:
# serverless.yml
functions:
getUser:
handler: src/handlers/users.get
events:
- http:
path: users/{id}
method: get
cors: true
environment:
USERS_TABLE: ${self:service}-${opt:stage}-users
timeout: 10
memorySize: 256
İşte şimdi 376 Lambda fonksiyonumuzun tamamını destekleyen production-grade construct:
// lib/constructs/production-lambda.ts - 376 fonksiyon tarafından kullanılıyor
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime, Tracing, Architecture } from 'aws-cdk-lib/aws-lambda';
import { Duration, Stack } from 'aws-cdk-lib';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
export interface ProductionLambdaProps extends Omit<NodejsFunctionProps, 'runtime'> {
stage: string;
functionName: string;
// 376 fonksiyon migration'ından öğrenilen performans optimizasyonları
enableProvisioning?: boolean;
enableSnapStart?: boolean;
}
export class ProductionLambda extends NodejsFunction {
constructor(scope: Construct, id: string, props: ProductionLambdaProps) {
super(scope, id, {
...props,
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64, // 34% daha iyi price-performance
// 376 fonksiyonu profiling'e dayalı memory optimizasyonu
memorySize: props.memorySize || ProductionLambda.getOptimalMemory(props.functionName),
// Timeout stratejisi: 28s max (API Gateway limiti 29s)
timeout: props.timeout || Duration.seconds(28),
// Tracing sadece production'da aktif
tracing: props.stage === 'prod' ? Tracing.ACTIVE : Tracing.DISABLED,
// Maliyet vs compliance için optimize edilmiş log retention
logRetention: props.stage === 'prod' ? RetentionDays.ONE_MONTH : RetentionDays.ONE_WEEK,
// Her fonksiyonun ihtiyaç duyduğu environment variable'lar
environment: {
NODE_OPTIONS: '--enable-source-maps --max-old-space-size=896',
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
STAGE: props.stage,
FUNCTION_NAME: props.functionName,
...props.environment,
},
// Cold start'ları 40% azaltan bundling optimizasyonları
bundling: {
minify: props.stage === 'prod',
sourceMap: true,
sourcesContent: false,
target: 'node20',
keepNames: true,
// Tree shaking bundle boyutunda 60% tasarruf sağladı
treeShaking: true,
// External module'ler (Lambda runtime tarafından sağlanan)
externalModules: [
'@aws-sdk/*', // Lambda runtime'da AWS SDK v3
'aws-lambda', // Lambda types
],
// Büyük fonksiyonlar için bundle analizi
metafile: props.stage !== 'prod',
// Production debugging için custom banner
banner: props.stage === 'prod'
? '/* Production Lambda - Generated by CDK */'
: undefined,
// Dead code elimination için define
define: {
'process.env.NODE_ENV': props.stage === 'prod' ? '"production"' : '"development"',
},
},
// Kritik fonksiyonlar için reserved concurrency
reservedConcurrentExecutions: props.enableProvisioning ? 10 : undefined,
});
// Tüm fonksiyonlar için standart tag'ler
Tags.of(this).add('Stage', props.stage);
Tags.of(this).add('FunctionName', props.functionName);
Tags.of(this).add('ManagedBy', 'CDK');
}
// 376 fonksiyonu profiling'e dayalı memory optimizasyonu
private static getOptimalMemory(functionName: string): number {
// API fonksiyonları: CPU-bound, daha fazla memory'den yarar görür
if (functionName.includes('api-')) return 1024;
// Background jobs: Memory-intensive processing
if (functionName.includes('job-')) return 2048;
// Webhook'lar: Hızlı response gerekli
if (functionName.includes('webhook-')) return 512;
// Default: Balanced performance/cost
return 1024;
}
}
// 376 farklı fonksiyon tanımını değiştiren kullanım
const getUserFn = new ProductionLambda(this, 'GetUserFunction', {
stage: config.stage,
functionName: 'api-get-user',
entry: 'src/handlers/users/get.ts',
handler: 'handler',
environment: {
USERS_TABLE: usersTable.tableName,
},
});
// Type-safe izinler (artık wildcard IAM policy yok)
usersTable.grantReadData(getUserFn);
// Düzgün error handling ile API Gateway entegrasyonu
const userIdResource = users.addResource('{id}');
userIdResource.addMethod('GET', new LambdaIntegration(getUserFn, {
// Düzgün error handling için integration response'lar
integrationResponses: [
{
statusCode: '200',
responseTemplates: {
'application/json': '$input.path("$")',
},
},
{
statusCode: '404',
selectionPattern: '.*"statusCode":404.*',
responseTemplates: {
'application/json': '{"error": "User not found"}',
},
},
],
}));
Lambda Layer Migration#
Serverless Framework layer'ları:
layers:
shared:
path: layers/shared
compatibleRuntimes:
- nodejs20.x
functions:
createUser:
handler: src/handlers/users.create
layers:
- {Ref: SharedLambdaLayer}
Daha iyi type safety ile CDK yaklaşımı:
// lib/constructs/shared-layer.ts
import { LayerVersion, Code, Runtime } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class SharedLayer extends LayerVersion {
constructor(scope: Construct, id: string) {
super(scope, id, {
code: Code.fromAsset('layers/shared'),
compatibleRuntimes: [Runtime.NODEJS_20_X],
description: 'Shared utilities and dependencies',
});
}
}
// Stack'te kullanım
const sharedLayer = new SharedLayer(this, 'SharedLayer');
const createUserFn = new ServerlessFunction(this, 'CreateUserFunction', {
entry: 'src/handlers/users.ts',
handler: 'create',
config,
layers: [sharedLayer],
});
Function Bundling ve Dependency'ler#
CDK'nın NodejsFunction
'ı gelişmiş bundling seçenekleri sağlıyor:
// lib/constructs/optimized-function.ts
export class OptimizedFunction extends ServerlessFunction {
constructor(scope: Construct, id: string, props: ServerlessFunctionProps) {
super(scope, id, {
...props,
bundling: {
minify: props.config.stage === 'prod',
sourceMap: true,
sourcesContent: false,
target: 'es2022',
keepNames: true,
// External module'ler (bundle edilmez)
externalModules: [
'@aws-sdk/*', // Lambda runtime tarafından sağlanan AWS SDK v3
'aws-lambda', // Sadece type'lar
],
// Belirli module'leri zorla dahil et
nodeModules: ['bcrypt', 'sharp'], // Native dependency'ler
// Build environment
environment: {
NODE_ENV: props.config.stage === 'prod' ? 'production' : 'development',
},
// Custom esbuild plugin'leri
esbuildArgs: {
'--log-level': 'warning',
'--tree-shaking': 'true',
},
},
});
}
}
API Gateway Gelişmiş Konfigürasyonları#
Request Validation#
Serverless Framework request validation:
functions:
createUser:
handler: src/handlers/users.create
events:
- http:
path: users
method: post
request:
schemas:
application/json: ${file(schemas/create-user.json)}
Inline model'ler ve validator'lar ile CDK:
// lib/constructs/validated-api.ts
import {
RestApi,
Model,
JsonSchema,
JsonSchemaType,
RequestValidator,
MethodOptions
} from 'aws-cdk-lib/aws-apigateway';
export class ValidatedApi extends RestApi {
private validator: RequestValidator;
constructor(scope: Construct, id: string, props: RestApiProps) {
super(scope, id, props);
// Reusable validator oluştur
this.validator = new RequestValidator(this, 'BodyValidator', {
restApi: this,
validateRequestBody: true,
validateRequestParameters: false,
});
}
addValidatedMethod(
resource: IResource,
httpMethod: string,
integration: Integration,
schema: JsonSchema
): Method {
// Schema'dan model oluştur
const model = new Model(this, `${httpMethod}${resource.path}Model`, {
restApi: this,
contentType: 'application/json',
schema,
});
// Validation ile method ekle
return resource.addMethod(httpMethod, integration, {
requestValidator: this.validator,
requestModels: {
'application/json': model,
},
});
}
}
// Kullanım
const createUserSchema: JsonSchema = {
type: JsonSchemaType.OBJECT,
required: ['email', 'name'],
properties: {
email: {
type: JsonSchemaType.STRING,
format: 'email',
},
name: {
type: JsonSchemaType.STRING,
minLength: 1,
maxLength: 100,
},
age: {
type: JsonSchemaType.INTEGER,
minimum: 0,
maximum: 150,
},
},
};
api.addValidatedMethod(
users,
'POST',
new LambdaIntegration(createUserFn),
createUserSchema
);
Response Transformations#
Serverless Framework response template'leri:
functions:
getUsers:
handler: src/handlers/users.list
events:
- http:
path: users
method: get
response:
headers:
Content-Type: "'application/json'"
template: $input.path(')
statusCodes:
200:
pattern: ''
404:
pattern: '.*"statusCode":404.*'
template: $input.path('$.errorMessage')
CDK integration response konfigürasyonu:
// lib/constructs/api-integration.ts
export function createLambdaIntegration(
fn: IFunction,
options?: {
enableCors?: boolean;
responseMapping?: Record<string, IntegrationResponse>;
}
): LambdaIntegration {
const responseParameters: Record<string, string> = {};
if (options?.enableCors) {
responseParameters['method.response.header.Access-Control-Allow-Origin'] = "'*'";
}
return new LambdaIntegration(fn, {
proxy: false,
integrationResponses: [
{
statusCode: '200',
responseParameters,
responseTemplates: {
'application/json': '$input.path("$")',
},
},
{
statusCode: '404',
selectionPattern: '.*"statusCode":404.*',
responseParameters,
responseTemplates: {
'application/json': '$input.path("$.errorMessage")',
},
},
{
statusCode: '500',
selectionPattern: '.*"statusCode":5\\d{2}.*',
responseParameters,
responseTemplates: {
'application/json': '{"error": "Internal Server Error"}',
},
},
],
});
}
API Gateway Authorizer'lar#
Serverless Framework authorizer'larından migration:
functions:
auth:
handler: src/handlers/auth.handler
getProfile:
handler: src/handlers/users.profile
events:
- http:
path: users/profile
method: get
authorizer: auth
CDK Lambda authorizer implementasyonu:
// lib/constructs/api-authorizer.ts
import {
TokenAuthorizer,
IdentitySource,
IRestApi
} from 'aws-cdk-lib/aws-apigateway';
import { Duration } from 'aws-cdk-lib';
export class ApiAuthorizer extends TokenAuthorizer {
constructor(scope: Construct, id: string, props: {
api: IRestApi;
authorizerFunction: IFunction;
}) {
super(scope, id, {
restApi: props.api,
handler: props.authorizerFunction,
identitySource: IdentitySource.header('Authorization'),
resultsCacheTtl: Duration.minutes(5),
authorizerName: `${props.api.restApiName}-authorizer`,
});
}
}
// Stack'te kullanım
const authFn = new ServerlessFunction(this, 'AuthorizerFunction', {
entry: 'src/handlers/auth.ts',
handler: 'handler',
config,
});
const authorizer = new ApiAuthorizer(this, 'ApiAuthorizer', {
api: this.api,
authorizerFunction: authFn,
});
// Korumalı endpoint
const profile = users.addResource('profile');
profile.addMethod('GET', new LambdaIntegration(getProfileFn), {
authorizer,
authorizationType: AuthorizationType.CUSTOM,
});
Error Handling Pattern'leri#
Structured Error Response'lar#
Sağlam bir error handling sistemi oluşturun:
// src/libs/api-gateway.ts
export class ApiError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
export const formatError = (error: unknown): APIGatewayProxyResultV2 => {
console.error('Error:', error);
if (error instanceof ApiError) {
return {
statusCode: error.statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: {
code: error.code || 'UNKNOWN_ERROR',
message: error.message,
},
}),
};
}
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred',
},
}),
};
};
// src/libs/lambda.ts
export const withErrorHandling = <T extends (...args: any[]) => any>(
handler: T
): T => {
return (async (...args: Parameters<T>) => {
try {
return await handler(...args);
} catch (error) {
return formatError(error);
}
}) as T;
};
Handler'larda Error Handling Kullanımı#
// src/handlers/users.ts
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { ApiError, withErrorHandling } from '../libs/api-gateway';
export const get = withErrorHandling(
async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> => {
const { id } = event.pathParameters || {};
if (!id) {
throw new ApiError(400, 'User ID is required', 'MISSING_PARAMETER');
}
// Database lookup simülasyonu
const user = await getUserById(id);
if (!user) {
throw new ApiError(404, 'User not found', 'USER_NOT_FOUND');
}
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user }),
};
}
);
API Versioning Stratejileri#
Path-Based Versioning#
// lib/stacks/versioned-api-stack.ts
export class VersionedApiStack extends Stack {
constructor(scope: Construct, id: string, props: ApiStackProps) {
super(scope, id, props);
const api = new RestApi(this, 'VersionedApi', {
restApiName: `my-service-${props.config.stage}`,
});
// Version 1
const v1 = api.root.addResource('v1');
this.setupV1Routes(v1, props.config);
// Breaking change'ler ile Version 2
const v2 = api.root.addResource('v2');
this.setupV2Routes(v2, props.config);
}
private setupV1Routes(parent: IResource, config: EnvironmentConfig) {
const users = parent.addResource('users');
// Legacy response format
const getUserV1Fn = new ServerlessFunction(this, 'GetUserV1Function', {
entry: 'src/handlers/v1/users.ts',
handler: 'get',
config,
});
users.addResource('{id}').addMethod('GET',
new LambdaIntegration(getUserV1Fn)
);
}
private setupV2Routes(parent: IResource, config: EnvironmentConfig) {
const users = parent.addResource('users');
// Pagination ile yeni response format
const getUserV2Fn = new ServerlessFunction(this, 'GetUserV2Function', {
entry: 'src/handlers/v2/users.ts',
handler: 'get',
config,
});
users.addResource('{id}').addMethod('GET',
new LambdaIntegration(getUserV2Fn)
);
}
}
Performans Optimizasyonları#
Lambda Cold Start Optimizasyonu#
// lib/constructs/warm-function.ts
import { Rule, Schedule } from 'aws-cdk-lib/aws-events';
import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
export class WarmFunction extends ServerlessFunction {
constructor(scope: Construct, id: string, props: ServerlessFunctionProps & {
warmingSchedule?: Schedule;
}) {
super(scope, id, props);
if (props.config.stage === 'prod' && props.warmingSchedule) {
// Warming rule oluştur
new Rule(this, 'WarmingRule', {
schedule: props.warmingSchedule,
targets: [
new LambdaFunction(this, {
event: {
source: 'warmer',
action: 'ping',
},
}),
],
});
// Handler'a warming check ekle
this.addEnvironment('ENABLE_WARMING', 'true');
}
}
}
// Handler'da
export const handler = async (event: any) => {
// Warming invocation'larını atla
if (event.source === 'warmer') {
return { statusCode: 200, body: 'Warmed' };
}
// Normal handler logic
};
API Gateway Caching#
// lib/constructs/cached-method.ts
export function addCachedMethod(
resource: IResource,
httpMethod: string,
integration: Integration,
cachingEnabled: boolean = true,
ttl: Duration = Duration.minutes(5)
): Method {
return resource.addMethod(httpMethod, integration, {
methodResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Cache-Control': true,
},
}],
requestParameters: {
'method.request.querystring.page': false,
'method.request.querystring.limit': false,
},
});
}
// Stage level'da caching aktif et
const api = new RestApi(this, 'CachedApi', {
deployOptions: {
cachingEnabled: true,
cacheClusterEnabled: true,
cacheClusterSize: '0.5',
cacheTtl: Duration.minutes(5),
cacheDataEncrypted: true,
},
});
Migration Checklist#
Production'a geçmeden önce şunları ele aldığınızdan emin olun:
- Tüm Lambda fonksiyonları uygun memory/timeout ayarlarıyla migrate edildi
- Environment variable'lar düzgün scope'lanmış ve şifrelenmiş
- API Gateway route'ları mevcut path'lerle tam olarak eşleşiyor
- CORS konfigürasyonu mevcut ayarlarla eşleşiyor
- Request validation schema'ları migrate edildi
- Custom authorizer'lar implement edildi ve test edildi
- Error response'lar backward compatibility'yi koruyor
- Lambda layer'lar düzgün konfigüre edildi
- Cold start optimizasyonları yerinde
- API caching stratejisi implement edildi
- Monitoring ve alarm'lar konfigüre edildi
Migration Sonuçları: 7. Hafta Performans Analizi#
376 Lambda fonksiyonun tamamını ProductionLambda construct'ımıza migrate ettikten sonra sonuçlar beklentileri aştı:
Performans İyileştirmeleri#
- Cold start azaltma: 850ms → 320ms (62% iyileştirme)
- Bundle boyutu azaltma: Ortalama 60% (tree shaking + external modules)
- Memory optimizasyonu: 30% maliyet azaltma (fonksiyon tipine göre doğru boyutlandırılmış memory)
- Error oranı: 0.3% → 0.1% (daha iyi error handling)
Operasyonel İyileştirmeler#
- Deployment tutarlılığı: 376 fonksiyon artık aynı pattern'leri kullanıyor
- Debugging verimliliği: 3 saat → 20 dakika (source map'ler + structured logging)
- Ekip hızı: 40% daha hızlı development (artık YAML debugging yok)
Kritik Ders#
En büyük kazanç teknik değildi - organizasyonaldı. ProductionLambda construct'ında standardize olmak şu anlama geldi:
- Yeni mühendisler 1. günde fonksiyon deploy edebiliyor
- Artık "özel yapılandırma" konfigürasyonları yok
- Güvenlik ve compliance otomatik hale geldi
- Performans optimizasyonları tüm fonksiyonlara uygulandı
Sırada Ne Var: Data Layer Zorluğu#
376 Lambda fonksiyonu başarıyla migrate edildi ve hiç olmadığı kadar iyi performans gösteriyorken, bir sonraki büyük zorlukla karşı karşıya kaldık: data layer.
Serverless Framework kurulumumuzda manuel olarak oluşturulmuş, kötü dokümantasyona sahip ve 2.8M$ ARR'mız için kritik olan 3 DynamoDB tablomuz vardı. Lambda fonksiyonlarının aksine, database'ler kolayca yeniden oluşturulamaz - yıllarca customer verisi içerirler.
4. Bölüm'de, tek bir customer kaydı kaybetmeden data altyapımızı migrate etme hikayesini paylaşacağım:
- Kıl payı kaçırdığımız 47K dolarlık data felaketi
- Production secret'larının ifşa edilmesini önleyen environment variable yönetimi
- RDS instance'larımız için VPC konfigürasyonları
- Migration sırasında bizi kurtaran backup stratejisi
API layer'ı kolay kısımdı. Data migration kariyerlerin yapıldığı veya bozulduğu yerdir.
Yorumlar (0)
Sohbete katıl
Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap
Henüz yorum yok
Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!
Yorumlar (0)
Sohbete katıl
Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap
Henüz yorum yok
Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!