Serverless Mimari Tasarım Prensipleri: Bulutta Uygulama Geliştirmenin Yeni Yolu

Bulut dünyasında “sunucu yönetme” derdi artık opsiyonel hale geldi. Serverless mimari, altyapı operasyonlarını bulut sağlayıcısına devrederek geliştiricilerin ve sistem yöneticilerinin asıl işe, yani iş mantığına odaklanmasını sağlıyor. Ama “serverless = kolay” denklemini kuranlar genellikle production’da acı gerçeklerle yüzleşiyor. Doğru tasarım prensiplerini uygulamadan kurulan serverless mimariler, geleneksel sunucu tabanlı sistemlerden çok daha karmaşık ve pahalı hale gelebiliyor. Bu yazıda serverless mimarisini gerçekten çalışır hale getiren prensipleri, pratik örneklerle ele alacağız.

Serverless Nedir, Ne Değildir?

Önce kavramı netleştirelim. Serverless, sunucuların olmadığı anlamına gelmiyor. Sunucular var, sadece senin yönetmediğin sunucular. AWS Lambda, Azure Functions, Google Cloud Functions gibi servisler kod paketini alıp çalıştırıyor, ölçeklendiriyor, yama yapıyor. Sen sadece fonksiyon kodunu yazıp deploy ediyorsun.

Serverless’ın temel karakteristikleri:

  • Olay güdümlü (event-driven) çalışma modeli
  • Otomatik ölçeklendirme, sıfırdan binlere ve geri
  • Kullanım başına faturalama (pay-per-execution)
  • Durumsuz (stateless) fonksiyon yaşam döngüsü
  • Kısa ömürlü çalışma süresi (genellikle maksimum 15 dakika)

Serverless’ın uygun olmadığı senaryolar da var: uzun süren batch işlemleri, persistent bağlantı gerektiren uygulamalar (WebSocket tabanlı gerçek zamanlı sistemler doğrudan Lambda ile değil, API Gateway entegrasyonuyla yönetilir), ve çok düşük latency gerektiren yüksek frekanslı işlemler.

Prensip 1: Tek Sorumluluk (Single Responsibility)

Her fonksiyon tek bir iş yapmalı. Bu kural kulağa basit geliyor ama pratikte çok sık ihlal ediliyor. Bir Lambda fonksiyonu içinde HTTP isteği alma, veritabanı yazma, e-posta gönderme, cache güncelleme işlemlerini bir arada yapan kod gördüğümde içim sızlıyor.

Neden önemli? Çünkü serverless’ta cold start süresi, bellek kullanımı ve çalışma süresi doğrudan maliyet ve performansı etkiliyor. Şişirilmiş fonksiyonlar her iki tarafı da kötü etkiliyor.

Kötü örnek: Tek fonksiyonda her şeyi yapmak.

# Kötü: Bir fonksiyon her şeyi yapıyor
# handler.py içinde: HTTP al -> DB yaz -> Email gönder -> Cache güncelle
# Bu fonksiyon 512MB RAM ve 30 saniye timeout istiyor
aws lambda create-function 
  --function-name process-order-monolith 
  --runtime python3.11 
  --memory-size 512 
  --timeout 30 
  --handler handler.process_order

İyi örnek: Sorumlulukları ayırmak.

# İyi: Her iş için ayrı fonksiyon
aws lambda create-function 
  --function-name order-validator 
  --runtime python3.11 
  --memory-size 128 
  --timeout 5 
  --handler validator.handle

aws lambda create-function 
  --function-name order-persister 
  --runtime python3.11 
  --memory-size 128 
  --timeout 10 
  --handler persister.handle

aws lambda create-function 
  --function-name order-notifier 
  --runtime python3.11 
  --memory-size 128 
  --timeout 5 
  --handler notifier.handle

128MB bellek kullanan üç fonksiyon, 512MB kullanan bir fonksiyondan hem daha ucuz hem de daha test edilebilir.

Prensip 2: Event-Driven Mimari ile Gevşek Bağlantı

