Event-Driven Mimari ve Serverless Entegrasyonu: Olaya Dayalı Sistemler Nasıl Kurulur?

Bulut mimarisi dünyasında bir şeylerin temelden değiştiğini fark ettiğinizde, genellikle bir production incident’ın ortasındasınızdır. Monolitik yapınızın bir köşesi çökmüş, diğer servisler domino gibi devrilmeye başlamıştır. İşte tam bu noktada event-driven mimari ve serverless’ın kombinasyonu neden bu kadar popüler oldu, daha net anlıyorsunuz. Bu yazıda teorik lafları bir kenara bırakıp, gerçek dünyada nasıl çalıştığını, nasıl kurduğunuzu ve nelere dikkat etmeniz gerektiğini konuşacağız.

Event-Driven Mimari Nedir, Neden Önemlidir

Event-driven mimari, sistemin bileşenlerinin birbirleriyle doğrudan değil, olaylar (events) aracılığıyla iletişim kurduğu bir yaklaşımdır. Bir kullanıcı sipariş verdiğinde, sipariş servisi doğrudan ödeme servisini çağırmaz. Bunun yerine “OrderCreated” adında bir event yayar, ödeme servisi bu event’i dinler ve kendi işini yapar.

Bu yaklaşımın sysadmin perspektifinden baktığınızda en büyük avantajı gevşek bağlılıktır. Ödeme servisi down olduğunda sipariş servisi bundan etkilenmez. Event kuyruğunda bekler, ödeme servisi ayağa kalktığında işlemeye devam eder. Bunu klasik request-response mimarisinde yapabilmek için çok daha karmaşık hata yönetimi mekanizmaları kurmanız gerekir.

Serverless ile birleştiğinde ise bu mimari gerçekten güçlü bir hal alır. AWS Lambda, Azure Functions veya Google Cloud Functions gibi servisler, event’lere tepki olarak tetiklenebilir, işi yapar ve kapanır. Siz yalnızca işlem süresine ödersiniz, idle time için tek kuruş harcamazsınız.

Temel Bileşenler ve Araçlar

Bir event-driven serverless mimarisi kurarken şu bileşenlerle uğraşacaksınız:

  • Event Bus/Message Broker: AWS EventBridge, Apache Kafka, RabbitMQ, Google Pub/Sub
  • Serverless Runtime: AWS Lambda, Azure Functions, Google Cloud Functions
  • Event Store: DynamoDB, Firestore, EventStoreDB
  • Dead Letter Queue (DLQ): Başarısız event’lerin tutulduğu kuyruk
  • Monitoring ve Tracing: CloudWatch, Datadog, Jaeger

AWS ekosistemi üzerinden örnekler vereceğim çünkü en yaygın kullanılan platform bu ve tooling açısından en olgun durumda.

Pratik Senaryo: E-ticaret Sipariş Yönetimi

Diyelim ki bir e-ticaret platformu yönetiyorsunuz. Sipariş verildiğinde şunların olması gerekiyor: stok güncellenmesi, ödeme alınması, kargo firmasına bildirim gönderilmesi, müşteriye email atılması ve analytics sistemine veri yazılması. Klasik monolitik yaklaşımda bu beş işlem sırayla çalışır ve biri patladığında her şey durur.

Event-driven yaklaşımda ise tek bir “OrderCreated” event’i yayarsınız, beş farklı Lambda fonksiyonu bunu paralel olarak dinler ve kendi işini yapar. Kargo bildirimi servisi geçici olarak down olsa bile diğerleri etkilenmez.

AWS SAM ile Temel Yapıyı Kurmak

Önce AWS SAM (Serverless Application Model) ile temel template’imizi oluşturalım:

# SAM CLI kurulumu
pip install aws-sam-cli

# Yeni proje oluştur
sam init --runtime python3.11 --name ecommerce-events

# Proje yapısına bakalım
ls -la ecommerce-events/

