Azure Functions ile Service Bus Entegrasyonu

Mesaj kuyruklama sistemleri kurumsal dünyada her zaman kritik bir yer tutmuştur. Ancak bu sistemleri yönetmek, ölçeklendirmek ve bakımını yapmak ciddi bir operasyonel yük getirir. Azure Functions ile Azure Service Bus’ı bir araya getirdiğinizde bu yükün büyük bölümünü Azure’a devredebilir, siz de asıl işe odaklanabilirsiniz. Bu yazıda gerçek dünya senaryoları üzerinden bu entegrasyonu derinlemesine inceleyeceğiz.

Azure Service Bus Nedir ve Neden Önemlidir?

Azure Service Bus, kurumsal düzeyde bir mesajlaşma altyapısıdır. Basit bir “mesaj kuyruğu” olarak küçümsenmemeli; garantili mesaj teslimi, dead-letter queue desteği, oturum yönetimi ve mesaj kilitleme gibi özelliklerle ciddi iş senaryolarını karşılar.

Günlük hayattan bir örnek verelim: Bir e-ticaret platformunda sipariş geldiğinde aynı anda stok güncelleme, fatura oluşturma, kargo bildirimi ve müşteri e-postası gönderilmesi gerekiyor. Bunların hepsini senkron olarak yapmaya kalksanız ya sistem yavaşlar ya da bir servis düştüğünde her şey durur. Service Bus burada devreye girer; her işlem ayrı bir mesaj olarak kuyruğa alınır, işlendiğinde onaylanır, başarısız olursa yeniden denenir.

Service Bus’ta iki temel kavram var:

  • Queue (Kuyruk): Bire bir iletişim. Bir mesajı yalnızca tek bir alıcı tüketir.
  • Topic/Subscription (Konu/Abonelik): Bir çoka iletişim. Aynı mesajı birden fazla alıcı farklı filtrelerle tüketebilir.

Geliştirme Ortamını Hazırlamak

Başlamadan önce gerekli araçları kuralım. Azure CLI, Azure Functions Core Tools ve .NET SDK olmadan bu işin içinden çıkmak zorlaşır.

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

# Azure Functions Core Tools kurulumu
npm install -g azure-functions-core-tools@4 --unsafe-perm true

# Azure'a giriş yap
az login

# Aboneliği kontrol et
az account show

# Çalışacağımız aboneliği seç
az account set --subscription "abonelik-id-buraya"

Ortam hazır olduğunda altyapıyı oluşturmaya başlayabiliriz. Her şeyi tek bir resource group altında tutmak yönetimi kolaylaştırır.

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

# Service Bus namespace oluştur (Standard tier minimum gereksinim topic için)
az servicebus namespace create 
  --name sb-demo-namespace-prod 
  --resource-group rg-servicebus-demo 
  --location westeurope 
  --sku Standard

# Sipariş kuyruğu oluştur
az servicebus queue create 
  --name orders-queue 
  --namespace-name sb-demo-namespace-prod 
  --resource-group rg-servicebus-demo 
  --max-delivery-count 3 
  --lock-duration PT30S 
  --default-message-time-to-live P1D

# Topic oluştur (bildirimler için)
az servicebus topic create 
  --name notifications-topic 
  --namespace-name sb-demo-namespace-prod 
  --resource-group rg-servicebus-demo

# Subscription'lar oluştur
az servicebus topic subscription create 
  --name email-subscription 
  --topic-name notifications-topic 
  --namespace-name sb-demo-namespace-prod 
  --resource-group rg-servicebus-demo 
  --max-delivery-count 5

az servicebus topic subscription create 
  --name sms-subscription 
  --topic-name notifications-topic 
  --namespace-name sb-demo-namespace-prod 
  --resource-group rg-servicebus-demo 
  --max-delivery-count 5

