İçeriğe atla

AWS Lambda Maliyet Optimizasyonu: VPC, Layers ve Advanced Pattern'ler

Advanced AWS Lambda pattern'leri ve maliyet optimizasyonu: Lambda Layers, VPC konfigürasyonu, cross-account execution ve mimari kararlar.

Ayhan Sipahi Ayhan Sipahi

Serverless faturaları ilk ayda kimseyi şaşırtmaz. Ölçek büyüdükçe şaşırtır. Fazla ayrılmış memory, boşta duran provisioned concurrency ve monolitik bir handler aynı invocation sayısını her ay yeniden çarpar; Lambda’nın invocation başına fiyatlandırması bu üç hatayı yapması ucuz, taşıması pahalı hale getirir.

Birkaç düzine function’dan sonra sorular değişir. Cold start’ı bozmadan dependency nasıl paylaşılır, VPC bağlantısı ne zaman maliyetine değer, hesaplar arası erişim nasıl güvenli kurulur ve fatura aslında nereye gider? Son sorunun yanıtı gösterişsiz: başka hiçbir şeye dokunmadan önce memory ve concurrency ayarlarını CloudWatch verisiyle denetleyin, çünkü konfigürasyon kayması genellikle kod seviyesindeki optimizasyonun kazandırdığından fazlasına mal olur.

Lambda Layer’lar: Basit Kod Paylaşımının Ötesinde#

Layer’ların Gerçekten Mantıklı Olduğu Durumlar#

Çoğu Lambda Layer tutorial’ı Layer’ları function’lar arası kod paylaşımı olarak anlatır; oysa bu, Layer’a başvurmak için en zayıf gerekçedir. Layer’lar asıl olarak ağır ve nadiren değişen dependency’leri tek yerde sabitlerken işe yarar: bir monitoring SDK’sı, bir veritabanı sürücüsü, custom bir runtime. İş mantığı ise function paketinde kalmalı; orada kendisini kullanan handler ile birlikte versiyonlanır ve geri alınır.

Değişim sıklığına göre ayırma:

// Layer 1: Ağır, nadiren değişen dependency'ler
// Layer'da /opt/nodejs/package.json
{
  "dependencies": {
    "@aws-sdk/client-dynamodb": "^3.400.0",
    "datadog-lambda-js": "^8.67.0",
    "pino": "^8.15.0"
  }
}

// Function kodu layer dependency'lerini kullanır
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; // Layer'dan
import { datadogLambda } from 'datadog-lambda-js';  // Layer'dan
import pino from 'pino';  // Layer'dan

// Function'a özel kod (layer'da değil)
import { validateUserInput } from './validation';  // Function'a özel
import { processPayment } from './payment';  // Function'a özel

Layer versiyonlama:

// Layer yönetimi için CDK stack
export class SharedLayerStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    // Layer'lar için semantic versioning
    const monitoringLayer = new LayerVersion(this, 'MonitoringLayer', {
      code: Code.fromAsset('layers/monitoring'),
      compatibleRuntimes: [Runtime.NODEJS_20_X],
      description: `Monitoring Layer v2.1.0 - ${new Date().toISOString()}`,
      layerVersionName: 'monitoring-layer-v2-1-0'
    });

    // Cross-stack kullanım için ARN export et
    new CfnOutput(this, 'MonitoringLayerArn', {
      value: monitoringLayer.layerVersionArn,
      exportName: 'MonitoringLayerV2-1-0'
    });
  }
}

Layer’ların Init Maliyeti#

Layer’lar çalışma anında bedava değildir. AWS’in belgelediğine göre Lambda, extension içeren layer’ları Init aşamasında /opt dizinine açar ve function ile tüm extension’ların açılmış toplam boyutu 250 MB’lık deployment paketi sınırını aşamaz. Aşamanın kendisinin de tavanı var: AWS Init süresini extension init, runtime init ve function init toplamı için 10 saniyeyle sınırlıyor. Bu bütçe tutmazsa Lambda aşamayı ilk invocation’da, bu kez konfigüre edilmiş function timeout’u altında yeniden dener. Bu 10 saniyelik tavan on-demand concurrency için geçerli. Provisioned concurrency ya da SnapStart kullanan function’lar daha uzun bir bütçeyle başlar: 130 saniye ya da konfigüre edilmiş function timeout’undan hangisi büyükse o.

Bu bütçe içinde bir megabyte’ın kaça mal olduğu ise AWS’in yayımladığı bir sayı değil. Adrian Tanasa 2023’te bunun bir ölçümünü yöntemiyle birlikte paylaştı: x86 üzerinde 256 MB’lık bir Node.js 18 function’ı, paketi 128 KB’tan 64 MB’a kadar ikiye katlanarak, her biri soğuk kalsın diye on dakika arayla yapılan 100 invocation ve CloudWatch Logs Insights üzerinden toplanan @initDuration değerleri. Ortalama cold start 1 KB’ta 171 ms iken 64 MB’ta 3,1 saniyeye çıktı. Megabyte başına süre oranı ters bir çan eğrisi çizdi: 1 MB civarında 26 ms/MB ile dibe indi, uçlarda 45 ms/MB’a yaklaştı. Tek bir mühendisin ölçümü üretici garantisi sayılmaz; asıl bulgu eğrinin biçimi, rakamlar fikir verici.

Aynı ölçüm kolay sonuca da izin vermiyor. Birebir aynı byte’lar function paketinden çıkarılıp layer’a taşındığında cold start kısaldı: yaklaşık 2 MB’tan itibaren ölçülebilir biçimde, 64 MB’ta ise kabaca iki saniye. AWS belgeleri belirli runtime’lar için bunun tersini söylüyor. Rust rehberi layer kullanımını önermiyor; gerekçesi, function’ların init aşamasında ek assembly’leri belleğe elle yüklemesinin cold start sürelerini artırması. Bir layer’ın işi kolaylaştırması da zorlaştırması da runtime’a ve layer’ın içeriğine bağlı. Her iki durumda da değişmeyen şu: layer byte’ları init yolunun üzerindedir ve bütçesi orada tutulur.