Template dosyamız şöyle görünecek:

cat > template.yaml << 'EOF'
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  OrderEventBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: ecommerce-order-bus

  OrderCreatedQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: order-created-queue
      VisibilityTimeout: 300
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt OrderDLQ.Arn
        maxReceiveCount: 3

  OrderDLQ:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: order-dead-letter-queue
      MessageRetentionPeriod: 1209600

  PaymentProcessorFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/payment/
      Handler: app.lambda_handler
      Runtime: python3.11
      Timeout: 60
      MemorySize: 256
      Events:
        SQSTrigger:
          Type: SQS
          Properties:
            Queue: !GetAtt OrderCreatedQueue.Arn
            BatchSize: 10
            FunctionResponseTypes:
              - ReportBatchItemFailures
EOF

Event Producer Lambda Fonksiyonu

Siparişi alan ve event yayınlayan fonksiyonu yazalım:

cat > functions/order/app.py << 'EOF'
import json
import boto3
import uuid
from datetime import datetime

eventbridge = boto3.client('events')
dynamodb = boto3.resource('dynamodb')

def lambda_handler(event, context):
    body = json.loads(event['body'])
    
    order_id = str(uuid.uuid4())
    order = {
        'order_id': order_id,
        'customer_id': body['customer_id'],
        'items': body['items'],
        'total_amount': body['total_amount'],
        'created_at': datetime.utcnow().isoformat(),
        'status': 'PENDING'
    }
    
    # DynamoDB'ye yaz
    table = dynamodb.Table('Orders')
    table.put_item(Item=order)
    
    # Event yayınla
    response = eventbridge.put_events(
        Entries=[
            {
                'Source': 'ecommerce.orders',
                'DetailType': 'OrderCreated',
                'Detail': json.dumps(order),
                'EventBusName': 'ecommerce-order-bus'
            }
        ]
    )
    
    failed = response.get('FailedEntryCount', 0)
    if failed > 0:
        raise Exception(f"EventBridge'e event gonderilemedi: {response['Entries']}")
    
    return {
        'statusCode': 201,
        'body': json.dumps({'order_id': order_id, 'status': 'PENDING'})
    }
EOF

Event Consumer: Ödeme Servisi

cat > functions/payment/app.py << 'EOF'
import json
import boto3
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    batch_item_failures = []
    
    for record in event['Records']:
        try:
            process_payment(record)
        except Exception as e:
            logger.error(f"Odeme isleme hatasi: {str(e)}")
            # Sadece basarisiz item'i DLQ'ya gonder
            batch_item_failures.append({
                'itemIdentifier': record['messageId']
            })
    
    return {'batchItemFailures': batch_item_failures}

def process_payment(record):
    body = json.loads(record['body'])
    detail = json.loads(body['detail']) if 'detail' in body else body
    
    order_id = detail['order_id']
    amount = detail['total_amount']
    
    logger.info(f"Odeme isleniyor: order_id={order_id}, amount={amount}")
    
    # Gercek senaryoda burada payment gateway API cagrisi olurdu
    # Simdi sadece log'luyoruz
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('Orders')
    table.update_item(
        Key={'order_id': order_id},
        UpdateExpression='SET #s = :status, payment_processed = :pp',
        ExpressionAttributeNames={'#s': 'status'},
        ExpressionAttributeValues={
            ':status': 'PAYMENT_PROCESSED',
            ':pp': True
        }
    )
    
    logger.info(f"Odeme tamamlandi: order_id={order_id}")
EOF

EventBridge Rule ile Event Routing

EventBridge’in en güçlü özelliklerinden biri content-based routing. Event içeriğine göre hangi Lambda’nın tetikleneceğine karar verebilirsiniz:

aws events put-rule 
  --name "HighValueOrderRule" 
  --event-bus-name "ecommerce-order-bus" 
  --event-pattern '{
    "source": ["ecommerce.orders"],
    "detail-type": ["OrderCreated"],
    "detail": {
      "total_amount": [{"numeric": [">=", 1000]}]
    }
  }' 
  --state ENABLED 
  --description "1000 TL ustu siparisler icin ozel islem"

# Rule'u Lambda ile iliskilendir
aws events put-targets 
  --rule "HighValueOrderRule" 
  --event-bus-name "ecommerce-order-bus" 
  --targets '[
    {
      "Id": "high-value-processor",
      "Arn": "arn:aws:lambda:eu-west-1:123456789:function:HighValueOrderProcessor",
      "RetryPolicy": {
        "MaximumRetryAttempts": 3,
        "MaximumEventAgeInSeconds": 3600
      }
    }
  ]'

Dead Letter Queue Yönetimi ve Hata Senaryoları

Production’da en çok baş ağrıtan konu DLQ yönetimidir. İşte gerçekçi bir DLQ işleyici:

cat > functions/dlq_processor/app.py << 'EOF'
import json
import boto3
import logging
from datetime import datetime

logger = logging.getLogger()
logger.setLevel(logging.INFO)

sqs = boto3.client('sqs')
sns = boto3.client('sns')
dynamodb = boto3.resource('dynamodb')

def lambda_handler(event, context):
    """
    DLQ'daki basarisiz mesajlari analiz et ve alert gonder
    Bu fonksiyon scheduled olarak calisir, ornegin her 5 dakikada bir
    """
    
    dlq_url = 'https://sqs.eu-west-1.amazonaws.com/123456789/order-dead-letter-queue'
    
    while True:
        response = sqs.receive_message(
            QueueUrl=dlq_url,
            MaxNumberOfMessages=10,
            AttributeNames=['All'],
            MessageAttributeNames=['All']
        )
        
        messages = response.get('Messages', [])
        if not messages:
            break
            
        for message in messages:
            analyze_and_alert(message, dlq_url)
    
    return {'statusCode': 200}

def analyze_and_alert(message, dlq_url):
    body = json.loads(message['Body'])
    approximate_receive_count = int(
        message['Attributes'].get('ApproximateReceiveCount', 0)
    )
    
    logger.error(f"""
        DLQ'da basarisiz mesaj bulundu!
        MessageId: {message['MessageId']}
        ReceiveCount: {approximate_receive_count}
        Body: {json.dumps(body, indent=2)}
    """)
    
    # Failure log'u DynamoDB'ye kaydet
    failure_table = dynamodb.Table('FailedEvents')
    failure_table.put_item(Item={
        'message_id': message['MessageId'],
        'body': body,
        'receive_count': approximate_receive_count,
        'failed_at': datetime.utcnow().isoformat(),
        'error_type': classify_error(body)
    })
    
    # Ops ekibine alert gonder
    sns.publish(
        TopicArn='arn:aws:sns:eu-west-1:123456789:ops-alerts',
        Subject='DLQ Alert: Basarisiz Event',
        Message=f"Order event isleme basarisiz oldu. MessageId: {message['MessageId']}"
    )

def classify_error(body):
    body_str = json.dumps(body).lower()
    if 'timeout' in body_str:
        return 'TIMEOUT'
    elif 'connection' in body_str:
        return 'CONNECTION_ERROR'
    elif 'validation' in body_str:
        return 'VALIDATION_ERROR'
    return 'UNKNOWN'
EOF

Idempotency: Event-Driven’ın En Kritik Konusu

Event-driven mimaride aynı event birden fazla kez işlenebilir. Bu kaçınılmazdır. O yüzden fonksiyonlarınız idempotent olmalıdır. AWS PowerTools kütüphanesi bu konuda çok yardımcı olur:

pip install aws-lambda-powertools

cat > functions/inventory/app.py << 'EOF'
from aws_lambda_powertools.utilities.idempotency import (
    idempotent_function,
    IdempotencyConfig,
    DynamoDBPersistenceLayer
)
import json
import logging

