Azure Functions ile Cosmos DB Bağlantısı Nasıl Kurulur

Serverless dünyasında en çok sorulan sorulardan biri şu: “Azure Functions’ı Cosmos DB ile nasıl düzgün bağlarım?” Kulağa basit geliyor ama işin içine girince connection string yönetimi, binding konfigürasyonu, partition key seçimi ve maliyet optimizasyonu gibi konular ortaya çıkıyor. Bu yazıda sıfırdan gerçek dünya senaryolarıyla bu entegrasyonu ele alacağız.

Azure Functions ve Cosmos DB: Neden Bu İkili?

Azure Functions, olay güdümlü iş yüklerini çalıştırmak için mükemmel bir platform. Cosmos DB ise global ölçekte dağıtılmış, düşük gecikmeli bir NoSQL veritabanı. Bu ikilinin bir araya gelmesi özellikle şu senaryolarda çok mantıklı:

  • IoT cihazlarından gelen veri akışlarını işlemek
  • E-ticaret siparişlerini asenkron olarak kaydetmek
  • API backend’i olarak serverless CRUD operasyonları
  • Change feed ile gerçek zamanlı veri senkronizasyonu

Klasik VM tabanlı bir uygulama yerine bu mimariyi kullandığınızda hem maliyet hem de ölçeklenebilirlik açısından ciddi avantaj elde ediyorsunuz. Ancak “az kod yazdım, az sorun yaşarım” yanılgısına düşmeyin. Yapılandırma hataları production’da beklenmedik masraflar ve performans sorunları yaratıyor.

Geliştirme Ortamını Hazırlamak

Başlamadan önce local ortamınızı düzgün kurmanız gerekiyor. Azure Functions’ı local olarak geliştirmek için Azure Functions Core Tools şart.

# Node.js üzerinden Azure Functions Core Tools kurulumu
npm install -g azure-functions-core-tools@4 --unsafe-perm true

# Kurulumu doğrulama
func --version

# Azure CLI kurulumu (Ubuntu/Debian)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Azure'a login
az login

# Gerekli extension'ları yükle
az extension add --name functionapp

Local geliştirme için Cosmos DB Emulator’ü de kurabilirsiniz. Windows’ta MSI paketi var, Linux’ta ise Docker ile çalıştırmanız gerekiyor:

# Docker ile Cosmos DB Emulator başlatma
docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator

docker run 
  --publish 8081:8081 
  --publish 10250-10255:10250-10255 
  --name cosmosdb-emulator 
  --detach 
  mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator

# Emulator'ün hazır olup olmadığını kontrol et
curl -k https://localhost:8081/_explorer/index.html

Emulator’ün primary key’i her zaman sabit: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b1n9inGJO07OD8gHNHKsisVJrIHaEIg==. Production’da asla bu key’i kullanmayın, ama local geliştirmede işinizi görecek.

Functions Projesi Oluşturmak

Yeni bir Azure Functions projesi oluşturalım. Python kullanacağız çünkü hem Cosmos DB SDK’sı çok olgun hem de Azure Functions’ta en yaygın kullanılan dillerden biri.

# Yeni Functions projesi oluştur
func init CosmosDBDemo --python

cd CosmosDBDemo

# HTTP trigger'lı bir function oluştur
func new --name GetProducts --template "HTTP trigger" --authlevel "function"

# Timer trigger'lı bir function oluştur
func new --name SyncInventory --template "Timer trigger"

# Proje bağımlılıklarını yükle
pip install azure-cosmos azure-functions azure-identity

# Bağımlılıkları requirements.txt'e kaydet
pip freeze > requirements.txt

Proje dizin yapısı şöyle görünmeli:

  • CosmosDBDemo/: Ana proje klasörü
  • GetProducts/: HTTP trigger function
  • SyncInventory/: Timer trigger function
  • host.json: Functions host konfigürasyonu
  • local.settings.json: Local geliştirme ayarları
  • requirements.txt: Python bağımlılıkları