O byte’lar ölçülebilir; AWS kendi monitoring layer’ı için bunları yayımlıyor. Lambda Insights extension’ının 1.0.404.0 sürümü extension binary’sini yaklaşık 9 MB’tan 5 MB’a, layer zip’ini yaklaşık 3,7 MB’tan 2,5 MB’a, agent’ın memory kullanımını da yaklaşık 11 MB’tan 7 MB’a indirdi. Extension’lar function’ın CPU’sunu, memory’sini ve depolamasını paylaştığı için bir monitoring layer’ı yalnızca init yolunda değil memory ayarında da karşınıza çıkar.

Bütçeyi tutarken ölçeği kendi iş yükünüz üzerinden düşünün. AWS’e göre cold start’lar tipik olarak invocation’ların %1’inden azında görülüyor, süresi 100 ms’nin altından 1 saniyenin üstüne kadar değişiyor ve geliştirme ile test function’larında üretimdekilerden daha sık yaşanıyor. Bu sıklık rakamı düzenli çağrılan function’ları anlatıyor. Seyrek ya da düzensiz çağrılan bir function çok daha sık init’e girer; init yolundaki layer byte’ları da gecikmeyi tam orada belirler. Kendi oranınız zaten loglarda: on-demand concurrency’de bu oran, REPORT satırı Init Duration taşıyan invocation’ların payıdır. Yukarıdaki 3,1 saniye, bilinçli olarak uca taşınmış 64 MB’lık bir paketten geliyor.

Buradan dört kural çıkıyor:

  • Layer’ı ancak birden fazla function’ın ihtiyaç duyduğu durumda ekleyin. Function başına sert sınır beş, ama dördüncüye uzanmak genelde budanması gereken bir dependency grafiğine işaret eder.
  • Layer’ları konuya göre değil, değişim sıklığına göre ayırın. Haftada bir değişen bir layer, dependency sabitleme amacını boşa çıkarır.
  • Layer’lara version ARN’i ile referans verin; böylece bir layer’ın yeniden deploy edilmesi başka bir function’ın dependency ağacını sessizce kaydıramaz.
  • Function’a özel mantığı layer dışında tutun; handler geri alındığında davranışı da geri alınsın.

VPC Konfigürasyonu ve Maliyet Ayak İzi#

VPC Bağlantısının Bugünkü Maliyeti#

VPC’ye bağlı bir function’ın her cold start’ta on saniyelik ENI kurulumu ödediği yönündeki eski tavsiye artık geçerli değil. Lambda bugün Hyperplane ENI kullanıyor. Her benzersiz subnet ve security group kombinasyonu için bir ENI oluşturuluyor ve bu ENI, aynı kombinasyonu kullanan tüm execution environment’lar arasında paylaşılıyor; yani kurulum maliyeti cold start başına değil, kombinasyon başına bir kez ödeniyor. Warm invocation’lar zaten hiçbir zaman etkilenmiyordu.

VPC bağlantısının hâlâ maliyeti olan tarafı para ve erişilebilirlik. VPC içindeki bir function, siz eklemedikçe public internete çıkamaz. Yaptığı her AWS API çağrısı ya bir NAT Gateway ya da bir VPC endpoint ister; “her şeyi private yapalım” kararının yinelenen bir bütçe kalemine dönüştüğü yer de burasıdır.

Bir function’ı VPC’ye ancak private bir kaynağa ihtiyacı varsa bağlayın: bir RDS cluster’ı, bir ElastiCache node’u, private bir load balancer arkasındaki iç servis. Güvende hissetmek için bağlamayın; VPC dışındaki bir function da AWS API’lerine kimlik doğrulamalı TLS endpoint’leri üzerinden ulaşır.

Minimal bir Lambda VPC konfigürasyonu:

# Lambda için optimize edilmiş CDK VPC kurulumu
VpcConfig:
  SecurityGroupIds:
    - !Ref LambdaSecurityGroup
  SubnetIds:
    - !Ref PrivateSubnet1
    - !Ref PrivateSubnet2
    # Anahtar: Farklı AZ'lerde birden fazla subnet kullan

# Minimal gerekli erişimli security group
LambdaSecurityGroup:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: Lambda function security group
    VpcId: !Ref Vpc
    SecurityGroupEgress:
      # Sadece kesinlikle gerekli olanlar
      - IpProtocol: tcp
        FromPort: 5432
        ToPort: 5432
        CidrIp: 10.0.0.0/16  # Sadece database subnet'i
      - IpProtocol: tcp
        FromPort: 443
        ToPort: 443
        CidrIp: 0.0.0.0/0  # AWS API çağrıları için HTTPS

Keep-Warm Zamanlamaları ve Sınırları#

Zamanlanmış ping’ler, Hyperplane öncesinde VPC cold start’ları için standart geçici çözümdü ve pattern hâlâ üretim kodunun her yerinde duruyor:

// Tek bir execution environment'ı açık tutan zamanlanmış ping
const keepWarmSchedule = new Rule(this, 'KeepVpcLambdaWarm', {
  schedule: Schedule.rate(Duration.minutes(5)),
  targets: [new LambdaFunction(vpcLambdaFunction, {
    event: RuleTargetInput.fromObject({ 
      source: 'keep-warm',
      warmup: true 
    })
  })]
});

// VPC function'ları için handler optimizasyonu
export const handler = async (event: any) => {
  // Warmup event'lerini handle et
  if (event.source === 'keep-warm') {
    return { statusCode: 200, body: 'Staying warm' };
  }
  
  // Asıl mantığın
  return processBusinessLogic(event);
};

Bu yöntem zamanlanmış invocation başına tek bir execution environment’ı açık tutar; zayıf tarafı da tam olarak budur. Tek bir ping, trafik sıçramasının yarattığı yüzüncü eşzamanlı environment’ı ısıtamaz ve warmup dalının her handler’da bakımı gerekir. Provisioned concurrency aynı işi belgelenmiş bir garanti ve okunabilir bir faturayla yapar; zamanlanmış ping’i yalnızca öngörülebilir ve tek haneli eşzamanlılıkta seyreden trafik için tutun.