Serverless fonksiyonları birbirini doğrudan çağırmamalı. Bu en çok yapılan mimari hatalardan biri. Fonksiyon A, Fonksiyon B’yi çağırıyor, B de C’yi çağırıyor. Birisi hata verdiğinde zincirleme başarısızlık yaşanıyor ve debugging kabusu başlıyor.

Bunun yerine olay tabanlı iletişim kullan: SQS, SNS, EventBridge, Kafka. Bir fonksiyon işini bitirip bir mesaj kuyruğuna bırakıyor, diğer fonksiyon kendi zamanında bu mesajı işliyor.

# SQS kuyruğu oluştur
aws sqs create-queue 
  --queue-name order-processing-queue 
  --attributes '{
    "VisibilityTimeout": "60",
    "MessageRetentionPeriod": "86400",
    "ReceiveMessageWaitTimeSeconds": "20"
  }'

# Lambda fonksiyonunu bu kuyruğa bağla
aws lambda create-event-source-mapping 
  --function-name order-persister 
  --event-source-arn arn:aws:sqs:eu-west-1:123456789:order-processing-queue 
  --batch-size 10 
  --maximum-batching-window-in-seconds 5

Bu yaklaşımın faydaları:

  • Hata izolasyonu: Bir fonksiyon çöktüğünde diğerleri etkilenmiyor
  • Yeniden deneme: Mesaj kuyruğu başarısız işlemleri otomatik retry yapıyor
  • Ölçeklendirme: Her fonksiyon bağımsız ölçekleniyor
  • Gözlemlenebilirlik: Kuyruk derinliği ile sistemin sağlığını izleyebilirsin

Prensip 3: Durumsuzluk ve Dış Durum Yönetimi

Serverless fonksiyonlar stateless olmak zorunda. İki çağrı arasında hiçbir şey bellekte tutamazsın çünkü aynı örnek bir sonraki çağrıda kullanılacağının garantisi yok. Bu pratikte ne anlama geliyor?

Her türlü durum bilgisi dış servislere taşınmalı:

  • Oturum verisi: Redis, DynamoDB veya ElastiCache
  • Dosyalar: S3
  • Veritabanı bağlantıları: RDS Proxy üzerinden connection pooling
  • Konfigürasyon: Parameter Store veya Secrets Manager

RDS Proxy kullanımı özellikle kritik. Serverless mimarilerde her fonksiyon çağrısı yeni bir DB bağlantısı açmaya çalışıyor. Yüksek yük altında bu, veritabanını bağlantı seli ile boğuyor. RDS Proxy bu sorunu çözüyor.

# RDS Proxy oluşturma
aws rds create-db-proxy 
  --db-proxy-name my-serverless-proxy 
  --engine-family MYSQL 
  --auth '[{
    "AuthScheme": "SECRETS",
    "SecretArn": "arn:aws:secretsmanager:eu-west-1:123456789:secret:db-creds",
    "IAMAuth": "REQUIRED"
  }]' 
  --role-arn arn:aws:iam::123456789:role/rds-proxy-role 
  --vpc-subnet-ids subnet-abc123 subnet-def456 
  --vpc-security-group-ids sg-xyz789

Lambda fonksiyonu içinde bağlantı bilgisini environment variable olarak değil, Secrets Manager’dan çekmeyi unutma.

Prensip 4: Cold Start Optimizasyonu

Cold start, bir fonksiyonun ilk kez veya uzun süre bekledikten sonra çağrıldığında yaşanan gecikme. Bu gecikme birkaç yüz milisaniyeden birkaç saniyeye kadar çıkabiliyor. Kullanıcı deneyimini doğrudan etkiliyor.

Cold start’ı azaltmanın pratik yolları:

Provisioned Concurrency kullan (kritik fonksiyonlar için):

# Provisioned concurrency ayarla
aws lambda put-provisioned-concurrency-config 
  --function-name order-validator 
  --qualifier production 
  --provisioned-concurrent-executions 5

