İçeriğe atla

AWS Lambda ve TypeScript: Production İçin En İyi Uygulamalar

AWS Lambda, API Gateway ve TypeScript ile production-ready serverless projeleri oluşturmanın en iyi uygulamaları, maliyet ve performans ipuçları.

Ayhan Sipahi Ayhan Sipahi

EC2 üzerinde çalışan geleneksel bir Express.js API’si sabit maliyet, öngörülebilir ölçeklendirme ve %99.9 uptime sunar. Lambda’ya geçiş kararı genellikle belirli bir uyumsuzluktan doğar: ayda bir kez, 10 dakikadan kısa sürede 50.000 webhook işlemesi gereken bir özellik.

Aylık 10 dakikalık bir yoğunluk için EC2 instance’larını 7/24 çalışır tutmak israftır; Lambda bu boşta bekleme maliyetini ortadan kaldırır. Karşılığında farklı bir hata kümesi getirir: hesap genelinde geçerli bir concurrency limiti, kritik yol üzerindeki cold start’lar ve dikkatsiz tek bir DynamoDB Scan’inin çevresindeki tüm compute’tan pahalıya geldiği bir faturalama modeli.

Serverless’a Yaygın İtirazlar#

Serverless’a karşı standart itiraz “ekstra adımlarla vendor lock-in” biçimindedir. Kubernetes cluster’ları yönetmekte ve JVM garbage collector’larını ince ayarlamakta rahat olan ekipler, Lambda’yı kontrolü bırakmak olarak görür. Üç tekrar eden senaryo bu görüşü değiştirme eğilimindedir:

Beklenmedik Trafik Artışı#

Büyük bir teknoloji link toplayıcısında yer alan bir Express API, trafiğin bir gecede dakikada 100 istekten dakikada 5.000 isteğe sıçradığını görebilir. Auto-scaling grupları yeni instance’ları başlatmak için tipik olarak 6-10 dakikaya ihtiyaç duyar. Bu pencerede ödeme işleme hataları birikir ve Redis cache’leri aşırı yüklenir.

Lambda’da başlatılacak bir instance yoktur. Tek bir fonksiyonun concurrency’si her 10 saniyede 1.000 execution environment’a kadar artar; bu da o trafik eğrisini kapasite kararı vermeden karşılar.

Webhook İşleme Zorluğu#

10.000’den fazla event’in patlamalar halinde geldiği Stripe webhook’larını işlemek, EC2’nin iki kötü seçeneğini açığa çıkarır:

  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 bu seçimi ortadan kaldırır. Her webhook kendi execution environment’ını alır; ayarlanacak bir queue ve patlamalar arasında ödenecek boş kapasite kalmaz.

Boşta Duran Kapasiteye Ödeme#

Sürekli açık API sunucularında yapılan kullanım incelemeleri, filonun günün büyük bölümünde boşta durduğunu, faturanınsa 24 saat boyunca tam kapasiteyi kapsadığını gösterir. Lambda çalışılan her milisaniye için faturalandırır; boşta geçen süre hiçbir şeye mal olmaz.

Production CDK Stack’i#

Trafik gerçek olduğunda önem kazanmaya başlayan ayarlarla bir CDK stack’i:

// Production CDK stack
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, MethodLoggingLevel } from 'aws-cdk-lib/aws-apigateway';
import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
import { Runtime, Tracing } 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
    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 trafik spike'larini karsilar
      // Point-in-time recovery kazara silmeye karsi korur
      pointInTimeRecovery: true,
      removalPolicy: RemovalPolicy.RETAIN,  // Prod verisini asla yanlislikla silme
    });

    // Farkli erisim pattern'leri icin GSI ekle
    dataTable.addGlobalSecondaryIndex({
      indexName: 'GSI1',
      partitionKey: { name: 'GSI1PK', type: AttributeType.STRING },
      sortKey: { name: 'GSI1SK', type: AttributeType.STRING },
    });

    // Production'a hazir ayarlarla Lambda fonksiyonu
    const apiHandler = new NodejsFunction(this, 'ApiHandler', {
      entry: 'src/handlers/api.ts',
      runtime: Runtime.NODEJS_20_X,
      // Memory, gerçek workload'ın profiling'ine göre ayarlandı
      memorySize: 1024,  // JSON işleyen bir handler için başlangıç noktası
      timeout: Duration.seconds(28),  // API Gateway'in 29s limitinin hemen altinda
      environment: {
        TABLE_NAME: dataTable.tableName,
        NODE_ENV: 'production',
        // Ozel env vars
        LOG_LEVEL: 'info',
        ENABLE_X_RAY: 'true',
      },
      bundling: {
        minify: true,
        target: 'node20',
        // aws-sdk'yi bundle'dan haric tut - Lambda runtime sagliyor
        externalModules: ['@aws-sdk/*'],
        // Kullanilmayan kodu tree-shake et
        treeShaking: true,
        // Prod sorunlarini debug etmek icin source maps
        sourceMap: true,
        // Dead code elimination icin define
        define: {
          'process.env.NODE_ENV': '"production"',
        },
      },
      // Debug icin X-Ray tracing'i etkinlestir
      tracing: Tracing.ACTIVE,
      // Lambda'nin tum hesap limitini tuketmesini onlemek icin reserved concurrency
      reservedConcurrentExecutions: 100,
    });

    // DynamoDB izinlerini ver
    dataTable.grantReadWriteData(apiHandler);

    // Dogru CORS ve throttling ile API Gateway
    const api = new RestApi(this, 'ServerlessApi', {
      restApiName: 'production-serverless-api',
      description: 'Production serverless API with proper error handling',
      defaultCorsPreflightOptions: {
        allowOrigins: process.env.NODE_ENV === 'production'
          ? ['https://yourdomain.com']
          : Cors.ALL_ORIGINS,
        allowMethods: Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date'],
      },
      deployOptions: {
        // Stage'e ozel throttling
        throttlingRateLimit: 1000,
        throttlingBurstLimit: 2000,
        // Detayli CloudWatch metriklerini etkinlestir
        metricsEnabled: true,
        loggingLevel: MethodLoggingLevel.INFO,
        // X-Ray tracing'i etkinlestir
        tracingEnabled: true,
      },
    });

    // Dogru integration ile resource ekle
    const items = api.root.addResource('items');
    items.addMethod('GET', new LambdaIntegration(apiHandler));
    items.addMethod('POST', new LambdaIntegration(apiHandler));

    const singleItem = items.addResource('{id}');
    singleItem.addMethod('GET', new LambdaIntegration(apiHandler));
    singleItem.addMethod('PUT', new LambdaIntegration(apiHandler));
    singleItem.addMethod('DELETE', new LambdaIntegration(apiHandler));
  }
}

Production Lambda Handler’ı#

Handler tarafı; bu hata modlarının gerektirdiği client kurulumu ve error handling ile:

// src/handlers/api.ts
import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';

// Connection reuse icin DynamoDB client'i handler disinda olustur
const dynamoClient = new DynamoDBClient({
  region: process.env.AWS_REGION,
  // Maliyet verimliligi icin connection pooling ayarlari
  maxAttempts: 3,
  requestHandler: {
    connectionTimeout: 1000,
    socketTimeout: 1000,
  },
});

const docClient = DynamoDBDocumentClient.from(dynamoClient, {
  marshallOptions: {
    removeUndefinedValues: true,  // DynamoDB validation hatalarini onler
    convertEmptyValues: false,
  },
});

interface Item {
  id: string;
  name: string;
  description?: string;
  createdAt: string;
  updatedAt: string;
}