Bağlantı Yönetimi: Connection String mi, Managed Identity mi?

Burası kritik. Çoğu tutorial connection string kullanıyor ama production ortamda Managed Identity kullanmak çok daha güvenli ve operasyonel açıdan daha az baş ağrısı yaratıyor.

local.settings.json dosyasını düzenleyin:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "COSMOS_DB_ENDPOINT": "https://your-account.documents.azure.com:443/",
    "COSMOS_DB_KEY": "your-primary-key-here",
    "COSMOS_DB_DATABASE": "ProductsDB",
    "COSMOS_DB_CONTAINER": "products"
  }
}

Production’da bu değerleri Key Vault’a taşıyıp Function App’in Managed Identity’si üzerinden erişeceksiniz. Bunu daha sonra ele alacağız.

Şimdi Cosmos DB bağlantısını yönetecek bir yardımcı modül oluşturalım:

# shared/cosmos_client.py
import os
import logging
from azure.cosmos import CosmosClient, PartitionKey, exceptions
from azure.identity import ManagedIdentityCredential, DefaultAzureCredential

logger = logging.getLogger(__name__)

class CosmosDBManager:
    _instance = None
    _client = None
    _database = None
    _container = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def get_container(self):
        """Singleton pattern ile container bağlantısını döndür"""
        if self._container is not None:
            return self._container

        endpoint = os.environ["COSMOS_DB_ENDPOINT"]
        database_name = os.environ["COSMOS_DB_DATABASE"]
        container_name = os.environ["COSMOS_DB_CONTAINER"]

        # Local geliştirmede key kullan, production'da Managed Identity
        if os.environ.get("AZURE_FUNCTIONS_ENVIRONMENT") == "Development":
            key = os.environ["COSMOS_DB_KEY"]
            self._client = CosmosClient(endpoint, key)
            logger.info("Cosmos DB bağlantısı key ile kuruldu (Development)")
        else:
            credential = DefaultAzureCredential()
            self._client = CosmosClient(endpoint, credential)
            logger.info("Cosmos DB bağlantısı Managed Identity ile kuruldu")

        self._database = self._client.get_database_client(database_name)
        self._container = self._database.get_container_client(container_name)
        
        return self._container

    def health_check(self):
        """Bağlantı sağlığını kontrol et"""
        try:
            container = self.get_container()
            # Minimal bir okuma ile bağlantıyı test et
            list(container.query_items(
                query="SELECT VALUE COUNT(1) FROM c",
                enable_cross_partition_query=True
            ))
            return True
        except exceptions.CosmosHttpResponseError as e:
            logger.error(f"Cosmos DB health check başarısız: {e.message}")
            return False

cosmos_manager = CosmosDBManager()

Singleton pattern kullanmamızın sebebi önemli: Her Function invocation’ında yeni bir Cosmos DB client oluşturmak hem maliyetli hem de gereksiz. Aynı worker process içinde bağlantıyı yeniden kullanmak performansı ciddi artırıyor.

HTTP Trigger ile CRUD Operasyonları

Gerçek bir e-ticaret senaryosu düşünelim. Ürünleri Cosmos DB’de tutacağız ve Azure Functions üzerinden yöneteceğiz.

# GetProducts/__init__.py
import json
import logging
import azure.functions as func
from azure.cosmos import exceptions
from shared.cosmos_client import cosmos_manager

logger = logging.getLogger(__name__)