Bu, 5 fonksiyon örneğini her zaman sıcak tutuyor. Maliyet artıyor ama kritik path’teki fonksiyonlar için değer.

Paket boyutunu küçült:

# Lambda paketini optimize et - gereksiz bağımlılıkları çıkar
# requirements.txt içinde sadece gerekli olanlar
pip install -r requirements.txt 
  --target ./package 
  --platform manylinux2014_x86_64 
  --only-binary=:all:

# Paket boyutunu kontrol et
du -sh package/
# Hedef: 10MB altında tutmak

# Zip oluştur
cd package && zip -r9 ../function.zip . && cd ..
zip -g function.zip handler.py

Runtime seçimi:

  • Python ve Node.js cold start süresi düşük
  • Java ve .NET cold start daha uzun (JVM başlatma süresi)
  • Eğer Java kullanmak zorundaysan GraalVM native image değerlendirilebilir

Global scope’ta bağlantıları başlat:

# Python Lambda örneği - bağlantıyı handler dışında başlat
# handler.py

import boto3
import json

# Bu kod cold start'ta bir kez çalışır
# Warm invocation'larda yeniden kullanılır
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('orders')

def handler(event, context):
    # table artık her çağrıda yeniden oluşturulmuyor
    response = table.get_item(
        Key={'order_id': event['order_id']}
    )
    return response['Item']

Prensip 5: Hata Yönetimi ve Dayanıklılık

Serverless’ta hatalar iki kategoriye giriyor: yeniden denenebilir (retryable) ve denenemeyen (non-retryable). Bu ayrımı yapmak kritik.

Bir SQS mesajını işlerken ağ hatası aldıysan, mesajı yeniden kuyruğa bırak. Ama aynı bozuk JSON’u tekrar tekrar işlemeye çalışmak anlamsız. Dead Letter Queue (DLQ) burada devreye giriyor.

# Dead Letter Queue oluştur
aws sqs create-queue 
  --queue-name order-processing-dlq 
  --attributes '{
    "MessageRetentionPeriod": "1209600"
  }'

# Ana kuyruğa DLQ ekle
aws sqs set-queue-attributes 
  --queue-url https://sqs.eu-west-1.amazonaws.com/123456789/order-processing-queue 
  --attributes '{
    "RedrivePolicy": "{"deadLetterTargetArn":"arn:aws:sqs:eu-west-1:123456789:order-processing-dlq","maxReceiveCount":"3"}"
  }'

3 deneme sonrası hala başarısız olan mesajlar DLQ’ya taşınıyor. Burada alert kurman ve manuel inceleme yapman gerekiyor.

Lambda için de benzer bir yapı kur:

# Lambda için async invocation DLQ ayarı
aws lambda put-function-event-invoke-config 
  --function-name order-notifier 
  --maximum-retry-attempts 2 
  --destination-config '{
    "OnFailure": {
      "Destination": "arn:aws:sqs:eu-west-1:123456789:order-notifier-dlq"
    }
  }'

Prensip 6: Gözlemlenebilirlik (Observability)

Serverless’ta debug yapmak geleneksel sistemlerden farklı. Sunucuya SSH atıp log bakamıyorsun. Distributed tracing ve structured logging zorunluluk haline geliyor.

Structured logging:

# CloudWatch Log Insights sorgusu ile hata analizi
aws logs start-query 
  --log-group-name '/aws/lambda/order-validator' 
  --start-time $(date -d '1 hour ago' +%s) 
  --end-time $(date +%s) 
  --query-string 'fields @timestamp, @message, error_code, order_id
    | filter level = "ERROR"
    | stats count(*) by error_code
    | sort count desc'

Her fonksiyonun logları şu alanları içermeli:

  • request_id: Lambda’nın verdiği benzersiz ID
  • correlation_id: Servisler arası takip için ürettiğin ID
  • duration_ms: İşlem süresi
  • level: DEBUG, INFO, WARNING, ERROR
  • function_name ve version: Hangi fonksiyonun hangi versiyonu

X-Ray ile distributed tracing:

# X-Ray tracing aktif et
aws lambda update-function-configuration 
  --function-name order-validator 
  --tracing-config Mode=Active

# X-Ray sampling kuralı tanımla
aws xray create-sampling-rule 
  --sampling-rule '{
    "RuleName": "order-flow-sampling",
    "Priority": 1,
    "FixedRate": 0.05,
    "ReservoirSize": 5,
    "ServiceName": "order-*",
    "ServiceType": "AWS::Lambda::Function",
    "Host": "*",
    "HTTPMethod": "*",
    "URLPath": "*",
    "ResourceARN": "*"
  }'

Yüksek trafikte her isteği trace etmek hem pahalı hem performans düşürücü. Sampling ile makul bir yüzde belirle.

Prensip 7: Güvenlik ve En Az Yetki (Least Privilege)

Her Lambda fonksiyonu için ayrı IAM rolü oluştur. Bir fonksiyonun sadece DynamoDB’ye yazması gerekiyorsa, S3’e erişim yetkisi verme. Bu hem güvenlik açısından doğru hem de bir hata durumunda hasarı sınırlıyor.

# Fonksiyona özel IAM politikası oluştur
aws iam create-policy 
  --policy-name order-persister-policy 
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "dynamodb:PutItem",
          "dynamodb:UpdateItem"
        ],
        "Resource": "arn:aws:dynamodb:eu-west-1:123456789:table/orders"
      },
      {
        "Effect": "Allow",
        "Action": [
          "sqs:ReceiveMessage",
          "sqs:DeleteMessage",
          "sqs:GetQueueAttributes"
        ],
        "Resource": "arn:aws:sqs:eu-west-1:123456789:order-processing-queue"
      },
      {
        "Effect": "Allow",
        "Action": [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ],
        "Resource": "arn:aws:logs:eu-west-1:123456789:log-group:/aws/lambda/order-persister:*"
      }
    ]
  }'

Environment variable’lara secret koyma. Secrets Manager veya Parameter Store kullan. Lambda’nın çalışma ortamını tam kontrol altında tutmak için VPC içinde konuşlandırmayı düşün ama bunu yaparken cold start süresinin artabileceğini unutma.

Prensip 8: Maliyet Optimizasyonu

Serverless ucuz diye biliniyor ama yanlış yapılandırılmış mimariler beklenmedik faturalar çıkarabiliyor. Gerçek hayatta karşılaştığım bir senaryo: Her istek için ayrı Lambda çağrısı yapan bir sistem, ay sonunda beklenenin 10 katı fatura kesiyor. Sorun neydi? Batch işlemleri düzgün yapılmıyordu, her kayıt için ayrı Lambda tetikleniyordu.

Bellek optimizasyonu:

Lambda’da bellek ayarı aynı zamanda CPU tahsisini de etkiliyor. Daha fazla bellek, daha hızlı CPU demek. Bazen 256MB yerine 512MB kullanmak, süreyi yarıya düşürüyor ve toplam maliyet aynı kalıyor veya düşüyor. AWS Lambda Power Tuning aracı bunu otomatize ediyor.

# AWS Lambda Power Tuning State Machine tetikle
aws stepfunctions start-execution 
  --state-machine-arn arn:aws:states:eu-west-1:123456789:stateMachine:powerTuningMachine 
  --input '{
    "lambdaARN": "arn:aws:lambda:eu-west-1:123456789:function:order-validator",
    "powerValues": [128, 256, 512, 1024],
    "num": 50,
    "payload": {"order_id": "test-123"},
    "parallelInvocation": true,
    "strategy": "cost"
  }'

SQS batch boyutunu optimize et:

Küçük batch boyutları çok sayıda Lambda invocation anlamına geliyor. Batch boyutunu ve bekleme sürelerini gerçek yük profiline göre ayarla.

# Batch boyutunu güncelle
aws lambda update-event-source-mapping 
  --uuid your-event-source-mapping-uuid 
  --batch-size 100 
  --maximum-batching-window-in-seconds 10