// Yuksek hacimli istekleri isleyen handler
export const handler: APIGatewayProxyHandler = async (event): Promise<APIGatewayProxyResult> => {
  // Performans optimizasyonu: bir kez parse et, her yerde kullan
  const { httpMethod, pathParameters, body, requestContext } = event;
  const requestId = requestContext.requestId;

  // Incident sirasinda gercekten yardimci olan yapilandirilmis 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) {
    // Hatayı sınırda bir kez, ilişkilendirme için request ID ile logla
    console.error('Handler error', {
      requestId,
      error: error.message,
      stack: error.stack,
      // Temizlenmis istek verisi (hassas bilgiyi asla loglama)
      method: httpMethod,
      path: event.path,
    });

    // Hata tipine gore farkli hata yanitlari
    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' });
    }

    // Beklenmeyen sorunlar icin genel sunucu hatasi
    return createResponse(500, {
      error: 'Internal server error',
      requestId,  // Destek talepleri icin ekle
    });
  }
};

async function handleGet(id: string | undefined, requestId: string): Promise<APIGatewayProxyResult> {
  if (!id) {
    // Tum item'lari pagination ile listele
    const result = await docClient.send(new QueryCommand({
      TableName: process.env.TABLE_NAME!,
      KeyConditionExpression: 'PK = :pk',
      ExpressionAttributeValues: {
        ':pk': 'ITEM',
      },
      Limit: 50,  // Timeout'a yol acan buyuk scan'leri onle
    }));

    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 });
  }

  // Tek item al
  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 });
  }

  const item: Item = {
    id: result.Item.SK.replace('ITEM#', ''),
    name: result.Item.name,
    description: result.Item.description,
    createdAt: result.Item.createdAt,
    updatedAt: result.Item.updatedAt,
  };

  return createResponse(200, { item, 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 (error) {
    return createResponse(400, { error: 'Invalid JSON', requestId });
  }

  // Hatalı girdiyi DynamoDB'ye ulaşmadan reddet
  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 });
  }

  if (data.name.length > 100) {
    return createResponse(400, { error: 'Name must be 100 characters or less', requestId });
  }

  const id = generateId();  // Ozel ID uretimi
  const now = new Date().toISOString();

  const item: Item = {
    id,
    name: data.name.trim(),
    description: data.description?.trim() || undefined,
    createdAt: now,
    updatedAt: now,
  };

  // Composite key'lerle single-table design
  await docClient.send(new PutCommand({
    TableName: process.env.TABLE_NAME!,
    Item: {
      PK: 'ITEM',
      SK: `ITEM#${id}`,
      ...item,
      // Alternatif erisim pattern'leri icin GSI key'leri
      GSI1PK: 'ITEMS_BY_NAME',
      GSI1SK: item.name.toLowerCase(),
    },
    // Mevcut item'larin uzerine yazmayi onle
    ConditionExpression: 'attribute_not_exists(PK)',
  }));

  console.log('Item created', { requestId, itemId: id });

  return createResponse(201, { item, requestId });
}

// Tutarli yanitlar icin utility fonksiyonu
function createResponse(statusCode: number, body: any): APIGatewayProxyResult {
  return {
    statusCode,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',  // Production icin ayarla
      'Access-Control-Allow-Headers': 'Content-Type,Authorization',
      'X-Request-ID': body.requestId || 'unknown',
    },
    body: JSON.stringify(body),
  };
}

// URL-safe benzersiz ID'ler uret
function generateId(): string {
  return `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`;
}

Maliyet Optimizasyon Kalıpları#

1. Memory vs. CPU Dengeleri#

Lambda, CPU’yu ayarlanan memory ile orantılı olarak tahsis eder; yani memory ayarı aslında bir hız kadranıdır. Memory’yi iki katına çıkarmak süreyi çoğu zaman yarıya indirir, bu da fonksiyon hızlanırken faturayı sabit tutar. İşin CPU-bound olmaktan çıktığı noktadan sonra süre yataylaşır ve fazladan memory saf maliyete dönüşür.

Bu noktanın nerede olduğu handler’a göre değişir. AWS Lambda Power Tuning (yeni sekmede açılır), tek bir fonksiyonu gerçek bir payload ile farklı memory ayarlarında çalıştırır ve maliyeti süreye karşı grafiğe döker. 1024 MB makul bir başlangıçtır; bunu doğrulayan şey ise tarama olur.

2. Connection Reuse#