Private yolun faturaya eklediği kalemler:

  • S3 ve DynamoDB için gateway endpoint’ler: saatlik ücret de veri işleme ücreti de yok.
  • Interface endpoint’ler (PrivateLink): availability zone başına saatlik ücret ve gigabyte başına işleme ücreti. Ek AZ’ler bu tutarı çarpar.
  • NAT Gateway: saatlik ücret ve gigabyte başına işleme ücreti; AWS API’lerine giden trafik dahil her byte faturalanır.
  • Hyperplane ENI’ler faturalanmaz, ama subnet IP adresi tüketirler; bu bir maliyet değil kapasite kısıtıdır.

Yaygın kurulum, iki availability zone’da iki interface endpoint ve bir NAT Gateway’dir. AWS’in VPC için yayımladığı ücretlerle (availability zone başına interface endpoint saati $0.01, NAT Gateway saati $0.045) bu kurulum saatte 4 × $0.01 + $0.045 = $0.085, 730 saat üzerinden ayda yaklaşık $62 eder; üstelik daha hiçbir gigabyte akmadan. Bu taban, function’lar hiç çalışmasa bile durur; yani bir function’ı VPC’ye bağlamak envantere sabit bir aylık maliyet ekler.

Cross-Account Lambda Execution Pattern’leri#

Multi-Account Mimarisi için IAM Stratejisi#

Lambda function’ları birden fazla AWS hesabında yönetmek dikkatli IAM tasarımı gerektirir:

// Cross-account erişim için assume role pattern'i
import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts';
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
import type { AwsCredentialIdentity } from '@aws-sdk/types';

export class CrossAccountExecutor {
  private stsClient: STSClient;
  
  constructor() {
    this.stsClient = new STSClient({});
  }

  async executeInAccount<T>(
    accountId: string,
    roleName: string,
    action: (credentials: AwsCredentialIdentity) => Promise<T>
  ): Promise<T> {
    const response = await this.stsClient.send(new AssumeRoleCommand({
      RoleArn: `arn:aws:iam::${accountId}:role/${roleName}`,
      RoleSessionName: `lambda-cross-account-${Date.now()}`,
      DurationSeconds: 3600,
      // Hedef roldeki sts:ExternalId koşuluyla eşleşmeli
      ExternalId: process.env.CROSS_ACCOUNT_EXTERNAL_ID
    }));

    const issued = response.Credentials!;

    // Credential'ların, ihtiyaç duyan client'a ulaşması gerekir. Bu
    // credential'lar verilmeden kurulan bir client function'ın kendi rolünü
    // kullanmaya devam eder; pattern'in sessiz hata biçimi budur.
    return action({
      accessKeyId: issued.AccessKeyId!,
      secretAccessKey: issued.SecretAccessKey!,
      sessionToken: issued.SessionToken!,
      expiration: issued.Expiration
    });
  }
}

// Kullanım
await executor.executeInAccount('222222222222', 'CrossAccountLambdaRole',
  (credentials) => new DynamoDBClient({ credentials }).send(
    new GetItemCommand({ TableName: 'shared-config', Key: { id: { S: 'billing' } } })
  )
);

Cross-Account Resource Access Pattern’i#