Burada dikkat edilmesi gereken birkaç önemli parametre var:

  • –max-delivery-count 3: Bir mesaj 3 kez başarısız işlenirse dead-letter queue’ya taşınır. Üretimde bu sayıyı iş gereksinimlerine göre belirleyin.
  • –lock-duration PT30S: Mesaj işlenirken 30 saniye kilitli kalır. İşleme süreniz uzunsa bu değeri artırın.
  • –default-message-time-to-live P1D: Mesaj 1 gün içinde işlenmezse silinir.

Azure Functions Projesi Oluşturmak

# Yeni Functions projesi oluştur
func init OrderProcessingFunctions --worker-runtime dotnet-isolated --target-framework net8.0

cd OrderProcessingFunctions

# Service Bus trigger ekle
func new --name ProcessOrderFunction --template "ServiceBusQueueTrigger"

# Gerekli NuGet paketleri
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.ServiceBus --version 5.14.1
dotnet add package Azure.Messaging.ServiceBus --version 7.18.1
dotnet add package Microsoft.Extensions.Logging --version 8.0.0

Queue Trigger ile Sipariş İşleme

İlk gerçek senaryo: Sipariş kuyruğundan mesaj alıp işlemek. Aşağıdaki kod production’da kullanabileceğiniz bir yapıyı gösteriyor.

using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Text.Json;

namespace OrderProcessingFunctions;

public class ProcessOrderFunction
{
    private readonly ILogger<ProcessOrderFunction> _logger;
    private readonly IOrderService _orderService;

    public ProcessOrderFunction(
        ILogger<ProcessOrderFunction> logger,
        IOrderService orderService)
    {
        _logger = logger;
        _orderService = orderService;
    }