logger = logging.getLogger()

persistence_layer = DynamoDBPersistenceLayer(
    table_name='IdempotencyTable'
)

config = IdempotencyConfig(
    event_key_jmespath='detail.order_id',
    raise_on_no_idempotency_key=True,
    expires_after_seconds=3600
)

@idempotent_function(
    data_keyword_argument='order_data',
    config=config,
    persistence_store=persistence_layer
)
def update_inventory(order_data: dict) -> dict:
    """Bu fonksiyon ayni order_id icin birden fazla calistirilsa bile
    sadece bir kez stok gunceller"""
    
    order_id = order_data['order_id']
    items = order_data['items']
    
    for item in items:
        logger.info(f"Stok guncelleniyor: {item['sku']}, miktar: -{item['quantity']}")
        # Stok guncelleme logic'i buraya
    
    return {'order_id': order_id, 'inventory_updated': True}

def lambda_handler(event, context):
    for record in event['Records']:
        body = json.loads(record['body'])
        order_data = json.loads(body.get('detail', body))
        update_inventory(order_data=order_data)
    
    return {'statusCode': 200}
EOF

Monitoring ve Observability

Production’da gözlemleme kritik. CloudWatch’a custom metric gönderme:

aws cloudwatch put-metric-data 
  --namespace "EcommerceEvents" 
  --metric-data '[
    {
      "MetricName": "OrderEventProcessingTime",
      "Value": 245,
      "Unit": "Milliseconds",
      "Dimensions": [
        {"Name": "FunctionName", "Value": "PaymentProcessor"},
        {"Name": "Environment", "Value": "production"}
      ]
    },
    {
      "MetricName": "DLQMessageCount",
      "Value": 3,
      "Unit": "Count",
      "Dimensions": [
        {"Name": "QueueName", "Value": "order-dead-letter-queue"}
      ]
    }
  ]'

# Lambda cold start'larini izle
aws cloudwatch get-metric-statistics 
  --namespace AWS/Lambda 
  --metric-name InitDuration 
  --dimensions Name=FunctionName,Value=PaymentProcessor 
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) 
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) 
  --period 300 
  --statistics Average,Maximum 
  --output table

Local Development ve Test Stratejisi

Serverless’ı local’de test etmek zorlaşabilir. SAM local ve LocalStack kombinasyonu hayat kurtarır:

# LocalStack kurulumu
pip install localstack awscli-local

# LocalStack'i baslat
docker run --rm -it 
  -p 4566:4566 
  -e SERVICES=sqs,sns,lambda,events,dynamodb 
  -e DEFAULT_REGION=eu-west-1 
  localstack/localstack

# LocalStack uzerinde SQS queue olustur
awslocal sqs create-queue 
  --queue-name order-created-queue 
  --attributes '{
    "VisibilityTimeout": "300",
    "MessageRetentionPeriod": "86400"
  }'

# Test event'i gonder
awslocal sqs send-message 
  --queue-url http://localhost:4566/000000000000/order-created-queue 
  --message-body '{
    "order_id": "test-001",
    "customer_id": "cust-123",
    "total_amount": 1250.00,
    "items": [
      {"sku": "PROD-001", "quantity": 2, "price": 625.00}
    ]
  }'

# SAM local ile Lambda'yi calistir
sam local invoke PaymentProcessorFunction 
  --event events/test-order-event.json 
  --env-vars env.json 
  --docker-network host

Production’da Dikkat Edilmesi Gereken Konular

Gerçek dünyada karşılaşacağınız sorunlar ve çözümleri:

Lambda Concurrency Kontrolü: SQS’ten beslenen Lambda fonksiyonlarınız ani traffic artışında binlerce concurrent execution başlatabilir. Bu hem downstream servisleri patlatır hem de maliyeti uçurur.

  • Reserved concurrency ayarını mutlaka yapın
  • SQS batch size ve maximum concurrency’yi dengeli konfigüre edin
  • Throttling durumunda SQS mesajları otomatik olarak retry eder, panik yapmayın

