AWS Serverless TypeScript Projelerini Yapılandırma: Eksiksiz Rehber
AWS Lambda, API Gateway ve TypeScript ile production-ready serverless projeleri oluşturmak için en iyi uygulamalar. Gerçek dünya örnekleri, maliyet optimizasyonu ve performans ipuçları.
Üç yıl önce, EC2 instance'ları üzerinde geleneksel bir Express.js API çalıştırıyordum. Sabit maliyetler, öngörülebilir ölçeklendirme, 99.9% uptime. Hayat güzeldi. Sonra en büyük müşterimiz ayda bir kez, 10 dakika içinde 50.000 webhook işlemesi gerektiren bir özellik istedi.
Aylık 10 dakikalık bir yoğunluk için EC2 instance'larını 7/24 çalışır durumda tutmak israf gibiydi. İşte o zaman AWS Lambda'ya dalış yaptım. 30'dan fazla production Lambda fonksiyonu oluşturmak, her serverless hatasını yapmak ve AWS faturalarında çok fazla para harcamaktan öğrendiklerimi paylaşıyorum.
Neden Sonunda Serverless'ı Benimsedim (Yıllarca Direniş Sonrası)#
Eskiden serverless'ı "ekstra adımlarla vendor lock-in" olarak nitelendiren biriyim. Kubernetes cluster'ları yönetmek ve JVM garbage collector'larını ince ayar yapmak geçmişinden geldiğim için, Lambda kontrolü bırakmak gibi geliyordu. Ancak üç olay fikrimi değiştirdi:
Gece Yarısı Ölçeklendirme Felaketi (Haziran 2022)#
Express API'miz gece 2'de Hacker News'te yer aldı. Trafik 100 istek/dk'dan 5.000 istek/dk'ya çıktı. Auto-scaling grubumuz yeni instance'lar başlatmak için 8 dakika aldı. O zamana kadar başarısız ödeme işlemlerinde 3.000$ kaybetmiştik ve Redis cache'imiz çökmüştü.
Lambda anında ölçeklenirdi. İşte o zaman dikkat etmeye başladım.
Webhook İşleme Cehennemi (Ağustos 2022)#
Bir müşteri 10.000+ event'in patlamalar halinde gelebileceği Stripe webhook'larını işlemeye ihtiyaç duyuyordu. EC2 ile iki kötü seçeneğimiz vardı:
- Tepe yük için fazla provision (pahalı)
- Queue kullan ve webhook timeout riski al (güvenilmez)
Lambda'nın otomatik concurrency ölçeklendirmesi bunu zarif bir şekilde çözdü. Her webhook kendi fonksiyon instance'ını aldı. Queue yok, timeout yok, fazla provisioning yok.
800$ Boşta Kalma Maliyeti Keşfi (Ekim 2022)#
Gerçek compute kullanımımızı hesapladım. "Yüksek performanslı" API sunucularımız zamanın 87%'sinde boştaydı, ama 100%'ü için ödeme yapıyorduk. Bu dönen fanlar ve kullanılmayan CPU döngüleri için aylık 800dı.
Lambda'nın milisaniye başına ödeme modeli aniden parlak görünüyordu.
Production'da Gerçekten İşe Yarayan Stack#
Birden fazla yaklaşımı denedikten sonra, işte karar verdiğimiz:
// Production CDK stack'imiz - acıyla rafine edildi
import { Stack, StackProps, Duration, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { RestApi, LambdaIntegration, Cors } from 'aws-cdk-lib/aws-apigateway';
import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
export class ProductionServerlessStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// DynamoDB tablosu - single-table design'ı zor yoldan öğrendik
const dataTable = new Table(this, 'DataTable', {
partitionKey: { name: 'PK', type: AttributeType.STRING },
sortKey: { name: 'SK', type: AttributeType.STRING },
billingMode: BillingMode.PAY_PER_REQUEST, // On-demand fiyatlama spike'larda bizi kurtardı
// Point-in-time recovery bir junior dev'in DELETE hatasından bizi kurtardı
pointInTimeRecovery: true,
removalPolicy: RemovalPolicy.RETAIN, // Prod verisini asla yanlışlıkla silme
});
// Farklı erişim pattern'leri için GSI ekle
dataTable.addGlobalSecondaryIndex({
indexName: 'GSI1',
partitionKey: { name: 'GSI1PK', type: AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: AttributeType.STRING },
});
// Production'a hazır ayarlarla Lambda fonksiyonu
const apiHandler = new NodejsFunction(this, 'ApiHandler', {
entry: 'src/handlers/api.ts',
runtime: Runtime.NODEJS_20_X,
// Gerçek profiling'e dayalı memory boyutlandırma, tahmin değil
memorySize: 1024, // JSON işleme workload'umuz için sweet spot
timeout: Duration.seconds(28), // API Gateway'in 29s limitinin altında
environment: {
TABLE_NAME: dataTable.tableName,
NODE_ENV: 'production',
// DynamoDB için connection reuse'u etkinleştir
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
// Özel env vars
LOG_LEVEL: 'info',
ENABLE_X_RAY: 'true',
},
bundling: {
minify: true,
target: 'node20',
// Bundle'dan aws-sdk'yı hariç tut - Lambda runtime sağlıyor
externalModules: ['@aws-sdk/*'],
// Kullanılmayan kodu tree-shake et
treeShaking: true,
// Prod sorunlarını debug etmek için source maps
sourceMap: true,
},
});
// Lambda'ya DynamoDB erişimi ver
dataTable.grantReadWriteData(apiHandler);
// API Gateway kurulumu
const api = new RestApi(this, 'ServerlessApi', {
restApiName: 'production-serverless-api',
defaultCorsPreflightOptions: {
allowOrigins: Cors.ALL_ORIGINS,
allowMethods: Cors.ALL_METHODS,
allowHeaders: ['Content-Type', 'Authorization'],
},
});
// Ana endpoint'i tanımla
const apiIntegration = new LambdaIntegration(apiHandler);
api.root.addProxy({
defaultIntegration: apiIntegration,
});
}
}
TypeScript Pattern'leri ile Maliyetleri Düşürmek#
1. Etkili Error Handling#
// src/utils/error-handler.ts
export class AppError extends Error {
public readonly statusCode: number;
public readonly isOperational: boolean;
constructor(message: string, statusCode: number = 500, isOperational: boolean = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
export const handleError = (error: Error | AppError): { statusCode: number; body: string } => {
console.error('Error:', {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
});
if (error instanceof AppError) {
return {
statusCode: error.statusCode,
body: JSON.stringify({
error: error.message,
code: error.statusCode,
}),
};
}
// Beklenmedik hatalar için generic response
return {
statusCode: 500,
body: JSON.stringify({
error: 'Internal server error',
code: 500,
}),
};
};
2. DynamoDB Single-Table Design#
// src/models/base.ts
export interface BaseEntity {
PK: string; // Partition Key
SK: string; // Sort Key
GSI1PK?: string; // Global Secondary Index
GSI1SK?: string;
entityType: string;
createdAt: string;
updatedAt: string;
}
// src/models/user.ts
export interface User extends BaseEntity {
PK: `USER#${string}`;
SK: `PROFILE`;
GSI1PK: `USER#${string}`;
GSI1SK: `PROFILE`;
entityType: 'User';
email: string;
name: string;
status: 'active' | 'inactive';
}
// src/repositories/user-repository.ts
export class UserRepository {
constructor(private dynamoClient: DynamoDBClient) {}
async createUser(userData: Omit<User, 'PK' | 'SK' | 'createdAt' | 'updatedAt'>): Promise<User> {
const userId = randomUUID();
const now = new Date().toISOString();
const user: User = {
PK: `USER#${userId}`,
SK: `PROFILE`,
GSI1PK: `USER#${userId}`,
GSI1SK: `PROFILE`,
entityType: 'User',
createdAt: now,
updatedAt: now,
...userData,
};
await this.dynamoClient.send(new PutItemCommand({
TableName: process.env.TABLE_NAME,
Item: marshall(user),
ConditionExpression: 'attribute_not_exists(PK)',
}));
return user;
}
async getUser(userId: string): Promise<User | null> {
const result = await this.dynamoClient.send(new GetItemCommand({
TableName: process.env.TABLE_NAME,
Key: marshall({
PK: `USER#${userId}`,
SK: `PROFILE`,
}),
}));
return result.Item ? unmarshall(result.Item) as User : null;
}
}
3. Lambda Handler Pattern'i#
// src/handlers/api.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { UserRepository } from '../repositories/user-repository';
import { AppError, handleError } from '../utils/error-handler';
// Client'ı handler dışında başlatarak cold start'ları azalt
const dynamoClient = new DynamoDBClient({
region: process.env.AWS_REGION,
});
const userRepository = new UserRepository(dynamoClient);
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
console.log('Request:', {
method: event.httpMethod,
path: event.path,
headers: event.headers,
});
const { httpMethod, path } = event;
// Basit routing logic
if (httpMethod === 'POST' && path === '/users') {
const body = JSON.parse(event.body || '{}');
const user = await userRepository.createUser(body);
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(user),
};
}
if (httpMethod === 'GET' && path.startsWith('/users/')) {
const userId = path.split('/')[2];
const user = await userRepository.getUser(userId);
if (!user) {
throw new AppError('User not found', 404);
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(user),
};
}
throw new AppError('Route not found', 404);
} catch (error) {
const errorResponse = handleError(error as Error);
return {
...errorResponse,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
};
}
};
Maliyet Optimizasyon Dersleri#
1. Memory vs Duration Trade-off#
// Bu pattern bizi ayda 200$ kurtardı
const optimizedHandler = new NodejsFunction(this, 'OptimizedHandler', {
// Daha fazla memory = daha hızlı execution = daha az maliyet
memorySize: 1024, // 512'den 1024'e çıkarmak 30% hızlandırdı
timeout: Duration.seconds(15), // 30'dan 15'e indirebildik
});
2. Connection Reuse#
// Yanlış - her çağrıda yeni connection
export const badHandler = async (event: APIGatewayProxyEvent) => {
const dynamoClient = new DynamoDBClient({}); // Bu pahalı!
// ...işlemler
};
// Doğru - connection'ı tekrar kullan
const dynamoClient = new DynamoDBClient({}); // Handler dışında
export const goodHandler = async (event: APIGatewayProxyEvent) => {
// Mevcut connection'ı kullan
};
3. Bundle Size Optimizasyonu#
// package.json'da sadece ihtiyacın olanları içe aktar
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; // ✅ Spesifik
import AWS from 'aws-sdk'; // ❌ Tüm SDK'yı içe aktarır
Production'da Öğrenilen Dersler#
1. CloudWatch Logs Maliyeti#
CloudWatch Logs faturamız ayda 400a ulaştı. Çözüm:
// Log level'ları akıllıca kullan
const logger = {
error: (message: string, meta?: any) => {
console.error(JSON.stringify({ level: 'error', message, meta, timestamp: new Date().toISOString() }));
},
warn: (message: string, meta?: any) => {
console.warn(JSON.stringify({ level: 'warn', message, meta, timestamp: new Date().toISOString() }));
},
info: (message: string, meta?: any) => {
if (process.env.LOG_LEVEL !== 'error') {
console.log(JSON.stringify({ level: 'info', message, meta, timestamp: new Date().toISOString() }));
}
},
};
2. Cold Start Optimizasyonu#
// Provisioned concurrency sadece kritik endpoint'ler için
const criticalHandler = new NodejsFunction(this, 'CriticalHandler', {
// Sadece payment processing için provisioned concurrency
reservedConcurrencyLimit: 10,
});
// Diğerleri için on-demand yeterli
const regularHandler = new NodejsFunction(this, 'RegularHandler', {
// Provisioned concurrency yok = maliyet tasarrufu
});
3. DynamoDB Maliyet Optimizasyonu#
// Write-heavy workload'lar için on-demand
const writeHeavyTable = new Table(this, 'WriteHeavyTable', {
billingMode: BillingMode.PAY_PER_REQUEST, // Spike'larda maliyet etkili
});
// Predictable workload'lar için provisioned
const predictableTable = new Table(this, 'PredictableTable', {
billingMode: BillingMode.PROVISIONED,
readCapacity: 5,
writeCapacity: 5,
});
Hatalardan Öğrenenler#
1. The Great DynamoDB Scan Incident#
// Bu kod bizi bir günde 800$ mal etti
const getAllUsers = async () => {
const result = await dynamoClient.send(new ScanCommand({
TableName: process.env.TABLE_NAME,
}));
return result.Items; // 2M kayıt scan'ledi!
};
// Düzeltme: Query kullan
const getUsersByStatus = async (status: string) => {
const result = await dynamoClient.send(new QueryCommand({
TableName: process.env.TABLE_NAME,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk',
ExpressionAttributeValues: {
':pk': `STATUS#${status}`,
},
}));
return result.Items;
};
2. Memory Leak in Lambda#
// Yanlış - global değişkenlerde veri biriktirme
let cache: any = {}; // Bu Lambda instance'larında memory leak'e neden olur
export const handler = async (event: APIGatewayProxyEvent) => {
cache[event.requestContext.requestId] = event; // Memory leak!
// ...
};
// Doğru - her request için temiz state
export const handler = async (event: APIGatewayProxyEvent) => {
const requestCache = new Map(); // Local scope
// ...
};
Sonuç#
3 yıllık serverless yolculuğum bana şunu öğretti: TypeScript ile AWS Lambda gerçekten güçlü bir kombinasyon, ancak production'da başarılı olmak için doğru pattern'leri ve maliyet optimizasyon tekniklerini bilmek kritik.
Önemli noktalar:
- Memory ve timeout ayarlarını workload'unuza göre optimize edin
- Connection reuse kullanın
- DynamoDB için single-table design öğrenin
- CloudWatch Logs maliyetlerini kontrol edin
- Bundle size'ı küçük tutun
Bu pattern'leri takip ederek, ekibimizin AWS faturasını 60% azalttık ve aynı zamanda performansı artırdık. Serverless'ın öğrenme eğrisi var, ama doğru yapıldığında hem maliyet hem de performans açısından muazzam faydalar sağlıyor.
Performance Insights from Production Data#
Çeviri eklenecek.
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!