def main(req: func.HttpRequest) -> func.HttpResponse:
    logger.info("GetProducts function tetiklendi")
    
    method = req.method
    product_id = req.route_params.get("id")
    category = req.params.get("category")

    try:
        container = cosmos_manager.get_container()

        if method == "GET":
            if product_id:
                # Tek ürün getir
                try:
                    item = container.read_item(
                        item=product_id,
                        partition_key=category or product_id
                    )
                    return func.HttpResponse(
                        json.dumps(item),
                        mimetype="application/json",
                        status_code=200
                    )
                except exceptions.CosmosResourceNotFoundError:
                    return func.HttpResponse(
                        json.dumps({"error": "Ürün bulunamadı"}),
                        mimetype="application/json",
                        status_code=404
                    )
            else:
                # Tüm ürünleri listele (kategori filtresi ile)
                if category:
                    query = "SELECT * FROM c WHERE c.category = @category"
                    params = [{"name": "@category", "value": category}]
                else:
                    query = "SELECT * FROM c"
                    params = []

                items = list(container.query_items(
                    query=query,
                    parameters=params,
                    enable_cross_partition_query=True,
                    max_item_count=100
                ))
                
                return func.HttpResponse(
                    json.dumps({"items": items, "count": len(items)}),
                    mimetype="application/json",
                    status_code=200
                )

        elif method == "POST":
            # Yeni ürün ekle
            product = req.get_json()
            
            if not product.get("id") or not product.get("category"):
                return func.HttpResponse(
                    json.dumps({"error": "id ve category alanları zorunlu"}),
                    mimetype="application/json",
                    status_code=400
                )

            created = container.create_item(body=product)
            
            return func.HttpResponse(
                json.dumps(created),
                mimetype="application/json",
                status_code=201
            )

        elif method == "DELETE":
            if not product_id or not category:
                return func.HttpResponse(
                    json.dumps({"error": "id ve category parametreleri gerekli"}),
                    mimetype="application/json",
                    status_code=400
                )
            
            container.delete_item(item=product_id, partition_key=category)
            return func.HttpResponse(status_code=204)

    except exceptions.CosmosHttpResponseError as e:
        logger.error(f"Cosmos DB hatası: {e.message}, status: {e.status_code}")
        return func.HttpResponse(
            json.dumps({"error": "Veritabanı hatası", "detail": e.message}),
            mimetype="application/json",
            status_code=500
        )

Cosmos DB Change Feed ile Event-Driven Mimari

Change Feed, Cosmos DB’nin en güçlü özelliklerinden biri. Container’daki her değişikliği yakalayıp Azure Functions ile işleyebilirsiniz. Bunu envanter senkronizasyonu için kullanalım:

# SyncInventory/__init__.py
import json
import logging
import azure.functions as func
from typing import List

logger = logging.getLogger(__name__)

def main(documents: func.DocumentList) -> None:
    """
    Cosmos DB Change Feed trigger - ürün değişikliklerini yakala
    function.json'da CosmosDBTrigger olarak konfigüre edilmeli
    """
    if not documents:
        logger.info("İşlenecek değişiklik yok")
        return

    logger.info(f"{len(documents)} adet değişiklik tespit edildi")
    
    for doc in documents:
        doc_dict = dict(doc)
        doc_id = doc_dict.get("id", "unknown")
        doc_type = doc_dict.get("type", "unknown")
        
        logger.info(f"Değişiklik işleniyor: {doc_id}, tip: {doc_type}")
        
        if doc_type == "product":
            handle_product_change(doc_dict)
        elif doc_type == "order":
            handle_order_change(doc_dict)
        else:
            logger.warning(f"Bilinmeyen döküman tipi: {doc_type}")

def handle_product_change(product: dict):
    """Ürün değişikliklerini işle"""
    product_id = product.get("id")
    stock = product.get("stock", 0)
    
    if stock < 10:
        logger.warning(f"Düşük stok uyarısı: {product_id}, stok: {stock}")
        # Burada notification service çağrısı yapılabilir
        send_low_stock_alert(product_id, stock)

def handle_order_change(order: dict):
    """Sipariş değişikliklerini işle"""
    order_id = order.get("id")
    status = order.get("status")
    
    logger.info(f"Sipariş güncellendi: {order_id}, durum: {status}")
    # Downstream sistemlere bildirim gönder

