Serverless ile Mikroservis Mimarisi Tasarımı
Mikroservis mimarisini serverless ile birleştirmek, ilk bakışta biraz kafa karıştırıcı gelebilir. “Zaten mikroservislerim var, neden serverless?” sorusu aklına gelebilir. Ama bu iki yaklaşımı doğru şekilde harmanlayınca ortaya çıkan mimari, hem ölçeklenebilirlik hem de maliyet açısından ciddi avantajlar sunuyor. Bu yazıda gerçek dünya senaryoları üzerinden serverless ve mikroservis mimarisini nasıl tasarlayacağını, hangi tuzaklardan kaçınacağını ve production’da ne işe yaradığını anlatacağım.
Serverless Nedir, Mikroservis ile Farkı Ne?
Klasik mikroservis mimarisinde her servisin kendi container’ı, kendi process’i ve kendi yaşam döngüsü vardır. Kubernetes üzerinde çalışıyorsan deployment, service, ingress tanımları yapıyorsun, health check’lerle uğraşıyorsun, pod’ların scale olmasını yönetiyorsun.
Serverless’ta ise sen sadece kodu yazıyorsun. Altyapı tamamen cloud provider’ın elinde. AWS Lambda, Google Cloud Functions veya Azure Functions gibi servisler fonksiyonunu çalıştırıyor, işi bitince kaynakları serbest bırakıyor. Sen sadece kullandığın süre için ödüyorsun.
Temel fark şu: Mikroservis bir mimari yaklaşım, serverless ise bir deployment ve çalıştırma modelidir. Bu yüzden birbirinin alternatifi değil, tamamlayıcısıdır.
Gerçek dünya örneği verecek olursam: Bir e-ticaret platformu düşün. Sipariş yönetimi, kullanıcı yönetimi, envanter servisi gibi core business logic’ler için container tabanlı mikroservisler kullanırsın çünkü bu servisler sürekli ayakta olmalı ve düşük latency gerektirir. Ama email bildirimi gönderme, PDF fatura oluşturma, stok raporu hazırlama gibi event-driven ve aralıklı çalışan işler için serverless fonksiyonlar çok daha mantıklıdır.
Mimari Tasarım Prensipleri
Serverless-mikroservis hibrit mimarisi tasarlarken birkaç temel prensibi aklında tutman gerekiyor.
Single Responsibility: Her Lambda fonksiyonu tek bir iş yapmalı. “Sipariş oluştur ve email gönder ve envanteri güncelle” yapan bir fonksiyon değil, bunların her biri ayrı fonksiyon olmalı.
Event-Driven Communication: Servisler arasındaki iletişim mümkün olduğunca event’ler üzerinden kurulmalı. AWS SQS, SNS veya EventBridge bu noktada devreye giriyor.
Stateless Functions: Lambda fonksiyonların herhangi bir local state tutmaması gerekiyor. State her zaman dış sistemlerde (DynamoDB, Redis, S3) yaşamalı.
Fail Fast: Fonksiyonların hızlı başarısız olması ve retry mekanizmalarının doğru kurulması kritik.
Proje Yapısını Kurmak
Önce serverless framework kurulumunu yapalım. AWS Lambda için Serverless Framework veya AWS SAM kullanabilirsin. Ben Serverless Framework’ü tercih ediyorum çünkü çok provider desteği var ve konfigürasyon daha okunaklı.
# Node.js kurulu olduğunu varsayarak
npm install -g serverless
# AWS credentials ayarla
aws configure
# Access Key ID, Secret Access Key, Region gir
# Yeni proje oluştur
serverless create --template aws-nodejs-typescript --path ecommerce-serverless
cd ecommerce-serverless
# Gerekli plugin'leri kur
npm install --save-dev serverless-offline serverless-plugin-typescript
npm install aws-sdk @aws-sdk/client-dynamodb @aws-sdk/client-sqs
Proje dizin yapısı şu şekilde olmalı:
mkdir -p src/{orders,inventory,notifications,shared}
mkdir -p src/shared/{models,utils,middleware}
# Dosya yapısı
tree src/
# src/
# ├── orders/
# │ ├── createOrder.ts
# │ ├── getOrder.ts
# │ └── updateOrderStatus.ts
# ├── inventory/
# │ ├── updateStock.ts
# │ └── checkAvailability.ts
# ├── notifications/
# │ ├── sendEmail.ts
# │ └── sendSMS.ts
# └── shared/
# ├── models/
# ├── utils/
# └── middleware/
Sipariş Servisi Lambda Fonksiyonu
Gerçek bir örnek üzerinden gidelim. Sipariş oluşturma fonksiyonu şu şekilde yazılabilir:
# serverless.yml - Ana konfigürasyon dosyası
cat > serverless.yml << 'EOF'
service: ecommerce-platform
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs18.x
region: eu-west-1
environment:
ORDERS_TABLE: ${self:service}-orders-${sls:stage}
INVENTORY_TABLE: ${self:service}-inventory-${sls:stage}
ORDER_EVENTS_QUEUE: !Ref OrderEventsQueue
NOTIFICATION_TOPIC: !Ref NotificationTopic
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:GetItem
- dynamodb:UpdateItem
- dynamodb:Query
Resource:
- !GetAtt OrdersTable.Arn
- Effect: Allow
Action:
- sqs:SendMessage
- sqs:ReceiveMessage
- sqs:DeleteMessage
Resource:
- !GetAtt OrderEventsQueue.Arn
- Effect: Allow
Action:
- sns:Publish
Resource:
- !Ref NotificationTopic
functions:
createOrder:
handler: src/orders/createOrder.handler
timeout: 30
memorySize: 256
events:
- http:
path: /orders
method: post
cors: true
environment:
FUNCTION_NAME: createOrder
processOrderEvent:
handler: src/orders/processOrderEvent.handler
timeout: 60
memorySize: 512
events:
- sqs:
arn: !GetAtt OrderEventsQueue.Arn
batchSize: 10
functionResponseType: ReportBatchItemFailures
updateInventory:
handler: src/inventory/updateStock.handler
timeout: 30
memorySize: 256
events:
- sqs:
arn: !GetAtt InventoryUpdateQueue.Arn
batchSize: 5
resources:
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.ORDERS_TABLE}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: orderId
AttributeType: S
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: orderId
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: UserOrdersIndex
KeySchema:
- AttributeName: userId
KeyType: HASH
Projection:
ProjectionType: ALL
OrderEventsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-order-events-${sls:stage}
VisibilityTimeout: 300
RedrivePolicy:
deadLetterTargetArn: !GetAtt OrdersDLQ.Arn
maxReceiveCount: 3
OrdersDLQ:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-orders-dlq-${sls:stage}
MessageRetentionPeriod: 1209600
NotificationTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: ${self:service}-notifications-${sls:stage}
EOF
Order Servisinin İş Mantığı
Şimdi asıl fonksiyon kodunu yazalım. Bu kısım TypeScript ama bash ile test edebileceğin şekilde göstereceğim:
# createOrder.ts içeriğini oluştur
cat > src/orders/createOrder.ts << 'EOF'
import { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
import { v4 as uuidv4 } from 'uuid';
const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
const docClient = DynamoDBDocumentClient.from(dynamoClient);
const sqsClient = new SQSClient({ region: process.env.AWS_REGION });
export const handler: APIGatewayProxyHandler = async (event) => {
try {
const body = JSON.parse(event.body || '{}');
const { userId, items, shippingAddress } = body;
// Input validation
if (!userId || !items || items.length === 0) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'userId ve items zorunludur' })
};
}
const orderId = uuidv4();
const totalAmount = items.reduce((sum: number, item: any) =>
sum + (item.price * item.quantity), 0);
const order = {
orderId,
userId,
items,
shippingAddress,
totalAmount,
status: 'PENDING',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// DynamoDB'ye kaydet
await docClient.send(new PutCommand({
TableName: process.env.ORDERS_TABLE!,
Item: order,
ConditionExpression: 'attribute_not_exists(orderId)'
}));
// SQS'e event gönder - diğer servisler bu event'i dinliyor
await sqsClient.send(new SendMessageCommand({
QueueUrl: process.env.ORDER_EVENTS_QUEUE!,
MessageBody: JSON.stringify({
eventType: 'ORDER_CREATED',
orderId,
userId,
items,
totalAmount,
timestamp: new Date().toISOString()
}),
MessageAttributes: {
eventType: {
DataType: 'String',
StringValue: 'ORDER_CREATED'
}
}
}));
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId, status: 'PENDING', totalAmount })
};
} catch (error) {
console.error('Order creation failed:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
EOF
Dead Letter Queue ve Hata Yönetimi
Production’da en önemli konulardan biri hata yönetimi. Lambda fonksiyonları başarısız olduğunda ne olacak? DLQ burada kritik rol oynuyor:
# DLQ mesajlarını izlemek ve yeniden işlemek için script
cat > scripts/process-dlq.sh << 'EOF'
#!/bin/bash
DLQ_URL="https://sqs.eu-west-1.amazonaws.com/ACCOUNT_ID/ecommerce-platform-orders-dlq-prod"
MAIN_QUEUE_URL="https://sqs.eu-west-1.amazonaws.com/ACCOUNT_ID/ecommerce-platform-order-events-prod"
echo "DLQ mesaj sayısı kontrol ediliyor..."
MESSAGE_COUNT=$(aws sqs get-queue-attributes
--queue-url $DLQ_URL
--attribute-names ApproximateNumberOfMessages
--query 'Attributes.ApproximateNumberOfMessages'
--output text)
echo "DLQ'da $MESSAGE_COUNT mesaj var"
if [ "$MESSAGE_COUNT" -gt "0" ]; then
echo "Mesajlar ana kuyruğa geri gönderiliyor..."
# DLQ'dan mesajları al
MESSAGES=$(aws sqs receive-message
--queue-url $DLQ_URL
--max-number-of-messages 10
--attribute-names All)
# Her mesajı ana kuyruğa gönder
echo $MESSAGES | jq -r '.Messages[] | @base64' | while read msg; do
DECODED=$(echo $msg | base64 -d)
BODY=$(echo $DECODED | jq -r '.Body')
RECEIPT=$(echo $DECODED | jq -r '.ReceiptHandle')
# Ana kuyruğa gönder
aws sqs send-message
--queue-url $MAIN_QUEUE_URL
--message-body "$BODY"
# DLQ'dan sil
aws sqs delete-message
--queue-url $DLQ_URL
--receipt-handle "$RECEIPT"
echo "Mesaj yeniden kuyruğa alındı: $(echo $BODY | jq -r '.orderId')"
done
fi
EOF
chmod +x scripts/process-dlq.sh
CloudWatch ile Monitoring Kurulumu
Serverless mimaride monitoring, container tabanlı yaklaşımdan farklı. Her fonksiyon için ayrı metrikler takip etmen gerekiyor:
# CloudWatch alarm ve dashboard oluşturma
cat > scripts/setup-monitoring.sh << 'EOF'
#!/bin/bash
FUNCTION_PREFIX="ecommerce-platform"
STAGE="prod"
SNS_ALARM_TOPIC="arn:aws:sns:eu-west-1:ACCOUNT_ID:alerts"
# Her Lambda fonksiyonu için error alarm kur
FUNCTIONS=("createOrder" "processOrderEvent" "updateInventory" "sendEmail")
for FUNC in "${FUNCTIONS[@]}"; do
FUNCTION_NAME="${FUNCTION_PREFIX}-${STAGE}-${FUNC}"
# Error rate alarm
aws cloudwatch put-metric-alarm
--alarm-name "${FUNCTION_NAME}-errors"
--alarm-description "${FUNC} fonksiyonu hata alıyor"
--metric-name Errors
--namespace AWS/Lambda
--statistic Sum
--period 300
--threshold 5
--comparison-operator GreaterThanThreshold
--dimensions Name=FunctionName,Value=$FUNCTION_NAME
--evaluation-periods 1
--alarm-actions $SNS_ALARM_TOPIC
--treat-missing-data notBreaching
# Duration alarm - timeout yaklaşıyorsa uyar
aws cloudwatch put-metric-alarm
--alarm-name "${FUNCTION_NAME}-duration"
--alarm-description "${FUNC} fonksiyonu yavaş çalışıyor"
--metric-name Duration
--namespace AWS/Lambda
--statistic p95
--period 300
--threshold 25000
--comparison-operator GreaterThanThreshold
--dimensions Name=FunctionName,Value=$FUNCTION_NAME
--evaluation-periods 2
--alarm-actions $SNS_ALARM_TOPIC
echo "Alarm kuruldu: $FUNCTION_NAME"
done
# Cold start metriği için custom metric gönderme
echo "Custom dashboard oluşturuluyor..."
aws cloudwatch put-dashboard
--dashboard-name "EcommerceServerless"
--dashboard-body '{
"widgets": [
{
"type": "metric",
"properties": {
"title": "Lambda Invocations",
"metrics": [
["AWS/Lambda", "Invocations", "FunctionName", "ecommerce-platform-prod-createOrder"],
["AWS/Lambda", "Invocations", "FunctionName", "ecommerce-platform-prod-processOrderEvent"]
],
"period": 300,
"stat": "Sum",
"view": "timeSeries"
}
}
]
}'
echo "Monitoring kurulumu tamamlandı"
EOF
chmod +x scripts/setup-monitoring.sh
Cold Start Sorununu Çözmek
Serverless mimarisinin en bilinen problemi cold start. Uzun süre istek almayan bir fonksiyon çağrıldığında Lambda container’ı başlatmak zorunda kalıyor ve bu 200ms-2 saniye arasında ekstra gecikme yaratıyor. Bunu birkaç yöntemle azaltabilirsin:
# Provisioned Concurrency ayarla - kritik fonksiyonlar için
aws lambda put-provisioned-concurrency-config
--function-name ecommerce-platform-prod-createOrder
--qualifier prod
--provisioned-concurrent-executions 5
# Warm-up için EventBridge scheduled rule
cat > warmup-rule.sh << 'EOF'
#!/bin/bash
# Her 5 dakikada bir fonksiyonları uyandır
aws events put-rule
--name "lambda-warmup-rule"
--schedule-expression "rate(5 minutes)"
--state ENABLED
# Lambda'yı hedef olarak ekle
aws events put-targets
--rule "lambda-warmup-rule"
--targets '[
{
"Id": "warmup-createOrder",
"Arn": "arn:aws:lambda:eu-west-1:ACCOUNT_ID:function:ecommerce-platform-prod-createOrder",
"Input": "{"source": "warmup", "detail-type": "Warmup", "detail": {}}"
}
]'
echo "Warmup kuralı oluşturuldu"
EOF
chmod +x warmup-rule.sh
# Fonksiyon içinde warmup event'ini handle et
cat > src/shared/middleware/warmup.ts << 'EOF'
export const warmupMiddleware = (handler: Function) => {
return async (event: any, context: any) => {
// Warmup isteği ise hızlıca dön
if (event.source === 'warmup') {
console.log('Warmup isteği - fonksiyon sıcak tutuldu');
return { statusCode: 200, body: 'Warm!' };
}
return handler(event, context);
};
};
EOF
Servisler Arası İletişim Patternleri
Hibrit mimaride en kritik konu servisler arası iletişim. Senkron HTTP çağrıları yerine asenkron event-driven yaklaşımı tercih etmelisin:
# EventBridge ile servisler arası event routing
cat > src/shared/utils/eventBus.ts << 'EOF'
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';
const eventBridgeClient = new EventBridgeClient({ region: process.env.AWS_REGION });
interface DomainEvent {
source: string;
detailType: string;
detail: Record<string, any>;
}
export async function publishEvent(event: DomainEvent): Promise<void> {
const command = new PutEventsCommand({
Entries: [{
Source: `ecommerce.${event.source}`,
DetailType: event.detailType,
Detail: JSON.stringify({
...event.detail,
timestamp: new Date().toISOString(),
eventId: crypto.randomUUID()
}),
EventBusName: process.env.EVENT_BUS_NAME || 'default'
}]
});
const result = await eventBridgeClient.send(command);
if (result.FailedEntryCount && result.FailedEntryCount > 0) {
throw new Error(`Event publish başarısız: ${JSON.stringify(result.Entries)}`);
}
}
// Kullanım örneği
// await publishEvent({
// source: 'orders',
// detailType: 'OrderStatusChanged',
// detail: { orderId: '123', newStatus: 'SHIPPED', userId: 'user456' }
// });
EOF
# EventBridge rule - inventory servisini tetikle
aws events put-rule
--name "order-created-to-inventory"
--event-pattern '{
"source": ["ecommerce.orders"],
"detail-type": ["OrderCreated"]
}'
--state ENABLED
--event-bus-name ecommerce-events
aws events put-targets
--rule "order-created-to-inventory"
--event-bus-name ecommerce-events
--targets '[
{
"Id": "inventory-update-target",
"Arn": "arn:aws:lambda:eu-west-1:ACCOUNT_ID:function:ecommerce-platform-prod-updateInventory"
}
]'
echo "EventBridge routing kuruldu"
Deployment Pipeline
CI/CD pipeline olmadan serverless mimarisi eksik kalır. GitHub Actions ile otomatik deployment:
# .github/workflows/deploy.yml
cat > .github/workflows/deploy.yml << 'EOF'
name: Deploy Serverless
on:
push:
branches:
- main
- staging
jobs:
test-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Node.js Kur
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Bağımlılıkları Yükle
run: npm ci
- name: Unit Testleri Çalıştır
run: npm test
- name: AWS Credentials Ayarla
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Staging Deploy
if: github.ref == 'refs/heads/staging'
run: |
npx serverless deploy --stage staging --verbose
echo "Staging deploy tamamlandı"
- name: Production Deploy
if: github.ref == 'refs/heads/main'
run: |
npx serverless deploy --stage prod --verbose
echo "Production deploy tamamlandı"
- name: Smoke Test
run: |
ENDPOINT=$(npx serverless info --stage prod | grep "POST" | awk '{print $2}')
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST $ENDPOINT/orders
-H "Content-Type: application/json"
-d '{"userId": "smoke-test", "items": [{"productId": "test", "price": 1, "quantity": 1}]}')
if [ "$STATUS" == "201" ]; then
echo "Smoke test başarılı: HTTP $STATUS"
else
echo "Smoke test başarısız: HTTP $STATUS"
exit 1
fi
EOF
echo "Pipeline yapılandırması tamamlandı"
Maliyet Optimizasyonu
Serverless’ın en büyük avantajlarından biri maliyet, ama yanlış yapılandırma faturayı patlatabilir. Şu noktalara dikkat et:
Memory boyutunu optimize et: Lambda’da CPU, memory ile orantılı. 512MB yerine 256MB kullanmak her zaman ucuz değil, bazen daha fazla memory daha hızlı çalıştırır ve toplam maliyet düşer.
Lambda Power Tuning kullan: AWS’nin open source aracı farklı memory konfigürasyonlarını test eder:
# Lambda Power Tuning State Machine çalıştır
aws stepfunctions start-execution
--state-machine-arn arn:aws:states:eu-west-1:ACCOUNT_ID:stateMachine:powerTuningMachine
--input '{
"lambdaARN": "arn:aws:lambda:eu-west-1:ACCOUNT_ID:function:ecommerce-platform-prod-createOrder",
"powerValues": [128, 256, 512, 1024],
"num": 50,
"payload": {"userId": "test", "items": [{"productId": "p1", "price": 10, "quantity": 1}]},
"parallelInvocation": true,
"strategy": "cost"
}'
# Reserved Concurrency ayarla - sınırsız scale'i engelle
aws lambda put-function-concurrency
--function-name ecommerce-platform-prod-createOrder
--reserved-concurrent-executions 100
echo "Maliyet optimizasyonu ayarları tamamlandı"
DynamoDB on-demand billing: Tahmin edilemeyen workload için on-demand, sabit yük için provisioned kullan.
S3 yerine EFS değil: Lambda’da büyük dosya işlemleri için S3 kullan, EFS ekstra maliyet getirir.
Log retention süresi: CloudWatch Logs’u sonsuz tutma, 30-90 gün yeterli.
Yaygın Hatalar ve Çözümleri
Serverless projelerinde sık karşılaştığım hataları özetleyeyim:
Connection pool problemi: Her Lambda invocation’ında yeni DB connection açmak hem yavaş hem pahalı. Connection’ı handler dışında tanımla, warm start’larda yeniden kullanılır.
Timeout yönetimi: Lambda timeout’u SQS visibility timeout’undan küçük olmalı. 30 saniyelik Lambda için en az 35 saniye visibility timeout ayarla.
Circular dependency: Servisler birbirini doğrudan çağırırsa tightly coupled olur. Her zaman event bus üzerinden haberleşin.
Environment variable limiti: Lambda’da 4KB environment variable limiti var. Büyük konfigürasyonları SSM Parameter Store’dan çek.
Bundle size: Node.js’te tüm node_modules’u pakete dahil etme. Tree shaking ve sadece gerekli modülleri import et. Paket boyutu 50MB’ı geçmemeli.
Sonuç
Serverless ve mikroservis mimarisini birleştirmek, doğru yapıldığında gerçekten güçlü bir sistem ortaya çıkarıyor. Özellikle e-ticaret, fintech ve SaaS ürünlerinde bu hibrit yaklaşım hem geliştirici verimliliğini artırıyor hem de operasyonel yükü azaltıyor.
Ama dikkat etmen gereken şey şu: Her şeyi serverless yapma hevesine kapılma. Sürekli çalışan, düşük latency gerektiren ve stateful olan iş yükleri için container tabanlı servisler hala daha uygun. Billing sistemi, authentication servisi, gerçek zamanlı WebSocket bağlantıları bunlara örnek verilebilir.
Doğru yaklaşım şu soruları sormak: Bu iş yükü ne sıklıkla çalışıyor? Latency toleransı nedir? State tutuyor mu? Cevaplara göre serverless mu container mi sorusu kendiliğinden cevap buluyor.
Production’a geçmeden önce mutlaka Load testing yap, DLQ’ları konfigüre et, CloudWatch alarm’larını kur ve maliyet tahminini hesapla. Serverless’ın “sihirli çözüm” olmadığını, sadece doğru araç için doğru yerde kullanıldığında değer yarattığını aklında tut.