Gerçek Dünya Senaryosu: E-ticaret Sipariş İşleme

Tüm bu prensipleri bir araya getiren örnek bir mimari düşünelim. Kullanıcı sipariş veriyor, sistem bunu işliyor.

Akış:

  1. API Gateway HTTP isteği alıyor
  2. order-validator Lambda tetikleniyor, sipariş doğrulanıyor
  3. Geçerli sipariş SQS kuyruğuna yazılıyor
  4. order-persister Lambda SQS’ten okuyor, DynamoDB’ye yazıyor
  5. DynamoDB Streams order-notifier Lambda’yı tetikliyor
  6. Kullanıcıya e-posta ve SMS gönderiliyor

Her adım izole, her adım bağımsız ölçekleniyor. Notifier çöktüğünde sipariş işleme etkilenmiyor. Persister yavaşladığında validator etkilenmiyor.

# DynamoDB Streams'i aktif et
aws dynamodb update-table 
  --table-name orders 
  --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES

# Notifier Lambda'yı bu stream'e bağla
STREAM_ARN=$(aws dynamodb describe-table 
  --table-name orders 
  --query 'Table.LatestStreamArn' 
  --output text)

aws lambda create-event-source-mapping 
  --function-name order-notifier 
  --event-source-arn $STREAM_ARN 
  --starting-position LATEST 
  --batch-size 10 
  --bisect-batch-on-function-error true

--bisect-batch-on-function-error parametresi önemli: Batch içinde bir hata olduğunda batch’i ikiye bölüyor ve hangisinin sorunlu olduğunu buluyor. Bozuk bir kayıt tüm batch’i mahvetmiyor.

Infrastructure as Code ile Serverless

Serverless mimarisini elle kurmak sürdürülebilir değil. Terraform, AWS SAM veya Serverless Framework kullanmak zorunlu. Özellikle multi-environment (dev, staging, prod) yönetiminde IaC olmadan kaos kaçınılmaz.

# AWS SAM ile deploy
sam build --use-container

sam deploy 
  --stack-name order-processing-prod 
  --s3-bucket my-deployment-bucket 
  --capabilities CAPABILITY_IAM 
  --parameter-overrides 
    Environment=prod 
    DbProxyEndpoint=proxy.cluster-xyz.eu-west-1.rds.amazonaws.com 
  --no-confirm-changeset

# Deploy sonrası smoke test
aws lambda invoke 
  --function-name order-validator-prod 
  --payload '{"order_id": "smoke-test-001", "items": []}' 
  --cli-binary-format raw-in-base64-out 
  response.json && cat response.json

Sonuç

Serverless mimari doğru uygulandığında operasyonel yükü ciddi ölçüde azaltan, otomatik ölçeklenen ve maliyet etkin bir platform sunuyor. Ama “sihirli değnek” değil. Yanlış uygulandığında distributed monolith yaratıyorsun, debug cehennemi yaşıyorsun ve ayda beklediğinden kat kat fazla fatura ödüyorsun.

Özetlemek gerekirse: Fonksiyonları küçük ve tek sorumlu tut. Servisler arası iletişimde mesaj kuyruğu kullan, direkt çağrıdan kaç. Durum bilgisini her zaman dış servislerde sakla. Cold start’ı görmezden gelme, kritik path’leri provisioned concurrency ile koru. Her fonksiyon için ayrı IAM rolü yaz, least privilege’dan taviz verme. Structured logging ve distributed tracing olmadan üretime çıkma. Son olarak maliyet takibini baştan kur, sürpriz fatura istemiyorsan CloudWatch’a billing alert ekle.

Serverless’a geçiş bir gecede olmuyor. Önce yeni servisleri serverless yaz, mevcut monoliti parça parça taşı. Deneyim kazandıkça hem mimariyi hem de maliyet optimizasyonunu daha iyi yapıyorsun. Başlangıç olarak bir event-driven kullanım senaryosu seç, küçük bir serverless modül yaz ve gerçek bir prodüksiyon yükünde nasıl davrandığını gözlemle.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir