Skip to content
~/sph.sh

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ı.

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. Production Lambda fonksiyonları oluşturmak, her serverless hatasını yapmak ve AWS faturalarında çok fazla para harcamaktan öğrendiklerimi paylaşıyorum. Bu rehber, maliyet sürprizlerinden kaçınmak ve performansı optimize etmek için somut adımlar sunuyor. CDK stack örnekleri, DynamoDB single-table design ve cold start optimizasyonları dahil—hepsi gerçek production sistemlerinden uyarlanmış. Lambda concurrency limitleri ve throttling'i baştan planlamak, webhook burst'lerinde beklenmedik hataları önler. İlk günden kapsamlı monitoring kurmak, incident sırasında gerçekten işe yarayacak tek şey.

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:

Beklenmedik Trafik Artışı (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 ciddi ödeme işleme hataları yaşamıştık ve Redis cache'imiz aşırı yüklenmişti.

Lambda anında ölçeklenirdi. Bu olay otomatik ölçeklendirmenin değerini vurguladı.

Webhook İşleme Zorluğu (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ı:

  1. Tepe yük için fazla provision (pahalı)
  2. 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.

Compute Kullanım Analizi (Ekim 2022)

Gerçek compute kullanımımızı analiz ettiğimizde, API sunucularımızın zamanın %87'sinde boşta olduğunu, ancak %100 kapasite için ödeme yaptığımızı gördük. Kullanılmayan kaynaklar için aylık maliyetler önemli ölçüde birikiyor.

Lambda'nın milisaniye başına ödeme modeli bu verimsizliği doğrudan çözdü. Idle sürelerde ödeme yapmıyorsun; sadece gerçek execution süresince faturalandırılıyorsun. Bu özellikle düzensiz trafik pattern'leri olan API'ler için önemli.

Production'da Gerçekten İşe Yarayan Stack

Birden fazla yaklaşımı denedikten sonra, işte karar verdiğimiz:

typescript
// Production CDK stack'imiz - acıyla rafine edildiimport { 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,    });  }}

Gerçeği Ele Alan Lambda Handler

İşte production incident'lerden öğrenilen tüm error handling ve optimizasyonlarla birlikte production Lambda handler'ımız:

typescript
// src/handlers/api.tsimport { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';import { DynamoDBClient } from '@aws-sdk/client-dynamodb';import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
// Connection reuse için handler dışında DynamoDB client oluşturconst dynamoClient = new DynamoDBClient({  region: process.env.AWS_REGION,  // Maliyetleri %15 düşüren connection pooling ayarları  maxAttempts: 3,  requestHandler: {    connectionTimeout: 1000,    socketTimeout: 1000,  },});
const docClient = DynamoDBDocumentClient.from(dynamoClient, {  marshallOptions: {    removeUndefinedValues: true,  // DynamoDB validation hatalarını önler    convertEmptyValues: false,  },});
interface Item {  id: string;  name: string;  description?: string;  createdAt: string;  updatedAt: string;}
// Yüksek hacimli istekleri işleyen handlerexport const handler: APIGatewayProxyHandler = async (event): Promise<APIGatewayProxyResult> => {  const { httpMethod, pathParameters, body, requestContext } = event;  const requestId = requestContext.requestId;
  // Incident sırasında gerçekten yardımcı olan yapılandırılmış loglama  console.log('Request received', {    requestId,    method: httpMethod,    path: event.path,    pathParams: pathParameters,    userAgent: event.headers['User-Agent'],    sourceIp: event.requestContext.identity.sourceIp,  });
  try {    switch (httpMethod) {      case 'GET':        return await handleGet(pathParameters?.id, requestId);      case 'POST':        return await handlePost(body, requestId);      case 'PUT':        return await handlePut(pathParameters?.id, body, requestId);      case 'DELETE':        return await handleDelete(pathParameters?.id, requestId);      default:        return createResponse(405, { error: 'Method not allowed' });    }  } catch (error) {    console.error('Handler error', {      requestId,      error: error.message,      stack: error.stack,      method: httpMethod,      path: event.path,    });
    if (error.name === 'ValidationException') {      return createResponse(400, { error: 'Invalid request data' });    }    if (error.name === 'ConditionalCheckFailedException') {      return createResponse(409, { error: 'Resource conflict' });    }    if (error.name === 'ResourceNotFoundException') {      return createResponse(404, { error: 'Resource not found' });    }
    return createResponse(500, {      error: 'Internal server error',      requestId,    });  }};
async function handleGet(id: string | undefined, requestId: string): Promise<APIGatewayProxyResult> {  if (!id) {    const result = await docClient.send(new QueryCommand({      TableName: process.env.TABLE_NAME!,      KeyConditionExpression: 'PK = :pk',      ExpressionAttributeValues: { ':pk': 'ITEM' },      Limit: 50,    }));    const items = result.Items?.map(item => ({      id: item.SK.replace('ITEM#', ''),      name: item.name,      description: item.description,      createdAt: item.createdAt,      updatedAt: item.updatedAt,    })) || [];    return createResponse(200, { items, count: items.length, requestId });  }
  const result = await docClient.send(new GetCommand({    TableName: process.env.TABLE_NAME!,    Key: { PK: 'ITEM', SK: `ITEM#${id}` },  }));
  if (!result.Item) {    return createResponse(404, { error: 'Item not found', requestId });  }
  return createResponse(200, {    item: {      id: result.Item.SK.replace('ITEM#', ''),      name: result.Item.name,      description: result.Item.description,      createdAt: result.Item.createdAt,      updatedAt: result.Item.updatedAt,    },    requestId,  });}
async function handlePost(body: string | null, requestId: string): Promise<APIGatewayProxyResult> {  if (!body) return createResponse(400, { error: 'Request body is required', requestId });
  let data: Partial<Item>;  try {    data = JSON.parse(body);  } catch {    return createResponse(400, { error: 'Invalid JSON', requestId });  }
  if (!data.name || typeof data.name !== 'string' || data.name.trim().length === 0) {    return createResponse(400, { error: 'Name is required and must be a non-empty string', requestId });  }
  const id = generateId();  const now = new Date().toISOString();  const item: Item = {    id,    name: data.name.trim(),    description: data.description?.trim() || undefined,    createdAt: now,    updatedAt: now,  };
  await docClient.send(new PutCommand({    TableName: process.env.TABLE_NAME!,    Item: {      PK: 'ITEM',      SK: `ITEM#${id}`,      ...item,      GSI1PK: 'ITEMS_BY_NAME',      GSI1SK: item.name.toLowerCase(),    },    ConditionExpression: 'attribute_not_exists(PK)',  }));
  return createResponse(201, { item, requestId });}
async function handlePut(id: string | undefined, body: string | null, requestId: string): Promise<APIGatewayProxyResult> {  if (!id || !body) return createResponse(400, { error: 'ID and body required', requestId });  // Update logic - benzer PutCommand ile mevcut item güncelleme  return createResponse(200, { requestId });}
async function handleDelete(id: string | undefined, requestId: string): Promise<APIGatewayProxyResult> {  if (!id) return createResponse(400, { error: 'ID required', requestId });  // DeleteCommand ile silme  return createResponse(200, { requestId });}
function createResponse(statusCode: number, body: any): APIGatewayProxyResult {  return {    statusCode,    headers: {      'Content-Type': 'application/json',      'Access-Control-Allow-Origin': '*',      'Access-Control-Allow-Headers': 'Content-Type,Authorization',      'X-Request-ID': body.requestId || 'unknown',    },    body: JSON.stringify(body),  };}
function generateId(): string {  return `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`;}

Gerçek Sorunlarda Uyarı Veren Monitoring Kurulumu

Çok fazla gereksiz alarmdan sonra production monitoring kurulumumuz:

typescript
// Ağlamayan CloudWatch alarmları - error rate, duration p95, throttleconst errorAlarm = new Alarm(this, 'HighErrorRate', {  metric: lambdaFunction.metricErrors({ statistic: 'Sum', period: Duration.minutes(5) }),  threshold: 0.05,  // %5 hata oranı  evaluationPeriods: 2,  treatMissingData: TreatMissingData.NOT_BREACHING,  // Gece invoke yoksa alarm yok});const throttleAlarm = new Alarm(this, 'ThrottledRequests', {  metric: lambdaFunction.metricThrottles({ statistic: 'Sum', period: Duration.minutes(1) }),  threshold: 1,  // Tek throttle bile alarm  evaluationPeriods: 1,});

TreatMissingData.NOT_BREACHING: Lambda hiç invoke edilmezse (ör. gece) alarm tetiklenmez – false positive yok. Çok fazla alarm gereksiz notification yorgunluğuna yol açar; sadece aksiyon gerektiren metrikleri izleyin.

Maliyet Optimizasyon Dersleri

1. Memory vs Duration Trade-off

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

typescript
// Yanlış - her çağrıda yeni connectionexport const badHandler = async (event: APIGatewayProxyEvent) => {  const dynamoClient = new DynamoDBClient({}); // Bu pahalı!  // ...işlemler};
// Doğru - connection'ı tekrar kullanconst dynamoClient = new DynamoDBClient({}); // Handler dışındaexport const goodHandler = async (event: APIGatewayProxyEvent) => {  // Mevcut connection'ı kullan};

3. Bundle Size Optimizasyonu

typescript
// package.json'da sadece ihtiyacın olanları içe aktarimport { DynamoDBClient } from '@aws-sdk/client-dynamodb'; // Good: Spesifikimport AWS from 'aws-sdk'; // Bad: 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:

typescript
// Log level'ları akıllıca kullanconst 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

typescript
// Provisioned concurrency sadece kritik endpoint'ler içinconst criticalHandler = new NodejsFunction(this, 'CriticalHandler', {  // Sadece payment processing için provisioned concurrency  reservedConcurrencyLimit: 10,});
// Diğerleri için on-demand yeterliconst regularHandler = new NodejsFunction(this, 'RegularHandler', {  // Provisioned concurrency yok = maliyet tasarrufu});

3. DynamoDB Maliyet Optimizasyonu

typescript
// Write-heavy workload'lar için on-demandconst writeHeavyTable = new Table(this, 'WriteHeavyTable', {  billingMode: BillingMode.PAY_PER_REQUEST, // Spike'larda maliyet etkili});
// Predictable workload'lar için provisionedconst predictableTable = new Table(this, 'PredictableTable', {  billingMode: BillingMode.PROVISIONED,  readCapacity: 5,  writeCapacity: 5,});

Hatalardan Öğrenenler

1. DynamoDB Scan Hatası

typescript
// Bu kod önemli maliyetlere neden olduconst 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 kullanconst 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

typescript
// Yanlış - global değişkenlerde veri biriktirmelet 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 stateexport const handler = async (event: APIGatewayProxyEvent) => {  const requestCache = new Map(); // Local scope  // ...};

3. 15 Dakikalık Timeout Keşfi

Lambda max 15 dakika. Uzun Step Functions veya batch job'lar için chunk'lama veya Fargate. Bizim job 22 dakika sürüyordu; 5'er dakikalık chunk'lara böldük.

Production Verisinden Performans Çıkarımları

18 aylık production süreci boyunca detaylı monitoring ile elde ettiğimiz veriler:

Cold Start Analizi

  • Ortalama cold start: 850ms
  • P95 cold start: 1,200ms
  • Bundle size etkisi: 10MB bundle = +400ms cold start
  • Memory etkisi: 1024MB vs 512MB = -200ms cold start

Maliyet Dağılımı (Aylık)

  • Lambda execution: $89/ay (8M invocation)
  • API Gateway: $28/ay (8M request)
  • DynamoDB: $67/ay (pay-per-request)
  • CloudWatch logs: $12/ay
  • Toplam: 196/ay(EC2es\cdeg˘eri196/ay (EC2 eşdeğeri 800/ay ile karşılaştırıldığında)

Güvenilirlik Metrikleri

  • Uptime: %99.97 (EC2'de %99.9'a karşı)
  • Error rate: %0.02 (çoğunlukla client hataları)
  • P95 response time: 180ms

Serverless Ne Zaman Kullanılmamalı

Serverless her zaman cevap değil. Uzun süreli process'ler, websocket ağırlıklı uygulamalar ve cold start hassas senaryolar için container'lar daha uygun. Serverless'e geçmeden önce workload karakteristiğini iyi anla.

Container'da kaldığım durumlar:

  1. Uzun süren süreçler – Video encoding, büyük batch joblar
  2. Websocket ağırlıklı uygulamalar – Gerçek zamanlı oyun, chat
  3. Legacy uygulamalar – Karmaşık deployment gereksinimleri
  4. Stateful workload'lar – In-memory cache, session'lar
  5. Cold start hassas – Sub-100ms yanıt gereksinimleri

Kırılmayan Deployment Pipeline'ı

CodePipeline ile zero-downtime deployment. Synth: npm ci, build, test, cdk synth. Test ve Prod stage'leri. Integration testleri post-step. Prod için ManualApproval, smoke testleri.

Sonuç

TypeScript ile AWS Lambda, ekibimizin özellik geliştirme sürecini dönüştürdü. Haftalık deployment'lardan günlük deployment'lara geçtik. AWS maliyetlerimiz önemli ölçüde düştü. Uptime'ımız %99.97'ye yükseldi.

En büyük kazanım? Azaltılmış operasyonel yük. Daha az sunucu çökmesi acil çağrısı, minimal kapasite planlaması ve işletim sistemi yaması yok.

Serverless öğrenme eğrisi diktir, ancak üretkenlik kazanımları ölçülebilir. Küçük başlayın, ilk günden kapsamlı monitoring uygulayın ve öğrenme sürecinde hatalar yapmayı bekleyin. CDK ile infrastructure as code kullanmak, manuel konsol işlemlerinden çok daha güvenli ve tekrarlanabilir.

Başlamaya hazır mısınız? Basit bir CRUD API ile başlayın, ilk günden düzgün monitoring ekleyin ve platformun özelliklerini öğrenirken kademeli olarak oluşturun.

İlgili Yazılar