def send_low_stock_alert(product_id: str, stock: int):
    """Düşük stok için uyarı gönder"""
    # Service Bus, Event Grid veya SMTP üzerinden bildirim
    logger.info(f"Düşük stok alarmı gönderiliyor: {product_id}")

Change Feed trigger için function.json dosyasını doğru yapılandırmanız gerekiyor:

# SyncInventory/function.json içeriği
cat > SyncInventory/function.json << 'EOF'
{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "type": "cosmosDBTrigger",
      "name": "documents",
      "direction": "in",
      "leaseCollectionName": "leases",
      "connectionStringSetting": "COSMOS_DB_CONNECTION_STRING",
      "databaseName": "ProductsDB",
      "collectionName": "products",
      "createLeaseCollectionIfNotExists": true,
      "feedPollDelay": 1000,
      "startFromBeginning": false
    }
  ]
}
EOF

Önemli not: Change Feed için ayrı bir leases container’ı oluşturulur. Bu container, hangi değişikliklerin işlendiğini takip eder. Birden fazla Function instance’ı aynı anda çalışırken partition dağılımını bu lease mekanizması yönetir.

Production Deployment ve Managed Identity

Artık production ortamına geçme zamanı. Managed Identity kullanarak güvenli bir deployment yapalım:

# Resource Group oluştur
az group create --name rg-cosmosdb-demo --location westeurope

# Cosmos DB hesabı oluştur
az cosmosdb create 
  --name cosmosdb-demo-prod 
  --resource-group rg-cosmosdb-demo 
  --default-consistency-level Session 
  --locations regionName=westeurope failoverPriority=0 isZoneRedundant=true

# Database ve container oluştur
az cosmosdb sql database create 
  --account-name cosmosdb-demo-prod 
  --resource-group rg-cosmosdb-demo 
  --name ProductsDB

az cosmosdb sql container create 
  --account-name cosmosdb-demo-prod 
  --resource-group rg-cosmosdb-demo 
  --database-name ProductsDB 
  --name products 
  --partition-key-path "/category" 
  --throughput 400

# Function App oluştur
az functionapp create 
  --resource-group rg-cosmosdb-demo 
  --consumption-plan-location westeurope 
  --runtime python 
  --runtime-version 3.11 
  --functions-version 4 
  --name funcapp-cosmosdb-demo 
  --storage-account stcosmosdbdemo 
  --assign-identity "[system]"

# Managed Identity'nin Cosmos DB'ye erişim iznini ver
PRINCIPAL_ID=$(az functionapp identity show 
  --name funcapp-cosmosdb-demo 
  --resource-group rg-cosmosdb-demo 
  --query principalId --output tsv)

COSMOS_ACCOUNT_ID=$(az cosmosdb show 
  --name cosmosdb-demo-prod 
  --resource-group rg-cosmosdb-demo 
  --query id --output tsv)

az cosmosdb sql role assignment create 
  --account-name cosmosdb-demo-prod 
  --resource-group rg-cosmosdb-demo 
  --role-definition-name "Cosmos DB Built-in Data Contributor" 
  --scope "$COSMOS_ACCOUNT_ID" 
  --principal-id "$PRINCIPAL_ID"

# Function App'e ortam değişkenlerini ekle
az functionapp config appsettings set 
  --name funcapp-cosmosdb-demo 
  --resource-group rg-cosmosdb-demo 
  --settings 
    COSMOS_DB_ENDPOINT="https://cosmosdb-demo-prod.documents.azure.com:443/" 
    COSMOS_DB_DATABASE="ProductsDB" 
    COSMOS_DB_CONTAINER="products"

# Uygulamayı deploy et
func azure functionapp publish funcapp-cosmosdb-demo

Performans ve Maliyet Optimizasyonu

Cosmos DB maliyetleri sizi şoke edebilir. İşte dikkat etmeniz gerekenler:

Request Unit (RU) Yönetimi

Her okuma/yazma operasyonu belirli sayıda RU tüketir. Partition key seçiminiz bunu doğrudan etkiler.

  • Kötü partition key seçimi: Hot partition sorununa yol açar, bazı partition’lar aşırı yüklenirken diğerleri boş kalır
  • İyi partition key: Veriyi eşit dağıtan, sorgularınızla uyumlu bir alan seçin
  • /category gibi düşük kardinaliteli alanlar yerine /id veya /userId daha iyi dağılım sağlar

Indexing Politikasını Özelleştirin

Cosmos DB varsayılan olarak tüm alanları indeksler. Bu okuma sorgularını hızlandırır ama yazma maliyetini artırır. Yüksek yazma hacminiz varsa:

# Sadece belirli alanları indeksle
az cosmosdb sql container update 
  --account-name cosmosdb-demo-prod 
  --resource-group rg-cosmosdb-demo 
  --database-name ProductsDB 
  --name products 
  --idx '{
    "indexingMode": "consistent",
    "includedPaths": [
      {"path": "/category/?"},
      {"path": "/price/?"},
      {"path": "/status/?"}
    ],
    "excludedPaths": [
      {"path": "/description/*"},
      {"path": "/imageData/*"},
      {"path": "/_etag/?"}
    ]
  }'

Autoscale vs Provisioned Throughput

  • Az ama yoğun trafik: Provisioned throughput ile reserved capacity satın alın
  • Düzensiz trafik: Autoscale kullanın (400 ile 4000 RU/s arası otomatik ölçeklenir)
  • Tamamen unpredictable trafik: Serverless Cosmos DB modunu değerlendirin
# Autoscale ile container oluştur
az cosmosdb sql container create 
  --account-name cosmosdb-demo-prod 
  --resource-group rg-cosmosdb-demo 
  --database-name ProductsDB 
  --name orders 
  --partition-key-path "/userId" 
  --max-throughput 4000

Hata Yönetimi ve Retry Mekanizması

Production’da Cosmos DB zaman zaman 429 (Too Many Requests) döndürebilir. Bu hatayı düzgün yönetmek şart:

# shared/retry_helper.py
import time
import logging
from functools import wraps
from azure.cosmos import exceptions

logger = logging.getLogger(__name__)