    [Function(nameof(ProcessOrderFunction))]
    public async Task Run(
        [ServiceBusTrigger(
            "orders-queue",
            Connection = "ServiceBusConnection",
            IsSessionsEnabled = false)]
        ServiceBusReceivedMessage message,
        ServiceBusMessageActions messageActions,
        CancellationToken cancellationToken)
    {
        var correlationId = message.CorrelationId ?? Guid.NewGuid().ToString();
        
        using var scope = _logger.BeginScope(new Dictionary<string, object>
        {
            ["CorrelationId"] = correlationId,
            ["MessageId"] = message.MessageId,
            ["DeliveryCount"] = message.DeliveryCount
        });

        try
        {
            _logger.LogInformation(
                "Sipariş mesajı alındı. DeliveryCount: {DeliveryCount}",
                message.DeliveryCount);

            var orderData = JsonSerializer.Deserialize<OrderMessage>(
                message.Body.ToString(),
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

            if (orderData == null)
            {
                _logger.LogError("Geçersiz mesaj formatı. Dead-letter'a gönderiliyor.");
                await messageActions.DeadLetterMessageAsync(
                    message,
                    deadLetterReason: "InvalidFormat",
                    deadLetterErrorDescription: "Mesaj JSON formatına dönüştürülemedi.",
                    cancellationToken: cancellationToken);
                return;
            }

            await _orderService.ProcessOrderAsync(orderData, cancellationToken);
            
            // Başarılı işleme sonrası mesajı onayla
            await messageActions.CompleteMessageAsync(message, cancellationToken);
            
            _logger.LogInformation(
                "Sipariş {OrderId} başarıyla işlendi.",
                orderData.OrderId);
        }
        catch (TransientException ex)
        {
            // Geçici hatalar için mesajı abandon et, tekrar denensin
            _logger.LogWarning(ex,
                "Geçici hata. Mesaj {DeliveryCount}. denemede.",
                message.DeliveryCount);
            await messageActions.AbandonMessageAsync(message, cancellationToken: cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Kritik hata. Sipariş işlenemedi. Dead-letter'a gönderiliyor.");
            await messageActions.DeadLetterMessageAsync(
                message,
                deadLetterReason: "ProcessingError",
                deadLetterErrorDescription: ex.Message,
                cancellationToken: cancellationToken);
        }
    }
}

Bu kodda özellikle messageActions parametresine dikkat edin. Isolated worker modelinde mesaj onaylama, abandoning ve dead-lettering bu nesne üzerinden yapılır. In-process modelden önemli bir fark bu.

Topic Subscription ile Bildirim Dağıtımı

Şimdi daha karmaşık bir senaryo: Bir sipariş işlendiğinde hem e-posta hem SMS göndermek istiyoruz. Topic/Subscription modeliyle aynı mesajı iki farklı function tüketebilir.

[Function("SendEmailNotification")]
public async Task RunEmail(
    [ServiceBusTrigger(
        "notifications-topic",
        "email-subscription",
        Connection = "ServiceBusConnection")]
    ServiceBusReceivedMessage message,
    ServiceBusMessageActions messageActions,
    CancellationToken cancellationToken)
{
    try
    {
        var notification = JsonSerializer.Deserialize<NotificationMessage>(
            message.Body.ToString());

        // Sadece e-posta tipindeki bildirimleri işle
        if (notification?.Type != "OrderConfirmation")
        {
            await messageActions.CompleteMessageAsync(message, cancellationToken);
            return;
        }

        var emailResult = await _emailService.SendAsync(new EmailRequest
        {
            To = notification.RecipientEmail,
            Subject = $"Siparişiniz alındı - #{notification.OrderId}",
            TemplateId = "order-confirmation",
            TemplateData = new
            {
                OrderId = notification.OrderId,
                TotalAmount = notification.TotalAmount,
                EstimatedDelivery = notification.EstimatedDelivery
            }
        }, cancellationToken);

        if (!emailResult.IsSuccess)
        {
            _logger.LogError(
                "E-posta gönderilemedi. OrderId: {OrderId}, Hata: {Error}",
                notification.OrderId,
                emailResult.ErrorMessage);
            
            // DeliveryCount kontrolü
            if (message.DeliveryCount >= 3)
            {
                await messageActions.DeadLetterMessageAsync(
                    message,
                    "EmailDeliveryFailed",
                    emailResult.ErrorMessage,
                    cancellationToken);
                return;
            }
            
            await messageActions.AbandonMessageAsync(message, cancellationToken: cancellationToken);
            return;
        }

        await messageActions.CompleteMessageAsync(message, cancellationToken);
        _logger.LogInformation(
            "E-posta başarıyla gönderildi. OrderId: {OrderId}",
            notification.OrderId);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "E-posta function hatası");
        throw; // Azure Functions retry mekanizmasına bırak
    }
}

Dead-Letter Queue Monitoring

Production’da dead-letter queue’yu izlemek kritiktir. İşlenemeyen mesajlar burada birikirken siz haberdar olmazsanız ciddi veri kaybı yaşanabilir.

# Dead-letter queue'daki mesaj sayısını kontrol et
az servicebus queue show 
  --name orders-queue 
  --namespace-name sb-demo-namespace-prod 
  --resource-group rg-servicebus-demo 
  --query "countDetails.deadLetterMessageCount"

# Metric alert oluştur (DLQ 10 mesajı aşarsa uyar)
az monitor metrics alert create 
  --name "DLQ-High-Count-Alert" 
  --resource-group rg-servicebus-demo 
  --scopes "/subscriptions/{subscription-id}/resourceGroups/rg-servicebus-demo/providers/Microsoft.ServiceBus/namespaces/sb-demo-namespace-prod" 
  --condition "avg DeadletteredMessages > 10" 
  --window-size 5m 
  --evaluation-frequency 1m 
  --action "/subscriptions/{subscription-id}/resourceGroups/rg-servicebus-demo/providers/microsoft.insights/actionGroups/ops-team-alerts" 
  --description "Sipariş DLQ'su 10 mesajı aştı. İnceleme gerekiyor."

Dead-letter queue’yu otomatik işlemek için ayrı bir function yazabilirsiniz:

[Function("ProcessDeadLetterQueue")]
public async Task RunDLQ(
    [ServiceBusTrigger(
        "orders-queue/$deadletterqueue",
        Connection = "ServiceBusConnection")]
    ServiceBusReceivedMessage message,
    ServiceBusMessageActions messageActions,
    CancellationToken cancellationToken)
{
    var deadLetterReason = message.DeadLetterReason;
    var deadLetterDescription = message.DeadLetterErrorDescription;
    var deliveryCount = message.DeliveryCount;

    _logger.LogError(
        "Dead-letter mesajı işleniyor. " +
        "MessageId: {MessageId}, Sebep: {Reason}, Açıklama: {Description}",
        message.MessageId,
        deadLetterReason,
        deadLetterDescription);

    // Duruma göre aksiyon al
    switch (deadLetterReason)
    {
        case "InvalidFormat":
            // Mesajı loglara yaz ve geliştirici ekibini bilgilendir
            await _alertService.SendSlackAlert(
                $"Geçersiz formatlı mesaj bulundu. ID: {message.MessageId}",
                message.Body.ToString());
            break;

        case "ProcessingError":
            // Belirli bir süre sonra tekrar dene
            if (ShouldRetry(message))
            {
                await _retryService.ScheduleRetryAsync(message);
            }
            break;

        default:
            // Manuel inceleme için kayıt oluştur
            await _incidentService.CreateIncidentAsync(message);
            break;
    }

    await messageActions.CompleteMessageAsync(message, cancellationToken);
}

local.settings.json Yapılandırması

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "ServiceBusConnection": "Endpoint=sb://sb-demo-namespace-prod.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=YOUR_KEY_HERE",
    "ServiceBusConnection__fullyQualifiedNamespace": "sb-demo-namespace-prod.servicebus.windows.net"
  }
}