# Cross-account Lambda execution için IAM role
CrossAccountExecutionRole:
  Type: AWS::IAM::Role
  Properties:
    RoleName: CrossAccountLambdaRole
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Principal:
            AWS: 
              - arn:aws:iam::ACCOUNT-A:role/LambdaExecutionRole
              - arn:aws:iam::ACCOUNT-B:role/LambdaExecutionRole
          Action: sts:AssumeRole
          Condition:
            StringEquals:
              'sts:ExternalId': 'unique-external-id-per-partner'
    ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
    Policies:
      - PolicyName: CrossAccountAccess
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - dynamodb:GetItem
                - dynamodb:PutItem
                - s3:GetObject
                - s3:PutObject
              Resource:
                - arn:aws:dynamodb:*:*:table/shared-*
                - arn:aws:s3:::shared-bucket/*

Advanced Dependency Yönetimi ve Güvenlik#

CI/CD’de Dependency Scanning#

Lambda deployment paketleri, kimsenin elle gözden geçirebileceğinden daha hızlı transitive dependency biriktirir. Kontrolün yeri CI’dır; orada bir merge’ü bloklar, üç ayda bir yapılan denetimde ortaya çıkmaz:

# GitHub Actions workflow
name: Lambda Security Scan
on:
  push:
    paths: 
      - 'lambda/**'
      - 'package*.json'

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Node.js Security Audit
        run: |
          npm audit --audit-level moderate
          npm audit fix --dry-run
          
      - name: Dependency Vulnerability Scan
        uses: securecodewarrior/github-action-add-sarif@v1
        with:
          sarif-file: 'security-scan-results.sarif'
          
      - name: Check for Secrets
        uses: trufflesecurity/trufflehog@v3.34.0
        with:
          path: ./
          base: main
          head: HEAD

Runtime Güvenlik Pattern’leri#

// Güvenli environment variable handling
export class SecureConfig {
  private static instance: SecureConfig;
  private config: Map<string, string> = new Map();
  
  private constructor() {
    this.loadConfig();
  }
  
  public static getInstance(): SecureConfig {
    if (!SecureConfig.instance) {
      SecureConfig.instance = new SecureConfig();
    }
    return SecureConfig.instance;
  }
  
  private loadConfig() {
    // Runtime'da Parameter Store'dan yükle
    const requiredParams = [
      'DB_CONNECTION_STRING',
      'API_KEY',
      'JWT_SECRET'
    ];
    
    // Tüm gerekli parameter'ların var olduğunu doğrula
    const missingParams = requiredParams.filter(
      param => !process.env[param]
    );
    
    if (missingParams.length > 0) {
      throw new Error(`Eksik gerekli parameter'lar: ${missingParams.join(', ')}`);
    }
    
    requiredParams.forEach(param => {
      this.config.set(param, process.env[param]!);
    });
  }
  
  public get(key: string): string {
    const value = this.config.get(key);
    if (!value) {
      throw new Error(`Configuration key '${key}' bulunamadı`);
    }
    return value;
  }
}

Lambda Faturası Aslında Nereye Gidiyor#

Maliyeti Function’lara Dağıtmak#

Cost Explorer varsayılan olarak “hangi servis” sorusunu yanıtlar. “Hangi function” sorusunu ise, her function Billing konsolunda etkinleştirilmiş bir cost allocation tag’i taşımadıkça yanıtlayamaz:

import {
  CostExplorerClient,
  GetCostAndUsageCommand
} from '@aws-sdk/client-cost-explorer';

// Cost allocation tag'ine göre gruplamak, tek bir Lambda kalemini function
// bazlı bir dağılıma çevirir. Tag'siz function'lar tek kovada toplanır.
export async function lambdaCostByFunction(start: string, end: string) {
  const client = new CostExplorerClient({});

  const response = await client.send(new GetCostAndUsageCommand({
    TimePeriod: { Start: start, End: end },  // YYYY-MM-DD, End hariç
    Granularity: 'MONTHLY',
    Metrics: ['UnblendedCost'],
    Filter: { Dimensions: { Key: 'SERVICE', Values: ['AWS Lambda'] } },
    GroupBy: [{ Type: 'TAG', Key: 'function-name' }]
  }));

  return (response.ResultsByTime ?? []).flatMap(period =>
    (period.Groups ?? []).map(group => ({
      period: period.TimePeriod?.Start,
      // TAG grupları "tagKey$tagValue" biçiminde döner
      function: group.Keys?.[0]?.split('$')[1] || 'untagged',
      cost: Number(group.Metrics?.UnblendedCost?.Amount ?? 0)
    }))
  );
}

Kullanımı iki sınır belirliyor. Cost Explorer API çağrı başına faturalanır ve verisi gerçek kullanımın bir güne kadar gerisinden gelir. Aylık gözden geçirmeye uygundur, deploy sırasındaki geri besleme döngüsüne değil.

Önce Bakılması Gereken Üç Etken#

1. Bir kez ayarlanıp bir daha dönülmeyen memory. Her Lambda REPORT satırı hem Memory Size hem Max Memory Used değerini taşır; AWS’in best practices sayfası da bu karşılaştırmayı Memory Size: 128 MB Max Memory Used: 18 MB örnek satırı üzerinden anlatır. Tam bir trafik döngüsü boyunca tepe kullanımı ayrılan miktarın çok altında kalan bir function, hiç dokunmadığı GB-saniyeler için ödeme yapar. Tuzak şu: tersi de en az o kadar yaygındır, çünkü memory aynı zamanda CPU payı satın alır. Lambda Power Tuning projesi kendi README’sinde iki yönü de kayda geçiriyor: bir iş yükü 128 MB’ta 35 saniye sürerken 1,5 GB’ta 3 saniyenin altına iniyor ve %14 daha ucuza çalışıyor; bir diğeri 128 MB’ta 2,4 saniyeden 1 GB’ta 300 ms’ye düşerken ortalama maliyeti aynı kalıyor. Hangi sonucun çıkacağı koda bakarak tahmin edilemez; AWS’in bu aracı adıyla önermesinin nedeni de bu.

Compute Optimizer aynı soruyu CloudWatch geçmişinden yanıtlar; boş bir sonucu “sorun yok” diye okumadan önce kör noktalarını bilmekte fayda var. AWS, Lambda önerilerini yalnızca 1.792 MB veya altında konfigüre edilmiş ve son 14 günde en az 50 kez çağrılmış function’lar için üretiyor. Bu memory değerinin üstünde bulgu nedeni Inconclusive, bu çağrı sayısının altında Insufficient data oluyor; Unavailable bulgusu alan function’lar ise konsolda hiç görünmüyor. Yanlış boyutlanmış olma ihtimali en yüksek function’lar çoğu zaman aracın yargılamaktan kaçındığı function’lardır.

Bir incelik daha, ama ne kadar uzağa taşıdığı sınırlı. Tanasa’nın denemeleri 256 MB ile 6 GB arasındaki tahsislerde cold start’ta iyileşme bulmadı. O function’ın init’ine paketin açılması ve yüklenmesi hâkimdi; fazladan CPU bunu kısaltmaz. Memory’nin satın aldığı CPU environment’ın tamamı için geçerlidir; init yolu da o environment’ın içinde çalışır. AWS, daha yüksek bir ayarın işe yaradığı yerler arasında import edilen kütüphaneleri ve layer’ları sayıyor. Başlangıçta gerçekten iş yapan bir init yolu, daha fazla memory ile hâlâ hızlanabilir. Hangi durumda olduğunuzu iki farklı tahsiste ölçülen Init Duration belirler.

2. Gerekçesini geride bırakmış provisioned concurrency. AWS mekaniği açıkça yazıyor: environment örneği hiçbir isteği işlemese bile initialization faturalanır; provisioned concurrency sürekli çalışır ve initialization ile invocation maliyetlerinden ayrı faturalanır. Yayımlanmış ücreti bir takvim ayıyla çarpınca taban dört işleme iniyor. GB-saniye başına $0.0000041667 üzerinden 730 saat boyunca tutulan bir provisioned gigabyte $0.0000041667 × 3600 × 730, yani yaklaşık $10.95 eder; daha tek bir istek gelmeden. ARM’de aynı gigabyte, GB-saniye başına $0.0000033334 üzerinden yaklaşık $8.76’dır. Bir lansmandan kalan sabit yükü bulmak için bunu memory boyutu ve konfigüre edilen sayıyla çarpın. Bir kısmı geri gelir: provisioned bir environment’ta süre, on-demand $0.0000166667 yerine GB-saniye başına $0.0000097222 üzerinden faturalanır; yoğun çalışan bir provisioned function tabanının bir bölümünü böyle geri kazanır.

Provisioned concurrency genelde bir lansman için alınır, lansmana göre ve AWS’in önerdiği %10’luk payla boyutlanır, sonra bir daha gözden geçirilmez. Konfigüre edilen sayıyı ProvisionedConcurrencyUtilization metriğiyle karşılaştırın; sıfıra yakın seyreden bir değer, karşılığı olmayan sabit bir aylık ödeme demektir. İşin diğer yarısını loglardan çözmeyi de beklemeyin: Tanasa’nın ölçümü provisioned concurrency 1 ile çalıştı ve isteklerin %16 ile %18’inde yine bir init süresi kaydetti. Bunlar, standart CloudWatch loglarının gerçek bir cold start’tan ayıramadığı asenkron yeniden hazırlıktan geliyor.

3. Dört işi birden yapan tek function. Zamanla birkaç sorumluluğu üstlenen bir handler, her isteğe hepsinin bedelini ödetir. Bütün dependency’ler init sırasında yüklenir; hiçbirine dokunmayan istekler için de. Memory ayarının en ağır dalı karşılaması gerekir, dolayısıyla ucuz dallar pahalı dalın ücretinden faturalanır. Execution role, hangi dal neye ihtiyaç duyuyorsa hepsinin birleşimini taşımak zorundadır; bu da faturayla birlikte etki alanını genişletir. Sorumluluğa göre bölmek her paketi küçültür, her parçanın kendi memory ayarını almasını sağlar ve rolleri daraltır. Aynı zamanda deploy ettiğiniz, izlediğiniz ve yetkilendirdiğiniz yüzeyi çoğaltır; bu yüzden ancak sorumlulukların kaynak profilleri gerçekten farklıysa kazandırır. Bu takasın karşılığı için yayımlanmış bir rakam yok; bir tasarruf öngörmeden önce bu biçimi kendi etiketlenmiş maliyet raporunuzda arayın.

Memory Optimizasyon Otomasyonu#

Bir script’in burada yapabileceği şey aday üretmektir, o adayı uygulamak değil. Memory her iki yönde de CPU payı satın alır. Function’ın hiç ulaşmadığı bir tahsisi kısmak, süreyi kazandırdığı GB-saniyeden pahalıya gelecek kadar uzatabilir ya da bir gecikme hedefini bütçesinin dışına taşırabilir. Aşağıdaki çıktıyı deploy edilecek bir değer değil, Power Tuning’e girecek bir kısa liste olarak görün:

// CloudWatch metriklere dayalı otomatik memory optimizasyonu
export class MemoryOptimizer {
  async optimizeFunction(functionName: string) {
    const metrics = await this.getCloudWatchMetrics(functionName, 30); // 30 gün
    
    const analysis = {
      avgMemoryUsed: metrics.avgMemoryUsed,
      maxMemoryUsed: metrics.maxMemoryUsed,
      currentMemoryAllocated: metrics.currentMemory,
      avgDuration: metrics.avgDuration,
      invocations: metrics.invocations
    };
    
    // Ölçülecek aday tahsis; doğrudan uygulanacak değer değil
    const candidateMemory = this.calculateCandidateMemory(analysis);
    
    if (candidateMemory !== analysis.currentMemoryAllocated) {
      return {
        recommendation: 'BENCHMARK_MEMORY',
        current: analysis.currentMemoryAllocated,
        candidate: candidateMemory,
        savingsIfDurationHolds: this.calculateSavings(analysis, candidateMemory),
        confidence: this.calculateConfidence(metrics)
      };
    }
    
    return { recommendation: 'NO_CHANGE', reason: 'Zaten optimize edilmiş' };
  }
  
  private calculateCandidateMemory(analysis: any): number {
    // Gözlenen tepe kullanımın üzerine %20 pay. Memory aynı zamanda CPU payını
    // belirler; hesaplama yoğun bir function bu ayarda yavaşlayıp pahalılaşabilir.
    // Aşağıdaki sayı Power Tuning'in bittiği değil, başladığı yerdir.
    const memoryWithBuffer = Math.ceil(analysis.maxMemoryUsed * 1.2);

    // Lambda 128 MB ile 10.240 MB arasında 1 MB adımlarla her değeri kabul eder.
    // 3008 MB'da biten eski sabit basamak listesi artık geçerli değil.
    return Math.min(10240, Math.max(128, memoryWithBuffer));
  }
}

Lambda Extensions: Custom Monitoring ve Processing#

Maliyet İzleme Extension’ı Yazmak#

Bir extension, runtime’ın yanında kendi süreci olarak çalışır. Handler’dan önce başlar ve yanıt döndükten sonra da ayakta kalır; bu yüzden koşan bir maliyet toplamını tutmak için makul bir yerdir. Aynı nedenle handler’ın değişkenlerini göremez ve faturalanan süreyi extension sürecindeki bir kronometreden değil Telemetry API’den almak zorundadır. Bu da fazladan bir parça demek. Extension bir HTTP dinleyicisi açar, ardından platform.report kayıtlarının oraya gönderilmesi için abone olur; dinleyicinin, abonelik çağrısı gitmeden önce bağlantı kabul ediyor olması gerekir:

// /opt/extensions/cost-monitor içinde dağıtılır, kendi süreci olarak çalışır
import { createServer } from 'node:http';
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';

const EXTENSION_NAME = 'cost-monitor';
const API = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension`;
const TELEMETRY_API = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2022-07-01/telemetry`;
// Lambda telemetriyi yalnızca sandbox içine gönderir; 9001 portu rezervedir
const LISTENER_HOST = 'sandbox.localdomain';
const LISTENER_PORT = 4243;

class CostMonitoringExtension {
  private cloudWatch = new CloudWatchClient({});
  private functionName = process.env.AWS_LAMBDA_FUNCTION_NAME!;
  private extensionId = '';
  private billedMs = 0;

  async register() {
    const response = await fetch(`${API}/register`, {
      method: 'POST',
      headers: {
        // /opt/extensions altındaki dosya adıyla birebir aynı olmalı
        'Lambda-Extension-Name': EXTENSION_NAME,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ events: ['INVOKE', 'SHUTDOWN'] })
    });

    // Sonraki her çağrı bunu geri göndermeli, yoksa /event/next 403 döner
    this.extensionId = response.headers.get('Lambda-Extension-Identifier')!;
  }

  // Lambda kayıtları dizi halinde buraya POST eder; sunucu, aşağıdaki abonelik
  // gönderilmeden önce dinliyor olmalı
  startTelemetryListener(): Promise<void> {
    return new Promise(resolve => {
      createServer((req, res) => {
        let body = '';
        req.on('data', chunk => { body += chunk; });
        req.on('end', () => {
          for (const event of JSON.parse(body)) {
            if (event.type === 'platform.report') {
              this.recordReport(event.record.metrics.billedDurationMs);
            }
          }
          res.writeHead(200).end();
        });
      }).listen(LISTENER_PORT, LISTENER_HOST, () => resolve());
    });
  }

  // Yukarıdaki register yalnızca yaşam döngüsü olaylarını verir. Faturalanan
  // süre ayrı bir aboneliktir; onu taşıyan kayıt da platform.report'tur.
  async subscribeToTelemetry() {
    await fetch(TELEMETRY_API, {
      method: 'PUT',
      headers: {
        'Lambda-Extension-Identifier': this.extensionId,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        schemaVersion: '2025-01-29',
        types: ['platform'],
        // Küçük buffer daha erken teslim eder, karşılığı daha çok POST
        buffering: { maxItems: 1000, maxBytes: 262144, timeoutMs: 100 },
        destination: {
          protocol: 'HTTP',
          URI: `http://${LISTENER_HOST}:${LISTENER_PORT}`
        }
      })
    });
  }

  // platform.report kaydı başına bir çağrı
  private recordReport(billedDurationMs: number) {
    this.billedMs += billedDurationMs;
  }

  async processEvents() {
    while (true) {
      const response = await fetch(`${API}/event/next`, {
        method: 'GET',
        headers: { 'Lambda-Extension-Identifier': this.extensionId }
      });

      const event = await response.json();

      // Yukarıdaki bloklayan GET, aynı zamanda extension'ın bir sonraki
      // invoke'a hazır olduğunu Lambda'ya bildirir; döngü onu atlayamaz.
      if (event.eventType === 'SHUTDOWN') {
        await this.flush();
        return;
      }
    }
  }

  private async flush() {
    const memoryMB = Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE);

    await this.cloudWatch.send(new PutMetricDataCommand({
      Namespace: 'Lambda/Cost',
      MetricData: [{
        MetricName: 'EnvironmentCost',
        Value: this.estimateCost(this.billedMs, memoryMB),
        Unit: 'None',
        Dimensions: [
          { Name: 'FunctionName', Value: this.functionName },
          { Name: 'MemorySize', Value: String(memoryMB) }
        ]
      }]
    }));
  }

  private estimateCost(billedMs: number, memoryMB: number): number {
    // İlk kademe x86 on-demand ücreti; istek başına ücret ayrı faturalanır
    const gbSeconds = (memoryMB / 1024) * (billedMs / 1000);
    return gbSeconds * 0.0000166667;
  }
}

const extension = new CostMonitoringExtension();
extension.register()
  .then(() => extension.startTelemetryListener())
  .then(() => extension.subscribeToTelemetry())
  .then(() => extension.processEvents());

Handler Sürecinde Structured Logging#

Handler sürecinde log yakalamak, extension yazmaktan farklı bir iştir ve ikisi yeterince sık karıştırılır. console’u sarmalamak, süreçler arası hiçbir kurulum gerektirmeden yapılandırılmış kayıtlar verir; karşılığında yalnızca bu sürecin yazdıklarını görür:

// Otomatik hata işaretleme ile structured logging sarmalayıcısı
class StructuredLogger {
  private logs: any[] = [];
  private functionName: string;
  
  constructor() {
    this.functionName = process.env.AWS_LAMBDA_FUNCTION_NAME!;
    this.setupLogCapture();
  }
  
  private setupLogCapture() {
    // Tüm console.* çağrılarını yakala
    const originalConsole = { ...console };
    
    console.log = (...args) => {
      this.logs.push({
        level: 'INFO',
        timestamp: new Date().toISOString(),
        message: args.join(' '),
        functionName: this.functionName
      });
      originalConsole.log(...args);
    };
    
    console.error = (...args) => {
      this.logs.push({
        level: 'ERROR',
        timestamp: new Date().toISOString(),
        message: args.join(' '),
        functionName: this.functionName,
        alert: true // Anında uyarı için flag
      });
      originalConsole.error(...args);
    };
  }
  
  async flushLogs() {
    if (this.logs.length === 0) return;
    
    // Logging service'e gönder
    await this.sendToLogService(this.logs);
    
    // Hatalar için uyarı gönder
    const errors = this.logs.filter(log => log.alert);
    if (errors.length > 0) {
      await this.sendAlerts(errors);
    }
    
    this.logs = [];
  }
}

Migration Pattern’leri: EC2/ECS’den Lambda’ya#

Bir Servis Taşınmalı mı#

İşe yarayan migration’lar servisi önce ayrıştırır, sonra taşır. Servisin aday olup olmadığına ise üç kontrol karar verir.

İlki kullanım oranı. Günün büyük bölümünde düşük CPU ile boşta duran bir container, hiç kullanmadığı ayrılmış kapasiteye para öder; istek başına faturalandırmanın uyduğu profil tam olarak budur. İkincisi çalışma profili: Lambda’nın 15 dakikalık sınırını düzenli olarak aşan, uzun ömürlü bağlantı tutan veya 10 GB’tan fazla memory isteyen işler kapsam dışıdır. Üçüncüsü trafik şekli; çünkü düz, yüksek ve gün boyu süren bir istek hızı genelde container üzerinde, invocation başına fiyatlandırmadan ucuza gelir.

Ayrıştırma:

// 1. Önce ayrık function'ları extract et
// Monolitik ECS service'den focused Lambda function'lara

// Öncesi: Her şeyi handle eden tek ECS task
class PaymentAPI {
  async processPayment(req: Request) { /* ... */ }
  async validateCard(req: Request) { /* ... */ }
  async sendNotification(req: Request) { /* ... */ }
  async updateInventory(req: Request) { /* ... */ }
}