def cosmos_retry(max_retries=3, base_delay=1.0):
    """
    Cosmos DB için exponential backoff retry decorator
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except exceptions.CosmosHttpResponseError as e:
                    if e.status_code == 429:
                        # Rate limit - retry-after header'ına bak
                        retry_after = e.headers.get("x-ms-retry-after-ms", 1000)
                        wait_time = int(retry_after) / 1000
                        logger.warning(
                            f"Rate limit aşıldı. {wait_time}s sonra tekrar denenecek. "
                            f"Deneme: {retries + 1}/{max_retries}"
                        )
                        time.sleep(wait_time)
                        retries += 1
                    elif e.status_code in [503, 500]:
                        # Geçici servis hatası
                        wait_time = base_delay * (2 ** retries)
                        logger.warning(
                            f"Servis hatası ({e.status_code}). "
                            f"{wait_time}s sonra tekrar denenecek."
                        )
                        time.sleep(wait_time)
                        retries += 1
                    else:
                        # Retry'a gerek yok
                        raise
            
            raise Exception(f"Maksimum retry sayısına ulaşıldı ({max_retries})")
        return wrapper
    return decorator

# Kullanım örneği
@cosmos_retry(max_retries=3, base_delay=0.5)
def safe_upsert(container, item):
    return container.upsert_item(body=item)

Monitoring ve Alerting

Functions ve Cosmos DB’yi Application Insights ile izlemeniz gerekiyor. Temel metriklere alert kurun:

# Application Insights bileşeni oluştur
az monitor app-insights component create 
  --app ai-cosmosdb-demo 
  --location westeurope 
  --resource-group rg-cosmosdb-demo 
  --kind web

# Function App'e bağla
INSTRUMENTATION_KEY=$(az monitor app-insights component show 
  --app ai-cosmosdb-demo 
  --resource-group rg-cosmosdb-demo 
  --query instrumentationKey --output tsv)

az functionapp config appsettings set 
  --name funcapp-cosmosdb-demo 
  --resource-group rg-cosmosdb-demo 
  --settings APPINSIGHTS_INSTRUMENTATIONKEY="$INSTRUMENTATION_KEY"

# Cosmos DB için RU tüketim alarmı kur
az monitor metrics alert create 
  --name "HighRUConsumption" 
  --resource-group rg-cosmosdb-demo 
  --scopes "$COSMOS_ACCOUNT_ID" 
  --condition "avg TotalRequestUnits > 3500" 
  --window-size 5m 
  --evaluation-frequency 1m 
  --action-group "/subscriptions/.../actionGroups/ops-team"

İzlemeniz gereken başlıca metrikler:

  • TotalRequestUnits: RU tüketimini takip edin, provision ettiğinizin yüzde kaçını kullandığınızı görün
  • ServerSideLatency: Cosmos DB tarafındaki gecikme; 10ms üzeri değerler alarm vermeli
  • NormalizedRUConsumption: Yüzde olarak RU kullanımı; yüzde 80 üzeri throttling riski demek
  • Function Invocation Count: Beklenmedik spike’lar cost anomalisi ve hata göstergesi olabilir
  • Function Duration: Uzun süren invocation’lar genellikle Cosmos DB sorgularından kaynaklanıyor

Sık Karşılaşılan Sorunlar

“PartitionKey extracted from document doesn’t match the one specified in the header” hatası alıyorsanız, dökümanınızdaki partition key değeri ile read/write operasyonunda belirttiğiniz değer eşleşmiyor. Document’ı oluştururken hangi alan partition key ise, o alanın değerini operasyonlarda tutarlı kullanın.

Soğuk başlangıç (cold start) sorunu yaşıyorsanız Premium Plan’a geçmeyi veya “Always Ready Instances” özelliğini aktif etmeyi düşünün. Cosmos DB bağlantısını singleton pattern ile yönettiğinizde aynı worker process içinde bağlantı tekrar kullanılıyor ama her yeni instance yine bağlantı kurmak zorunda.

Cross-partition query’ler maliyetlidir. enable_cross_partition_query=True kullandığınızda Cosmos DB tüm partition’ları taramak zorunda kalabilir. Sorgularınızı mümkün olduğunca partition key filtresiyle destekleyin.

Sonuç

Azure Functions ile Cosmos DB entegrasyonu doğru yapıldığında son derece güçlü ve maliyet etkin bir mimari ortaya çıkıyor. Bu yazıda ele aldığımız konuları özetlersek:

Managed Identity kullanımı güvenlik açısından zorunlu, connection string’leri asla koda gömmüyoruz. Singleton pattern ile Cosmos DB client’ı yeniden kullanmak cold start etkisini ve bağlantı overhead’ini azaltıyor. Partition key seçimi mimari kararların en kritik noktası; hatalı seçim hem performans hem maliyet felaketi yaratıyor. Change Feed ile event-driven iş akışları kurabilir, downstream sistemleri gerçek zamanlı besleyebilirsiniz. RU limitlerini aşmamak için retry mekanizması ve uygun indexing politikası şart.

Bunları uyguladıktan sonra Cosmos DB maliyetlerinizin düştüğünü, gecikmelerin azaldığını ve operasyonel yükün hafiflemesini gözlemleyeceksiniz. Bir sonraki adım olarak Azure Service Bus ile Functions’ı entegre ederek daha karmaşık event-driven senaryoları incelemeye devam edeceğiz.

Bir yanıt yazın

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