Production’da connection string yerine Managed Identity kullanmak çok daha güvenlidir:

# Function App'e Managed Identity ver
az functionapp identity assign 
  --name func-order-processing 
  --resource-group rg-servicebus-demo

# Principal ID'yi al
PRINCIPAL_ID=$(az functionapp identity show 
  --name func-order-processing 
  --resource-group rg-servicebus-demo 
  --query principalId -o tsv)

# Service Bus Data Receiver rolü ata
az role assignment create 
  --assignee $PRINCIPAL_ID 
  --role "Azure Service Bus Data Receiver" 
  --scope "/subscriptions/{subscription-id}/resourceGroups/rg-servicebus-demo/providers/Microsoft.ServiceBus/namespaces/sb-demo-namespace-prod"

# Service Bus Data Sender rolü de ekle (gerekirse)
az role assignment create 
  --assignee $PRINCIPAL_ID 
  --role "Azure Service Bus Data Sender" 
  --scope "/subscriptions/{subscription-id}/resourceGroups/rg-servicebus-demo/providers/Microsoft.ServiceBus/namespaces/sb-demo-namespace-prod"

Managed Identity kullanırken local.settings.json‘da connection string yerine namespace adresini kullanırsınız. ServiceBusConnection__fullyQualifiedNamespace ayarı tam olarak bu amaç için vardır.

Ölçeklendirme ve Performans Ayarları

host.json dosyası Azure Functions’ın davranışını belirler. Service Bus entegrasyonunda bu dosya son derece önemlidir.

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "maxTelemetryItemsPerSecond": 20
      }
    }
  },
  "extensions": {
    "serviceBus": {
      "prefetchCount": 0,
      "autoCompleteMessages": false,
      "maxConcurrentCalls": 16,
      "maxConcurrentSessions": 8,
      "maxMessageBatchSize": 1000,
      "minMessageBatchSize": 1,
      "maxBatchWaitTime": "00:00:30",
      "sessionIdleTimeout": "00:01:00",
      "transportType": "amqpWebSockets"
    }
  },
  "functionTimeout": "00:10:00"
}

Önemli parametrelerin anlamları:

  • prefetchCount: 0 bırakmak genellikle güvenlidir. Yüksek değerler throughput artırır ama mesaj kilitleme sorunlarına yol açabilir.
  • autoCompleteMessages: false olması zorunludur. Mesajı kod içinde manuel onaylamak istiyoruz.
  • maxConcurrentCalls: Aynı anda işlenebilecek maksimum mesaj sayısı. Consumption planında CPU başına 16 iyi bir başlangıçtır.
  • transportType: Güvenlik duvarları arkasında AMQP portu (5671) kapalıysa WebSockets (443) kullanın.