// Sonrası: Uzmanlaşmış Lambda function'lar
// payment-processor-lambda
export const handler = async (event: PaymentEvent) => {
  return processPayment(event.paymentData);
};

// card-validator-lambda  
export const handler = async (event: CardEvent) => {
  return validateCard(event.cardData);
};

// notification-sender-lambda
export const handler = async (event: NotificationEvent) => {
  return sendNotification(event.notificationData);
};

Asıl Önemli Olan Maliyet Karşılaştırması#

Yalnızca compute fiyatlarını karşılaştırmak farkın büyük kısmını gizler. Defterin container tarafında, trafik sıfırken bile var olan sabit kalemler bulunur: task’ın kendisi, bir load balancer ve çoğunlukla bir NAT Gateway. Lambda tarafında boşta maliyet yoktur, ama istek başına ücret, GB-saniye başına ücret ve önüne konan bileşenin maliyeti eklenir. API Gateway milyon istek başına faturalanır, ALB saatlik bir tabanla birlikte trafiğe bağlı LCU ücreti getirir, Function URL’in ise Lambda fiyat listesinde kendine ait bir kalemi yoktur.

İki tarafa da yayımlanmış ücretleri koyduğunuzda karşılaştırmanın çoğu dört işleme dönüşür. Bir HTTP API arkasında ortalama 200 ms faturalanan süreye sahip 512 MB’lık bir function ile, bir ALB arkasında 0,5 vCPU ve 1 GB’lık tek bir Fargate task’ını karşılaştıralım. Aşağıdaki ücretlerin tamamı AWS fiyat sayfalarındaki us-east-1 liste fiyatlarıdır; bölgeye göre değişir ve zamanla güncellenir.

