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.