Function App Deployment

# Function App oluştur (Consumption plan)
az functionapp create 
  --name func-order-processing 
  --resource-group rg-servicebus-demo 
  --storage-account storderprocessing 
  --consumption-plan-location westeurope 
  --runtime dotnet-isolated 
  --runtime-version 8 
  --functions-version 4 
  --os-type Linux

# Application Insights bağla
APPINSIGHTS_KEY=$(az monitor app-insights component show 
  --app ai-order-processing 
  --resource-group rg-servicebus-demo 
  --query instrumentationKey -o tsv)

az functionapp config appsettings set 
  --name func-order-processing 
  --resource-group rg-servicebus-demo 
  --settings "APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=$APPINSIGHTS_KEY"

# Publish et
dotnet publish -c Release -o ./publish
cd publish && zip -r ../function-app.zip .
cd ..

az functionapp deployment source config-zip 
  --name func-order-processing 
  --resource-group rg-servicebus-demo 
  --src function-app.zip

Gerçek Dünya Dikkat Noktaları

Production ortamında sıkça karşılaşılan sorunları ve çözümlerini paylaşmak istiyorum.

Mesaj kilidi kaybetme problemi: İşleme süreniz lockDuration değerinden uzun sürerse Azure mesajın işlenmediğini düşünür ve başka bir consumer’a verir. Bu durumda aynı mesaj iki kez işlenebilir. Çözüm olarak işlem başında kilidi yenilemek veya lock süresini artırmak gerekir.

Idempotency: Service Bus “at-least-once” garantisi verir, yani aynı mesaj birden fazla kez teslim edilebilir. Her mesajı işlerken MessageId veya CorrelationId ile daha önce işlenip işlenmediğini kontrol edin. Bir Redis cache veya veritabanında işlenen mesaj ID’lerini tutmak bu sorunu çözer.

Batching vs Single Message: Yüksek hacimli senaryolarda tek tek mesaj yerine batch processing düşünün. maxMessageBatchSize parametresini artırarak function’ınızı liste alan bir trigger ile yazabilirsiniz.

Cost optimizasyonu: Consumption planında her mesaj işleme ayrı bir fatura kalemi olur. Yoğun iş yüklerinde Premium plan veya App Service plan daha ekonomik olabilir. 1 milyon execution ücretsizdir ama Service Bus namespace maliyetlerini de hesaba katın.

Monitoring ve alerting: Application Insights’ta custom metric’ler oluşturun. Özellikle işleme süresi, hata oranı ve DLQ mesaj sayısı için alert threshold’ları belirleyin. TelemetryClient ile custom event’ler gönderebilirsiniz.

Sonuç

Azure Functions ile Service Bus entegrasyonu, kurumsal uygulamalarda asenkron mesajlaşmayı hem güçlü hem de yönetilebilir hale getirir. Yazdığımız kod örneklerinde görüldüğü gibi, mesaj onaylama, dead-letter yönetimi ve hata işleme gibi kritik konulara baştan dikkat etmek production sorunlarını büyük ölçüde azaltır.

Özellikle vurgulamamız gereken üç nokta var: Managed Identity kullanımı güvenlik açısından vazgeçilmezdir, autoCompleteMessages: false ile mesaj yaşam döngüsünü kendiniz kontrol etmek kararlılık sağlar ve dead-letter queue’yu izlemek için mutlaka bir monitoring mekanizması kurmalısınız.

Bu mimariyi bir kez doğru kurduğunuzda, yüksek trafikte bile servisleriniz birbirinden bağımsız çalışmaya devam eder. Bir mikro servis düşse bile mesajlar kuyruğa bekler, servis ayağa kalktığında işleme devam eder. Bu dayanıklılık, modern uygulamalarda olmazsa olmaz bir özellik haline gelmiştir.

Bir yanıt yazın

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