Lambda, milyon istek başına:

  • İstekler: milyon başına $0.20.
  • Compute: 0,5 GB × 0,2 s = istek başına 0,1 GB-saniye. GB-saniye başına $0.0000166667 (aylık 6 milyar GB-saniyeden sonra kademe inen ilk kademe x86 ücreti) üzerinden bu, milyon başına $1.67 eder.
  • HTTP API: ilk 300 milyon için milyon başına $1.00.
  • Toplam: milyon istek başına yaklaşık $2.87, sıfır trafikte hiçbir şey.

Container, 730 saatlik ay başına:

  • Fargate: (0,5 × vCPU-saati $0.04048) + (1 × GB-saati $0.004445) = saatte $0.024685, yani $18.02.
  • ALB: saatte $0.0225, yani $16.43; trafik geldiğinde üstüne LCU-saati başına $0.008 biner.
  • Toplam: daha tek bir istek gelmeden yaklaşık $34.45.

$34.45’i $2.87’ye bölünce ayda yaklaşık 12 milyon istek, yani sürekli 4,6 istek/saniye çıkıyor. Bu rakam geçiş noktasının alt sınırıdır. Altında container fiyatta kazanamaz, çünkü yalnızca sabit aylık maliyeti bile Lambda faturasını aşar. Üstünde ise soru açık kalır, çünkü hesaba bir kalem daha girer. ALB, trafik gelir gelmez LCU ücreti işletir; AWS de ölçtüğü dört boyuttan hangisi yüksek çıkarsa yalnızca onu faturalar: yeni bağlantılar, aktif bağlantılar, işlenen byte’lar ve kural değerlendirmeleri. Bunların hiçbirini istek sayısı belirlemez; payload boyutu ile bağlantı yeniden kullanımı belirler. Benzer trafiği hâlihazırda taşıyan bir load balancer’ın ConsumedLCUs değerini okuyup container tarafına ekleyin; geçiş noktası 12 milyonun ötesinde bir yere düşer. Kapasite de aynı yöne iter, çünkü hızı artık karşılayamayan tek task’ın ikiye çıkması gerekir.