Event Schema Yönetimi: Event formatınız değiştiğinde producer ve consumer’ları aynı anda güncellemeniz gerekmez ama schema versioning yapmanız şart. AWS EventBridge Schema Registry kullanın ya da kendi versioning sisteminizi kurun.

Timeout Kaskadı: Lambda timeout değerleri ile SQS visibility timeout arasındaki ilişkiyi iyi anlayın. Lambda timeout 60 saniye ise SQS visibility timeout en az 70 saniye olmalı. Aksi takdirde işlenmeye devam eden mesaj diğer consumer’lara görünür ve duplicate processing yaşarsınız.

Cost Yönetimi: Her şey serverless diye ödeme optimizasyonunu ihmal etmeyin. Lambda’yı 128MB ile başlatıp yetersiz bulunca 1024MB’a çıkarmak yerine, profiling yapın. Doğru memory seçimi hem performansı artırır hem de maliyeti düşürür. AWS Lambda Power Tuning aracı bu konuda objektif veri sunar.

Event Ordering: SQS Standard Queue event sırasını garanti etmez. Sıra önemliyse SQS FIFO Queue kullanın ama throughput’un ciddi şekilde düştüğünü ve maliyetin arttığını unutmayın. Gerçekten sıraya ihtiyacınız var mı, iyi düşünün.

Maliyet Analizi ve Optimizasyon

Bir e-ticaret platformu için gerçekçi rakamlar düşünelim. Günde 100.000 sipariş işlediğinizi varsayalım:

  • Her sipariş için 5 event (ödeme, stok, kargo, email, analytics)
  • Toplam 500.000 Lambda invocation/gün
  • Ortalama 200ms execution time, 256MB memory

AWS Lambda fiyatlandırmasıyla bu senaryo aylık 15-20 dolar civarında kalır. Aynı yükü her zaman ayakta duran EC2 instance’larıyla karşılamak isteseydiniz, idle time düşünüldüğünde bu rakam birkaç kat daha yukarıda olurdu. Ama büyük trafiğiniz varsa Lambda’nın GB-saniye fiyatı EC2’ya göre pahalıdır. 100.000 invocation/gün altında serverless, üzerinde hibrit mimari düşünmenizi öneririm.

Sonuç

Event-driven mimari ve serverless kombinasyonu, doğru kullanıldığında operasyonel yükünüzü ciddi ölçüde azaltır. Patch yönetimi, kapasite planlama, idle resource maliyeti gibi dertlerden kurtulursunuz. Ama bu mimari kendi dertlerini de beraberinde getirir: idempotency, distributed tracing, DLQ yönetimi ve cold start optimizasyonu başlıca zorluklar.

Sysadmin olarak bu mimariye geçişte en büyük zihinsel değişim şudur: artık sunucu değil, event akışı yönetiyorsunuz. Monitoring araçlarınızı, alarm eşiklerinizi ve runbook’larınızı buna göre güncellemeniz gerekiyor. Bir Lambda fonksiyonunun neden patladığını bulmak, bir servisin neden çöktüğünü bulmaktan farklı bir beceri gerektirir; distributed tracing ve structured logging olmadan bu işi yapmak neredeyse imkansızlaşır.

Küçük başlayın. Mevcut monolitinizin bir köşesini, örneğin email bildirim servisini, event-driven serverless olarak yeniden yazın. Production’da nasıl davrandığını gözlemleyin, maliyetlerini hesaplayın ve ekibinizin bu mimariye ne kadar hızlı adapte olduğunu değerlendirin. Sonra adım adım genişletin. Tüm sistemi bir gecede dönüştürmeye çalışmak, başladığınız yerden daha kötü bir yere götürür sizi.

Bir yanıt yazın

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