Her TLS handshake’i faturalanan milisaniyelere mal olur ve kısa DynamoDB çağrılarında handshake, işlemin kendisinden daha ağır basabilir. AWS SDK for JavaScript v3, TCP bağlantılarını varsayılan olarak yeniden kullanır; burada belirleyici olan, client’ın nerede oluşturulduğudur:

// Handler içinde: her invocation'da yeni client, yeni handshake
export const handler = async () => {
  const client = new DynamoDBClient({});
  // ...
};

// Handler dışında: execution environment başına tek client, sıcak kaldıkça yeniden kullanılır
const dynamoClient = new DynamoDBClient({
  region: process.env.AWS_REGION,
  maxAttempts: 3,
  requestHandler: {
    connectionTimeout: 1000,
    socketTimeout: 1000,
  },
});

AWS_NODEJS_CONNECTION_REUSE_ENABLED=1 hâlâ birçok örnekte karşınıza çıkar. Bu bir v2 ayarıdır; keep-alive zaten açık olduğu için v3 onu yok sayar.

3. Bundle Size Optimizasyonu#

Runtime, handler’ın ilk satırı çalışmadan önce deployment paketini indirip açar; bu yüzden bundle boyutu doğrudan cold start süresine yansır:

bundling: {
  minify: true,
  target: 'node20',
  externalModules: [
    '@aws-sdk/*',  // Node.js runtime tarafından sağlanır
  ],
  treeShaking: true,
  sourceMap: process.env.NODE_ENV !== 'production',  // Debug bilgisi sadece dev'de
  define: {
    'process.env.NODE_ENV': '"production"',
  },
  banner: '/* Production Lambda bundle */',
}

Tree-shaking yalnızca import buna izin verdiğinde işe yarar. CommonJS bir yardımcı kütüphanenin default import’u paketin tamamını içeri alır; submodule import’u tek bir fonksiyonu alır:

// Kütüphanenin tamamını içeri alır
import _ from 'lodash';

// Tek bir fonksiyonu içeri alır
import throttle from 'lodash/throttle';

4. CloudWatch Logs Hacmi#

CloudWatch Logs alımı gigabayt başına faturalanır. Yüksek hacimli info loglaması faturayı tek başına şişirebilir. LOG_LEVEL üzerinden anahtarlanan yapısal bir logger, hata ve uyarıları her zaman görünür tutarken production ortamında ayrıntılı info çıktısını bastırır:

// Error ve warn her zaman yazılır; info yalnızca info/debug seviyesinde, böylece LOG_LEVEL=warn onu susturur
const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 } as const;
const threshold = LEVELS[(process.env.LOG_LEVEL as keyof typeof LEVELS) ?? 'info'] ?? LEVELS.info;
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 (threshold >= LEVELS.info) {
      console.log(JSON.stringify({ level: 'info', message, meta, timestamp: new Date().toISOString() }));
    }
  },
};

5. DynamoDB Faturalama Modu#

Faturalama modu, trafik şekline bağlı bir maliyet kaldıracıdır. On-demand (PAY_PER_REQUEST) öngörülemeyen ani yükleri kapasite planlaması olmadan karşılar. Provisioned kapasite ise istikrarlı ve öngörülebilir throughput için daha ucuzdur:

// Write-heavy, spike'li workload'lar icin on-demand
const writeHeavyTable = new Table(this, 'WriteHeavyTable', {
  billingMode: BillingMode.PAY_PER_REQUEST, // Spike'larda maliyet etkili
});

// Ongorulebilir workload'lar icin provisioned
const predictableTable = new Table(this, 'PredictableTable', {
  billingMode: BillingMode.PROVISIONED,
  readCapacity: 5,
  writeCapacity: 5,
});

Production Monitoring Kurulumu#

Bir alarm ancak birinin harekete geçeceği durumda işe yarar. Dört alarm, Lambda’nın hata modlarının çoğunu kapsar:

import { Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Alarm, Metric, TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch';
import { Function } from 'aws-cdk-lib/aws-lambda';

export class ServerlessMonitoring extends Construct {
  constructor(scope: Construct, id: string, props: { lambdaFunction: Function }) {
    super(scope, id);

    // Periyot boyunca ortalanan Errors, başarısız invocation oranını verir
    const errorAlarm = new Alarm(this, 'HighErrorRate', {
      metric: props.lambdaFunction.metricErrors({
        statistic: 'Average',
        period: Duration.minutes(5),
      }),
      threshold: 0.05,  // invocation'ların %5'i başarısız
      evaluationPeriods: 2,
      treatMissingData: TreatMissingData.NOT_BREACHING,
    });

    // Sure alarmi - 95. persentil 5 saniyenin uzerinde
    const durationAlarm = new Alarm(this, 'SlowRequests', {
      metric: props.lambdaFunction.metricDuration({
        statistic: 'p95',
        period: Duration.minutes(5),
      }),
      threshold: 5000,  // 5 saniye
      evaluationPeriods: 3,
    });

    // Throttle alarmi - herhangi bir throttling kotudur
    const throttleAlarm = new Alarm(this, 'ThrottledRequests', {
      metric: props.lambdaFunction.metricThrottles({
        statistic: 'Sum',
        period: Duration.minutes(1),
      }),
      threshold: 1,
      evaluationPeriods: 1,
    });

    // Is mantigi hatalari icin ozel metrik
    const businessErrorAlarm = new Alarm(this, 'BusinessLogicErrors', {
      metric: new Metric({
        namespace: 'MyApp/Lambda',
        metricName: 'BusinessErrors',
        statistic: 'Sum',
      }),
      threshold: 10,
      evaluationPeriods: 2,
    });
  }
}

Yaygın Production Hataları#

1. Concurrent Execution Limit Sorunu#

Concurrency hesap düzeyinde havuzlanır ve hesaptaki her fonksiyon aynı havuzdan çeker. Bir yük patlamasında webhook işleyen Lambda’lar, varsayılan kotadaki bir hesabın 1.000 concurrent execution’ının tamamını tüketebilir. Kullanıcıya bakan API ise geriye kapasite kalmadığı için hata vermeye başlar.

Çözüm: Kritik fonksiyonlarda reserved concurrency ayarla:

reservedConcurrentExecutions: 100,  // Kapasiteyi garanti et

2. DynamoDB Hot Partition Sorunu#

Partition key olarak sıralı ID’ler kullanmak, ardışık yazmaları aynı partition’a gönderir. DynamoDB partition başına throttle uygular; bu yüzden tablo, tablo düzeyindeki kapasite tükenmeden çok önce throttling hatası döndürmeye başlar.

Çözüm: Key uzayına yayılan partition key’leri:

// Kötü: sıralı ID'ler yazmaları tek partition'da toplar
PK: `USER#${sequentialId}`

// İyi: rastgele ID yazmaları yayar ve key ile geri okunabilir
PK: `USER#${randomUUID()}`

3. 15 Dakikalık Yürütme Tavanı#

Tam 15 dakikada durmak tek bir nedeni işaret eder: hiçbir ayarın yükseltmediği maksimum yürütme süresi tavanı. Büyük bir batch’i senkron işlemek, batch bu pencereye sığmayacak kadar büyüdüğü anda tavana dayanır.

Çözüm: Pagination ile batch işleme:

// Daha kucuk parcalar halinde isle
const BATCH_SIZE = 100;
const MAX_EXECUTION_TIME = 14 * 60 * 1000; // 14 dakika
const startTime = Date.now();

for (let i = 0; i < items.length; i += BATCH_SIZE) {
  if (Date.now() - startTime > MAX_EXECUTION_TIME) {
    // SQS uzerinden devami planla
    await scheduleRemainingWork(items.slice(i));
    break;
  }

  const batch = items.slice(i, i + BATCH_SIZE);
  await processBatch(batch);
}

4. DynamoDB Scan Maliyet Tuzağı#

Scan tüm tabloyu okur ve yalnızca eşleşen birkaç kayıt için değil, incelenen her kayıt için faturalandırır. Büyük bir tabloda bu, kayda değer ve tekrar eden bir maliyete dönüşür. Global Secondary Index artı Query, yalnızca eşleşen partition’ı okur:

// Tabloyu baştan sona okur ve incelenen her kayıt için faturalandırır
const getAllUsers = async () => {
  const result = await docClient.send(new ScanCommand({
    TableName: process.env.TABLE_NAME,
  }));
  return result.Items; // Tek bir 1 MB'lık sayfa; tam tarama bu maliyeti tekrarlar
};

// Duzeltme: Query kullan
const getUsersByStatus = async (status: string) => {
  const result = await docClient.send(new QueryCommand({
    TableName: process.env.TABLE_NAME,
    IndexName: 'GSI1',
    KeyConditionExpression: 'GSI1PK = :pk',
    ExpressionAttributeValues: {
      ':pk': `STATUS#${status}`,
    },
  }));
  return result.Items;
};

5. Lambda Memory Leak#

Sıcak bir Lambda container’ı, modül-global state’i çağrılar arasında yeniden kullanır. Modül seviyesindeki bir nesneye eklenen her şey, container geri dönüştürülene kadar sınırsız büyür ve sonunda belleği tüketir. Request kapsamlı state ise tek bir çağrıyla birlikte yaşar ve sona erer:

// Yanlis - global degiskenlerde veri biriktirme
let cache: any = {}; // Bu Lambda instance'larinda memory leak'e neden olur

export const handler = async (event: APIGatewayProxyEvent) => {
  cache[event.requestContext.requestId] = event; // Memory leak
  // ...
};

// Dogru - her request icin temiz state
export const handler = async (event: APIGatewayProxyEvent) => {
  const requestCache = new Map(); // Local scope
  // ...
};

Production Güvenilirliği için TypeScript Kalıpları#

1. Katı Event Tip Tanımları#

// Daha iyi IntelliSense icin ozel tip tanimlari
interface StrictAPIGatewayEvent extends APIGatewayProxyEvent {
  pathParameters: { [key: string]: string };  // Path parametresi tanimlayan route'larda null olmaz
  body: string;  // POST/PUT icin her zaman mevcut
}

// Runtime guvenligi icin type guard'lar
function isValidItemData(data: any): data is Partial<Item> {
  return typeof data === 'object' &&
         data !== null &&
         (data.name === undefined || typeof data.name === 'string');
}

2. Environment Variable Doğrulaması#

// Zorunlu bir değişken eksikse hemen hata ver
interface Environment {
  TABLE_NAME: string;
  LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error';
  NODE_ENV: 'development' | 'production';
}

function validateEnvironment(): Environment {
  const env = process.env;

  if (!env.TABLE_NAME) {
    throw new Error('TABLE_NAME environment variable is required');
  }

  return {
    TABLE_NAME: env.TABLE_NAME,
    LOG_LEVEL: (env.LOG_LEVEL as any) || 'info',
    NODE_ENV: (env.NODE_ENV as any) || 'development',
  };
}

// Modul yuklenirken bir kez dogrula
const ENV = validateEnvironment();

3. Hata Yönetimi için Result Type’ları#

// Temiz hata yonetimi icin Rust'tan ilham alinmis Result type
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

async function getItem(id: string): Promise<Result<Item, string>> {
  try {
    const result = await docClient.send(new GetCommand({
      TableName: ENV.TABLE_NAME,
      Key: { PK: 'ITEM', SK: `ITEM#${id}` },
    }));

    if (!result.Item) {
      return { success: false, error: 'Item not found' };
    }

    return { success: true, data: transformDynamoItem(result.Item) };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

// Kullanim
const result = await getItem(id);
if (!result.success) {
  return createResponse(404, { error: result.error });
}
// TypeScript result.data'nin Item oldugunu biliyor
const item = result.data;

Sayılar Nereden Gelir#

Yayımlanan cold start ve maliyet rakamları workload’lar arasında nadiren taşınır; çünkü bundle boyutuna, runtime’a, memory ayarına ve payload şekline bağlıdırlar. Belirli bir fonksiyonun sayıları üç kaynaktan gelir:

  • Cold start’lar: CloudWatch Logs’taki REPORT satırı, her soğuk invocation için Init Duration taşır. Bu alanı Logs Insights’ta sorgulamak, kuyruğu gizleyen bir ortalama yerine dağılımın kendisini verir.
  • Süre ve limitler: Duration, ConcurrentExecutions ve Throttles metrikleri, yavaş bir handler ile concurrency limitinin arkasında kuyruğa giren bir fonksiyonu birbirinden ayırır.
  • Maliyet dağılımı: Servise göre gruplanmış Cost Explorer, faturayı Lambda compute, API Gateway istekleri, DynamoDB ve CloudWatch Logs alımı arasında böler. Ekipleri en sık şaşırtan kalem log alımıdır.

Bu ölçümleri herhangi bir ayarı denemeden önce alın ki değişikliğin aşması gereken bir referans noktası olsun.

Serverless’ın Sınırları#

Serverless her zaman doğru araç değildir. Container’lar şu durumlar için daha iyi seçenek olmayı sürdürür:

  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 uygulamaları
  3. Legacy uygulamalar - Karmaşık deployment gereksinimleri
  4. Stateful workload’lar - In-memory cache’ler, session’lar
  5. Cold start hassas - Sub-100ms yanıt gereksinimleri

Deployment Pipeline’ı#

Lambda, sunucuları boşaltıp yeniden başlatmak yerine bir alias’ı yeni sürüme yönlendirir; dolayısıyla dağıtımın kesinti maliyeti yoktur. Geriye kalan risk, hatalı bir sürümün tüm çağıranlara aynı anda ulaşmasıdır; aşağıdaki test kapısı ve onay adımı tam olarak bunun içindir:

// CDK pipeline'i: test stage, integration test, onay, ardindan prod
export class ServerlessPipeline extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const pipeline = new CodePipeline(this, 'Pipeline', {
      synth: new ShellStep('Synth', {
        input: CodePipelineSource.gitHub('yourorg/repo', 'main'),
        commands: [
          'npm ci',
          'npm run build',
          'npm run test',
          'npx cdk synth',
        ],
      }),
    });

    // Asamali rollout ile stage deployment'lari
    const testStage = new ServerlessStage(this, 'Test', {
      stageName: 'test',
    });

    const prodStage = new ServerlessStage(this, 'Prod', {
      stageName: 'prod',
    });

    pipeline.addStage(testStage, {
      post: [
        new ShellStep('IntegrationTests', {
          commands: [
            'npm run test:integration',
          ],
          envFromCfnOutput: {
            API_URL: testStage.apiUrl,
          },
        }),
      ],
    });

    pipeline.addStage(prodStage, {
      pre: [
        new ManualApprovalStep('PromoteToProd'),
      ],
      post: [
        new ShellStep('SmokeTests', {
          commands: [
            'npm run test:smoke',
          ],
        }),
      ],
    });
  }
}

Bunun üzerine kademeli bir rollout için fonksiyonun önüne bir alias koyun ve CodeDeploy’un trafiği yeni sürüme adım adım kaydırmasına izin verin; rollback tetikleyicisi olarak bir CloudWatch alarmı kullanın.

Bu Kurulum Nerede İşe Yarar#

TypeScript ile Lambda, trafiğin patlamalı veya öngörülemez olduğu ve her isteğin dakikalar değil saniyeler içinde bittiği durumlarda başvurulacak ilk seçenektir. Bu profilde milisaniye başına faturalama boşta duran instance’lardan ucuza gelir ve concurrency ölçeklendirmesi kapasite planlamasının yerini alır. Bunun dışında, yukarıda sıralanan durumlarda container daha basit cevaptır; trafik düzleştikçe aradaki fark da açılır.

Çalışan bir Lambda ile ucuz bir Lambda arasındaki mesafenin çoğu iki ayara bakar: power tuning taramasıyla bulunan memory boyutu ve CloudWatch alımının faturaya ne kadar yansıyacağını belirleyen log seviyesi. İkisi de tek satırlık değişiklik, ikisi de fonksiyondan gerçek trafik geçtikten sonra yeniden gözden geçirmeye değer.

Kaynaklar#

İlgili yazılar