Her girdi bu çizgiyi oynatır. Memory’yi ya da süreyi ikiye katlarsanız yalnızca compute bileşeni oynar: $1.67, $3.34’e çıkar; bu da Lambda toplamını milyon başına yaklaşık $4.54’e, alt sınırı da ayda yaklaşık 7,6 milyon isteğe indirir. İstek başına ücret ile HTTP API ücreti bu hareketin dışında kalır. Hangi tarafa NAT Gateway eklerseniz o tarafın tabanına saatte $0.045, yani ayda yaklaşık $32.85 ve gigabyte başına $0.045 biner. İki taraf da ARM’i x86’nın yaklaşık %20 altında fiyatlıyor (Lambda GB-saniye başına $0.0000133334, Fargate vCPU-saati başına $0.03238); yani mimari değiştirmek karşılaştırmayı sonuçlandırmaz, iki tarafı birden kaydırır.

Geçişin ters yönde yaşandığı da olur ve bunun belgelenmiş bir örneği var. InfoQ’nun 2023’te aktardığına göre Prime Video’nun ses ve video kalitesi izleme servisi, Step Functions, Lambda ve S3 üzerine kurulu tasarımdan ECS on EC2 üzerinde tek bir sürece taşındı; operasyonel maliyetlerde bildirilen azalma %90 oldu. Serverless tasarım, state transition hesap limitine takılmadan önce beklenen yükün ancak yaklaşık %5’ini karşılayabiliyordu. Nedeni dikkatle okumakta fayda var: maliyeti büyüten kalemler o state transition’lar ile ara video kareleri ve ses tamponları için S3’e yapılan yoğun okuma ve yazmalardı. Lambda GB-saniyeleri hiçbir zaman baskı altındaki kalem olmadı. Genelleşen şey mekanizma: çok yüksek frekanslı bir yolda birim başına faturalandırma bir noktadan sonra her şeyin önüne geçiyor. Amazon özgün mühendislik yazısını yayından kaldırdı; anlatı ikincil kaynaklar üzerinden ayakta duruyor.

Karar vermeden önce iki tarafı da kendi trafiğinizle hesaplayın.

Migration Tuzakları ve Çözümleri#

1. State Yönetimi Zorluğu

// Sorun: ECS service in-memory caching'e sahipti
// Çözüm: DynamoDB ile external state

// Öncesi (ECS memory'sinde)
const cache = new Map<string, UserData>();

// Sonrası (DynamoDB ile Lambda)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

export class UserCache {
  private dynamodb = new DynamoDBClient({});
  
  async get(userId: string): Promise<UserData | null> {
    // Caching için TTL ile DynamoDB kullan
    const result = await this.dynamodb.send(new GetItemCommand({
      TableName: 'UserCache',
      Key: { userId: { S: userId } }
    }));
    
    return result.Item ? JSON.parse(result.Item.data.S!) : null;
  }
}

