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.

Bir yanıt yazın

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