Serverless Mimaride Çoklu Bulut Stratejisi: Vendor Bağımlılığından Kurtulma Rehberi
Serverless dünyasına adım attığınızda her şey güzel görünür: sıfır sunucu yönetimi, otomatik ölçekleme, kullandığın kadar öde modeli. Sonra bir gün fark edersiniz ki uygulamanızın her köşesi tek bir bulut sağlayıcısına kilitlenmiş. AWS Lambda’dan Azure Functions’a geçmek istediğinizde ya da GCP Cloud Run’ı test etmeye kalkıştığınızda gerçekle yüzleşirsiniz: vendor lock-in tuzağına düşmüşsünüzdür. Bu yazıda bu tuzaktan nasıl çıkacağınızı, serverless mimaride çoklu bulut stratejisini nasıl kuracağınızı ve gerçek dünya senaryolarıyla pratik araçları ele alacağız.
Vendor Lock-in Neden Bu Kadar Sinsi Bir Sorun?
Serverless’ta vendor lock-in diğer bulut mimarilerinden çok daha tehlikeli bir hal alıyor çünkü sizi sıkıştıran şey sadece compute katmanı değil. AWS’de Lambda yazarken aynı zamanda API Gateway, DynamoDB, SQS, EventBridge ve Cognito kullanıyorsunuz. Azure’da Functions yazarken Cosmos DB, Service Bus ve Active Directory entegrasyonlarına giriyorsunuz. Her servis bir çengel, her entegrasyon bir zincir.
Gerçek hayatta bununla nasıl karşılaşırsınız? Şöyle düşünün: 18 ay önce AWS üzerine serverless bir e-ticaret platformu kurduğunuz. Şimdi müşteriniz Avrupa’da veri egemenliği yasaları nedeniyle workload’ların bir kısmını Azure’a taşımak istiyor. Ya da AWS’nin fiyatlandırması değişti ve GCP sizin için %40 daha ucuz bir seçenek haline geldi. İşte o an anlıyorsunuz ki migration’ın maliyeti, tasarrufun çok üzerinde.
Çoklu Bulut Stratejisinin Temelleri
Multi-cloud serverless stratejisi üç temel prensip üzerine inşa edilir:
- Portabilite: Fonksiyonlarınızın herhangi bir cloud provider’da çalışabilmesi
- Abstraction: Cloud-specific servislerin soyutlanması
- Standardizasyon: Deployment, monitoring ve logging’in tek bir yerden yönetilmesi
Bu prensipleri hayata geçirmek için kullanabileceğiniz temel araç ailesi şunlar: Serverless Framework, Terraform, Pulumi ve container tabanlı yaklaşımlar (Knative gibi).
Serverless Framework ile Provider-Agnostic Fonksiyonlar Yazmak
Serverless Framework bugün hala en yaygın kullanılan çoklu bulut abstraction aracı. Şu an kullandığınız provider’dan bağımsız olarak aynı serverless.yml yapısını kullanabilirsiniz.
Önce temel bir proje yapısı oluşturalım:
mkdir multi-cloud-serverless && cd multi-cloud-serverless
npm install -g serverless
serverless create --template aws-nodejs --path order-service
cd order-service
npm init -y
npm install --save-dev serverless-dotenv-plugin
Şimdi provider-agnostic bir serverless.yml yapısı kuralım:
cat > serverless.yml << 'EOF'
service: order-service
frameworkVersion: '3'
plugins:
- serverless-dotenv-plugin
provider:
name: ${opt:provider, 'aws'}
runtime: nodejs18.x
region: ${opt:region, 'eu-west-1'}
environment:
DB_CONNECTION: ${env:DB_CONNECTION}
QUEUE_URL: ${env:QUEUE_URL}
STAGE: ${opt:stage, 'dev'}
functions:
createOrder:
handler: src/handlers/createOrder.handler
events:
- http:
path: orders
method: post
processOrder:
handler: src/handlers/processOrder.handler
events:
- schedule: rate(5 minutes)
EOF
Buradaki kritik nokta ${opt:provider, 'aws'} kullanımı. Deployment sırasında --provider azure diyerek provider’ı değiştirebilirsiniz. Ancak bu yeterli değil çünkü event trigger’ları provider’a göre farklılık gösteriyor.
Abstraction Layer: Cloud-Specific Servisleri Soyutlamak
Asıl iş buradan başlıyor. Handler kodunuzu doğrudan AWS SDK veya Azure SDK ile yazmak yerine, bir abstraction layer oluşturmanız gerekiyor. Pratik bir örnek görelim:
mkdir -p src/adapters src/handlers src/services
cat > src/adapters/storageAdapter.js << 'EOF'
const provider = process.env.CLOUD_PROVIDER || 'aws';
let storageClient;
if (provider === 'aws') {
const { DynamoDBClient, PutItemCommand, GetItemCommand } = require('@aws-sdk/client-dynamodb');
const client = new DynamoDBClient({ region: process.env.AWS_REGION });
storageClient = {
async save(tableName, item) {
const command = new PutItemCommand({
TableName: tableName,
Item: item
});
return client.send(command);
},
async get(tableName, key) {
const command = new GetItemCommand({
TableName: tableName,
Key: key
});
return client.send(command);
}
};
} else if (provider === 'azure') {
const { CosmosClient } = require('@azure/cosmos');
const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING);
storageClient = {
async save(containerName, item) {
const container = client
.database(process.env.COSMOS_DB_NAME)
.container(containerName);
return container.items.create(item);
},
async get(containerName, key) {
const container = client
.database(process.env.COSMOS_DB_NAME)
.container(containerName);
return container.item(key.id, key.partitionKey).read();
}
};
} else if (provider === 'gcp') {
const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();
storageClient = {
async save(collectionName, item) {
return firestore.collection(collectionName).add(item);
},
async get(collectionName, id) {
const doc = await firestore.collection(collectionName).doc(id).get();
return doc.data();
}
};
}
module.exports = storageClient;
EOF
Bu yaklaşım başlangıç için işe yarıyor ama uzun vadede bakımı zorlaşabiliyor. Daha iyi bir yöntem: facade pattern kullanarak her provider için ayrı modüller yazıp bunları merkezi bir factory’den yönetmek.
Terraform ile Multi-Cloud Infrastructure as Code
Fonksiyon kodunu taşınabilir hale getirdiniz, şimdi infrastructure’ı da taşınabilir yapmanız lazım. Terraform burada en güçlü seçenek çünkü tüm büyük provider’ları destekliyor.
mkdir -p terraform/modules/serverless-function
mkdir -p terraform/environments/aws
mkdir -p terraform/environments/azure
cat > terraform/modules/serverless-function/variables.tf << 'EOF'
variable "function_name" {
description = "Serverless function adi"
type = string
}
variable "runtime" {
description = "Calisma ortami"
type = string
default = "nodejs18.x"
}
variable "memory_size" {
description = "MB cinsinden bellek"
type = number
default = 512
}
variable "timeout" {
description = "Saniye cinsinden timeout"
type = number
default = 30
}
variable "environment_variables" {
description = "Ortam degiskenleri"
type = map(string)
default = {}
}
variable "source_zip_path" {
description = "Fonksiyon kodunun zip yolu"
type = string
}
EOF
Şimdi AWS için somut bir Terraform modülü yazalım:
cat > terraform/environments/aws/main.tf << 'EOF'
provider "aws" {
region = var.aws_region
}
module "order_function" {
source = "../../modules/serverless-function"
function_name = "order-service-${var.environment}"
runtime = "nodejs18.x"
memory_size = 512
timeout = 30
source_zip_path = "../../../dist/order-service.zip"
environment_variables = {
CLOUD_PROVIDER = "aws"
DB_TABLE_NAME = aws_dynamodb_table.orders.name
QUEUE_URL = aws_sqs_queue.order_queue.url
STAGE = var.environment
}
}
resource "aws_dynamodb_table" "orders" {
name = "orders-${var.environment}"
billing_mode = "PAY_PER_REQUEST"
hash_key = "orderId"
attribute {
name = "orderId"
type = "S"
}
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_sqs_queue" "order_queue" {
name = "order-queue-${var.environment}"
visibility_timeout_seconds = 60
message_retention_seconds = 86400
}
EOF
CI/CD Pipeline: Tek Pipeline, Birden Fazla Cloud
Çoklu bulut stratejisinin en zorlu kısmı deployment pipeline’ını yönetmek. GitHub Actions kullanarak provider’a göre otomatik deployment yapan bir workflow örneği:
cat > .github/workflows/multi-cloud-deploy.yml << 'EOF'
name: Multi-Cloud Serverless Deploy
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Node.js Kur
uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm test
- name: Kod paketini hazirla
run: |
npm run build
cd dist && zip -r ../order-service.zip .
deploy-aws-prod:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: aws-production
steps:
- uses: actions/checkout@v3
- name: AWS Credentials
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: Terraform Init ve Deploy
run: |
cd terraform/environments/aws
terraform init
terraform plan -var="environment=prod"
terraform apply -auto-approve -var="environment=prod"
deploy-azure-dr:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: azure-dr
steps:
- uses: actions/checkout@v3
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure Functions Deploy
run: |
cd terraform/environments/azure
terraform init
terraform apply -auto-approve
-var="environment=dr"
-var="subscription_id=${{ secrets.AZURE_SUBSCRIPTION_ID }}"
EOF
Monitoring ve Observability: Her Şeyi Tek Yerden Görmek
Multi-cloud mimarisinde en büyük operasyonel sorun şu: AWS CloudWatch’ta bir alarm var, Azure Monitor’da başka bir sorun var, GCP Cloud Logging’de de bir hata var. Bunları tek bir yerden takip etmek zorundasınız.
OpenTelemetry burada kurtarıcınız oluyor. Provider-agnostic bir observability standard olarak tüm bulut ortamlarından metrik, log ve trace toplayıp merkezi bir platforma (Grafana, Datadog veya kendi kurduğunuz Prometheus stack’i) gönderebilirsiniz.
npm install @opentelemetry/api @opentelemetry/sdk-node
@opentelemetry/auto-instrumentations-node
@opentelemetry/exporter-otlp-http
cat > src/telemetry/tracer.js << 'EOF'
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-otlp-http');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const exporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://otel-collector:4318/v1/traces',
headers: {
'x-cloud-provider': process.env.CLOUD_PROVIDER || 'unknown',
'x-environment': process.env.STAGE || 'dev'
}
});
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'order-service',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.STAGE || 'dev',
'cloud.provider': process.env.CLOUD_PROVIDER || 'unknown',
'cloud.region': process.env.CLOUD_REGION || 'unknown'
}),
traceExporter: exporter,
instrumentations: [getNodeAutoInstrumentations()]
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().then(() => process.exit(0));
});
module.exports = sdk;
EOF
Bu yapı sayesinde AWS Lambda’dan gelen trace ile Azure Functions’tan gelen trace’i aynı Grafana dashboard’unda görebilirsiniz.
Gerçek Dünya Senaryosu: Disaster Recovery Stratejisi
Bir e-ticaret müşterim için şöyle bir senaryo yaşadık: AWS eu-west-1 bölgesinde cidli bir outage oldu ve sipariş sistemi 40 dakika çöktü. Bunun ardından multi-cloud DR stratejisi kurduk. Temel fikir şu: kritik fonksiyonlar her iki bulutta da deploy edilmiş olacak ama normalde sadece AWS çalışacak. Sorun çıktığında Route 53 health check’leri otomatik olarak Azure’daki endpoint’lere yönlendirecek.
Bu yaklaşımda dikkat etmeniz gereken şeyler:
- Data sync: DynamoDB ile Cosmos DB arasında gerçek zamanlı ya da near-realtime senkronizasyon zorunlu
- State management: Hangi siparişlerin işlendiğini her iki bulutta da tutarlı tutmak
- Cold start: DR ortamı nadiren trafik aldığı için cold start sürelerini minimize etmek için provisioned concurrency kullanmak
- Maliyet: İki ortamı da hazır tutmak ciddi maliyet getirebilir, bunu hybrid bir şekilde yönetmek gerekiyor
Çoğu zaman “active-active” yerine “active-passive” yaklaşımı daha mantıklı oluyor. DR ortamındaki fonksiyonları minimal kapasitede hazır tutup yük devri anında hızla scale etmek daha ekonomik.
Knative ile Konteyner Tabanlı Serverless
Eğer vendor lock-in’den gerçekten kurtulmak istiyorsanız, en radikal ama en etkili çözüm Knative kullanmak. Knative, Kubernetes üzerinde çalışan open source bir serverless platform. AWS, Azure, GCP veya kendi on-premise Kubernetes cluster’ınızda aynı şekilde çalışıyor.
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.11.0/serving-crds.yaml
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.11.0/serving-core.yaml
cat > knative-order-service.yaml << 'EOF'
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: order-service
namespace: production
labels:
app: order-service
version: "1.0.0"
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "100"
autoscaling.knative.dev/target: "50"
spec:
containerConcurrency: 50
timeoutSeconds: 30
containers:
- image: your-registry/order-service:1.0.0
ports:
- containerPort: 8080
env:
- name: CLOUD_PROVIDER
value: "knative"
- name: DB_CONNECTION
valueFrom:
secretKeyRef:
name: order-service-secrets
key: db-connection
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
EOF
kubectl apply -f knative-order-service.yaml
Knative ile şunu elde ediyorsunuz: aynı YAML manifestini AWS EKS’te, Azure AKS’te, GCP GKE’de veya kendi bare-metal Kubernetes cluster’ınızda çalıştırabiliyorsunuz. Bu gerçek anlamda vendor bağımsızlığı.
Dikkat Edilmesi Gereken Tuzaklar
Multi-cloud serverless stratejisi kurarken birkaç konuda gerçekçi olmak lazım:
- Complexity artışı: İki cloud yönetmek iki katlı değil, üç ila dört katlı operasyonel yük demek. Küçük ekipler için bunu sürdürmek zor olabiliyor.
- Network latency: Bulutlar arası veri transferi hem maliyetli hem yavaş. Egress ücretleri göz ardı edilemez.
- Lowest common denominator riski: Her provider’da çalışmak zorunda olan kod, her provider’ın en iyi özelliklerinden yararlanamıyor. AWS Lambda’nın SnapStart’ını Knative’de kullanamazsınız.
- Secret management: Her bulut için ayrı secret yönetimi karmaşıklığı. HashiCorp Vault gibi provider-agnostic bir çözüm burada zorunlu hale geliyor.
- Testing zorluğu: Lokalde tüm cloud provider davranışlarını simüle etmek için LocalStack, Azurite gibi araçların birlikte konfigüre edilmesi gerekiyor.
Hangi Strateji Ne Zaman Uygun?
Her projeye “hadi multi-cloud yapalım” demek yanlış. Durum değerlendirmesini şöyle yapabilirsiniz:
- Eğer küçük bir startup’sınız ve ayda 10.000 dolardan az bulut harcaması yapıyorsanız, multi-cloud’un getireceği operasyonel yük muhtemelen faydasından ağır basar.
- Eğer kritik bir üretim sistemini yönetiyorsanız ve tek bir provider’ın outage’ı işinizi durduruyorsa, DR odaklı bir multi-cloud stratejisi mantıklı.
- Eğer Avrupa’da GDPR, Türkiye’de KVKK gibi veri egemenliği gereksinimleri varsa ve bunlar farklı provider’lara işaret ediyorsa, multi-cloud zorunluluk haline geliyor.
- Eğer maliyet optimizasyonu için workload’ları provider’lar arasında kaydırmak istiyorsanız, bunu otomatize edecek bir platform yapısına ihtiyacınız var.
Sonuç
Serverless mimaride vendor bağımlılığından kurtulmak, tek bir araç ya da tek bir kararla olmuyor. Bu bir strateji ve bu stratejiyi katman katman inşa etmeniz gerekiyor: önce abstraction layer ile fonksiyon kodunu taşınabilir yapıyorsunuz, sonra Terraform ile infrastructure’ı kod haline getirip provider’dan bağımsız kılıyorsunuz, ardından CI/CD pipeline’ınızı çoklu deployment’ı destekler hale getiriyorsunuz ve son olarak OpenTelemetry ile observability’yi merkezi bir noktaya topluyorsunuz.
Knative gibi araçlar uzun vadede en temiz çözümü sunuyor ancak öğrenme eğrisi ve Kubernetes operasyonel yüküyle birlikte geliyor. Serverless Framework ise mevcut takımların daha hızlı adapte olabileceği, pragmatik bir başlangıç noktası.
En pratik öneri şu: bugün tek bir provider kullanıyor olsanız bile, kodunuzu sanki yarın taşınacakmış gibi yazın. Cloud-specific API’leri doğrudan değil, bir adapter üzerinden çağırın. Bu alışkanlık sizi gelecekte hem büyük migration maliyetlerinden kurtarır hem de multi-cloud stratejisine geçişi çok daha az acılı hale getirir. Vendor lock-in’in maliyeti genellikle göçün maliyetiyle yüz yüze gelene kadar görünmez, ama o noktada iş işten geçmiş olur.