2. Connection Pool Migration’ı

// Sorun: ECS persistent DB connection'lara sahipti
// Çözüm: RDS Proxy ile invocation başına connection

// Öncesi (persistent connection'larla ECS)
const pool = new Pool({
  host: 'db.internal',
  max: 20,
  idleTimeoutMillis: 30000
});

// Sonrası (RDS Proxy ile Lambda)
import { Client } from 'pg';

export const handler = async (event: any) => {
  const client = new Client({
    host: 'rds-proxy.cluster-xyz.us-east-1.rds.amazonaws.com',
    port: 5432,
    database: 'mydb',
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    ssl: { rejectUnauthorized: false }
  });
  
  await client.connect();
  try {
    const result = await client.query('SELECT * FROM users WHERE id = $1', [event.userId]);
    return result.rows[0];
  } finally {
    await client.end();
  }
};

Advanced Mimari Pattern’leri#

Lambda ile Event-Driven Mimari#

// Distributed transaction'lar için Saga pattern implementasyonu
import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn';

export class PaymentSaga {
  private stepFunctions: SFNClient;
  
  constructor() {
    this.stepFunctions = new SFNClient({});
  }
  
  async executePayment(paymentData: PaymentData) {
    // State machine ile birlikte deploy edilir, invocation başına yeniden
    // kurulmaz. Buraya alındı, çünkü atlanan kısım telafi dallarıdır.
    const sagaDefinition = {
      Comment: 'Payment processing saga',
      StartAt: 'ValidatePayment',
      States: {
        ValidatePayment: {
          Type: 'Task',
          Resource: 'arn:aws:lambda:us-east-1:123456789:function:validate-payment',
          Next: 'ProcessPayment',
          Catch: [
            {
              ErrorEquals: ['ValidationError'],
              Next: 'PaymentFailed'
            }
          ]
        },
        ProcessPayment: {
          Type: 'Task', 
          Resource: 'arn:aws:lambda:us-east-1:123456789:function:process-payment',
          Next: 'UpdateInventory',
          Catch: [
            {
              ErrorEquals: ['PaymentError'],
              Next: 'CompensateValidation'
            }
          ]
        },
        UpdateInventory: {
          Type: 'Task',
          Resource: 'arn:aws:lambda:us-east-1:123456789:function:update-inventory', 
          Next: 'SendConfirmation',
          Catch: [
            {
              ErrorEquals: ['InventoryError'],
              Next: 'CompensatePayment'
            }
          ]
        },
        SendConfirmation: {
          Type: 'Task',
          Resource: 'arn:aws:lambda:us-east-1:123456789:function:send-confirmation',
          End: true
        },
        // Compensation state'leri
        CompensatePayment: {
          Type: 'Task',
          Resource: 'arn:aws:lambda:us-east-1:123456789:function:refund-payment',
          Next: 'CompensateValidation'
        },
        CompensateValidation: {
          Type: 'Task', 
          Resource: 'arn:aws:lambda:us-east-1:123456789:function:cleanup-validation',
          Next: 'PaymentFailed'
        },
        PaymentFailed: {
          Type: 'Fail',
          Cause: 'Payment processing başarısız'
        }
      }
    };
    
    const execution = await this.stepFunctions.send(new StartExecutionCommand({
      stateMachineArn: process.env.PAYMENT_SAGA_STATE_MACHINE!,
      input: JSON.stringify(paymentData)
    }));
    
    return execution.executionArn;
  }
}

Lambda için Circuit Breaker Pattern#

// External service çağrıları için circuit breaker
export class CircuitBreaker {
  private failures: number = 0;
  private lastFailureTime: number = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
  
  constructor(
    private failureThreshold: number = 5,
    private recoveryTimeMs: number = 60000
  ) {}
  
  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.recoveryTimeMs) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker OPEN');
      }
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
    }
  }
}

// Lambda function'da kullanım
const circuitBreaker = new CircuitBreaker(5, 30000);

export const handler = async (event: any) => {
  try {
    return await circuitBreaker.execute(async () => {
      return await callExternalService(event.data);
    });
  } catch (error) {
    return {
      statusCode: 503,
      body: JSON.stringify({ error: 'Service geçici olarak kullanılamıyor' })
    };
  }
};

Seri Özeti#

Bölüm 1 cold start’ları ve runtime seçimini, bölüm 2 memory ve süreyi, bölüm 3 function’lar çalışırken neyi görebildiğinizi ele aldı. Yukarıdaki pattern’ler ise bir Lambda envanterinin bunlardan sonra ihtiyaç duyduğu şeyler: paylaşılan dependency’ler, private ağ, hesaplar arası sınırlar ve bütçe toplantısından sağ çıkan bir maliyet modeli.

Varsayılan, sıradan durumda geçerlidir. Hiçbir şeyi yeniden yazmadan önce memory ve provisioned concurrency ayarlarını CloudWatch verisiyle denetleyin, private bir kaynağa ihtiyacı olmayan function’ları VPC dışında tutun ve her Layer’ın byte’larını bedava bir soyutlama gibi değil, init yolundaki bir bütçe kalemi olarak hesaba katın. İş yükü aksini söylediğinde bu varsayılanı geçersiz kılın. Hesaplama yoğun bir function çoğu zaman memory ayarı yükseldikçe ucuzlar; bir gecikme hedefi de liste fiyatından provisioned concurrency’ye değebilir. Düz ve gün boyu süren trafiğe sahip bir servis ise, ayrıştırma ne kadar temiz görünürse görünsün container üzerinde kalabilir.

İlk adım hiç kod değişikliği gerektirmez: kayda değer trafiği olan her function için Max Memory Used değerini çekin ve ayrılan memory ile karşılaştırın.

Tüm AWS Lambda rehber serisi:

Kaynaklar#

AWS Lambda Production Rehberi: 5 Yıllık Gerçek Dünya Deneyimi

5+ yıllık production deneyimine dayalı kapsamlı AWS Lambda rehberi. Cold start optimizasyonu, performans ayarlama, monitoring ve maliyet optimizasyonu ile gerçek savaş hikayeleri ve pratik çözümler.

İlerleme 4/4 yazı tamamlandı

İlgili yazılar