Serverless Framework'ten AWS CDK'ya Geçiş: Bölüm 3 - Lambda Fonksiyonları ve API Gateway
Lambda fonksiyonlarını ve API Gateway konfigürasyonlarını CDK'ya taşıma: bundling optimizasyonu, memory ayarı, hata yönetimi pattern'leri.
Lambda fonksiyonları ve API Gateway konfigürasyonları, migration karmaşıklığının yoğunlaştığı yerdir. YAML’dan TypeScript’e geçiş mekanik bir iş gibi görünür; ta ki bundling, memory boyutlandırma ve hata yönetimi ayrı birer karara dönüşene kadar.
İşe yarayan yaklaşım tek bir ortak NodejsFunction construct’ıdır: runtime, architecture, memory, tracing ve bundling varsayılanları tek yerde durur, farklı davranması gereken fonksiyonlar için her prop override edilebilir kalır. API Gateway tarafında da aynı yöntem geçerlidir; validator’lar, authorizer’lar ve integration response’lar bir kez tanımlanıp route’lar arasında yeniden kullanılır.
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
Fonksiyon Karmaşıklığı#
Lambda fonksiyon migration’ları, gerçek bir sistemde kaç farklı pattern olduğunu fark ettiğinizde hızla karmaşıklaşır. Fonksiyonlar genellikle gereksinimleri birbirinden ayrışan kategorilere bölünür:
Yaygın fonksiyon türleri:
- Farklı response pattern’leri olan API endpoint handler’ları
- Değişken memory ihtiyaçları olan background job processor’lar
- Hızlı response süresi gerektiren webhook handler’lar
- Farklı timeout gereksinimleri olan scheduled fonksiyonlar
Her tür farklı memory ayarlarından, timeout konfigürasyonlarından ve deployment pattern’lerinden faydalanır.
Standartlaştırılmış Lambda Construct#
Serverless Framework’te her fonksiyon tanımı kendi memory, timeout ve environment ayarlarını taşır; bu ayarlar fonksiyon başına tekrar eder:
# 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
CDK karşılığı bu varsayılanları tek bir construct’a taşır; çağrı yerinde yalnızca fonksiyona özgü farklar kalır:
// lib/constructs/production-lambda.ts
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime, Tracing, Architecture } from 'aws-cdk-lib/aws-lambda';
import { Duration, Tags } from 'aws-cdk-lib';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';
export interface ProductionLambdaProps extends Omit<NodejsFunctionProps, 'runtime'> {
stage: string;
functionName: string;
// Fonksiyon bazlı opsiyonel override'lar
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, // ARM64 daha iyi price-performance sunar
// Fonksiyon kategorisine göre memory varsayılanı
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 iyileştirmesi için bundling optimizasyonları
bundling: {
minify: props.stage === 'prod',
sourceMap: true,
sourcesContent: false,
target: 'node20',
keepNames: true,
// Daha küçük bundle boyutları için tree shaking
esbuildArgs: { '--tree-shaking': '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');
}
// Fonksiyon kategorisine göre memory varsayılanları
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;
}
}
// Standartlaştırılmış pattern'lerle kullanım örneği
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);
// API Gateway entegrasyonu: proxy entegrasyonu handler'ın statusCode,
// headers ve body değerlerini olduğu gibi geçirir; hata response'ları
// handler içinde kalır
const userIdResource = users.addResource('{id}');
userIdResource.addMethod('GET', new LambdaIntegration(getUserFn));
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',
},
},
});
}
}
Gelişmiş API Gateway 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 olmayan entegrasyon: selectionPattern Lambda hatasıyla eşleşir,
// yani bu handler'lar status code döndürmek yerine hata fırlatır
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#
Yapılandırılmış 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): APIGatewayProxyResult => {
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 { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { ApiError } from '../libs/api-gateway';
import { withErrorHandling } from '../libs/lambda';
export const get = withErrorHandling(
async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
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 Tabanlı Versiyonlama#
// 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, RuleTargetInput } 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: RuleTargetInput.fromObject({
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
): Method {
return resource.addMethod(httpMethod, integration, {
methodResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Cache-Control': true,
},
}],
// Stage cache'inin key olarak kullanabileceği query string'ler
requestParameters: {
'method.request.querystring.page': false,
'method.request.querystring.limit': false,
},
});
}
// Caching'in kendisi stage seviyesinde açılır
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
Ortak Construct Ne Zaman Mantıklı#
Ortak construct, birkaç fonksiyon aynı runtime, tracing ve bundling varsayılanlarına ihtiyaç duyduğu anda kendini amorti eder. Log retention veya architecture değişikliği her stack yerine tek dosyada yapılır. Bir avuç fonksiyonun altındaysanız düz NodejsFunction çağrıları daha okunaklı kalır, construct de araya yalnızca bir katman ekler.
Bir fonksiyon gerçekten farklıysa construct’ı genişletmek yerine prop’ları override edin. 10 GB memory ve 15 dakikalık timeout isteyen bir video encoder, getOptimalMemory içine yeni bir dal eklemek yerine bu değerleri açıkça geçmelidir. Helper üçüncü özel duruma ulaştığında, isimlendirme kuralı kararı taşımayı bırakmış demektir. Bundling için de aynı kural geçerli: externalModules ve nodeModules fonksiyona özgü bilgilerdir, varsayılandan ayrıldıklarında çağrı yerinde kalmaları daha iyidir.
Sonraki Adım: Database ve Environment Yönetimi#
Lambda fonksiyonları ve API Gateway konfigürasyonları taşındıktan sonra sıra database kaynaklarına ve environment yönetimine gelir. Stateless fonksiyonların aksine database’ler kalıcı veri tutar; kolayca yeniden oluşturulamadıkları için migration adımları da farklıdır.
4. Bölüm şunları ele alıyor:
- DynamoDB tabloları ve RDS instance’larını migrate etmek
- Environment variable yönetimi ve secret handling
- Database erişimi için VPC konfigürasyonları
- Backup ve disaster recovery stratejileri
- Cross-environment tutarlılık pattern’leri
Kaynaklar#
- AWS CDK API Referansı - aws-lambda-nodejs (yeni sekmede açılır) - Paketleme, harici modüller ve tree shaking’i kapsayan NodejsFunction construct referansı
- Amazon API Gateway Geliştirici Rehberi (yeni sekmede açılır) - REST API yapılandırması, istek doğrulama, CORS ve method yanıt kalıpları
- TypeScript ile Lambda fonksiyonu oluşturma (yeni sekmede açılır) - TypeScript handler kalıpları, paketleme seçenekleri ve TypeScript fonksiyonları için Lambda runtime
- Lambda fonksiyonu için provisioned concurrency yapılandırma (yeni sekmede açılır) - Isınma zamanlamalarına alternatif olarak provisioned concurrency ile cold start’ları ortadan kaldırma
- AWS Lambda fonksiyonları için en iyi uygulamalar (yeni sekmede açılır) - AWS’den bellek boyutlandırma, ARM64 mimarisi ve paket optimizasyonu tavsiyeleri
- AWS CDK v2 Geliştirici Rehberi (yeni sekmede açılır) - Construct kompozisyonu, environment yapılandırması ve CDK deployment kalıpları
Serverless Framework'ten AWS CDK'ya Geçiş Rehberi
Serverless Framework'ten AWS CDK'ya tam geçiş sürecini kapsayan 6 bölümlük kapsamlı rehber. Kurulum, uygulama pattern'leri ve best practice'ler dahil.
Bu serideki tüm yazılar
İlgili yazılar
İç servis katmanı kurmadan önce kurup kurmayacağınıza karar verin. Katmanın çağrı başına maliyeti, VPC Lattice'in kazandığı hacim ve direct invoke'un hâlâ kazandığı an.
aws · aws-cdk · lambda +4
Private REST API gRPC’yi yapısal olarak taşıyamaz ve gRPC konuşan her AWS yüzeyi Lambda hedeflerini dışlar. gRPC’den neyi tutmalı, neyi bırakmalı.
aws · aws-cdk · lambda +4
Private REST API, onu açan resource policy, route bazlı AWS_IAM yetkileri, iki CDK stack'i ve Node 22 Lambda'dan çağrıyı imzalamak.
aws · aws-cdk · lambda +4
SigV4 hangi servisin çağırdığını kanıtlar, kimin adına çağırdığını değil. Doğrulanmış özneyi nasıl taşırsınız ve taşıma katmanı gerçekte neyi şifreler.
aws · aws-cdk · lambda +4
Aynı hesapta resource policy ile çağıranın identity policy'si bir OR'dur. Hesaplar arası çağrıda AND'e döner ve sessizlik reddeder. Veri sınırında bunun sonuçları.
aws · aws-cdk · lambda +4