Azure Functions ile Serverless Fonksiyon Oluşturma

Bulut dünyasında “sunucu yönetme derdi olmadan kod çalıştırma” fikri kulağa çok güzel geliyor. Azure Functions tam da bu vaadi yerine getiriyor. Bir HTTP isteği geldiğinde tetiklenen, bir kuyrukta mesaj biriktikçe devreye giren, zamanlı olarak çalışan küçük fonksiyonlar yazıyorsunuz ve Azure altyapının geri kalanını hallediyor. Ölçekleme yok, sunucu yamalaması yok, kapasite planlaması yok. Bu yazıda sıfırdan başlayıp production kalitesinde Azure Functions ortamı kuracağız, gerçek hayat senaryolarıyla anlatacağız.

Azure Functions Nedir ve Ne Zaman Kullanmalısın

Azure Functions, Microsoft’un serverless hesaplama platformudur. Kod sadece tetiklendiğinde çalışır ve siz yalnızca o çalışma süresi için ücret ödersiniz. Saniyenin binde biri cinsinden faturalandırılırsınız. Ayda 1 milyon istek ücretsizdir, bu da geliştirme ve test ortamları için son derece ekonomik bir seçenek olduğu anlamına gelir.

Peki ne zaman Functions kullanmak mantıklı?

  • Olay güdümlü işler: Blob Storage’a dosya yüklendiğinde otomatik işleme, Service Bus mesajlarını tüketme
  • API arka uçları: Hafif REST API’lar için App Service’e gerek kalmadan
  • Zamanlı görevler: Her gece çalışan rapor üretimi, veritabanı temizleme scriptleri
  • Entegrasyon köprüleri: İki sistem arasında veri dönüştürme ve yönlendirme
  • Webhook işleyicileri: GitHub, Stripe, Twilio gibi üçüncü taraf servislerden gelen callback’leri karşılama

Azure Functions kullanmak istemediğiniz durumlar da var. Uzun süren işlemler (Consumption planında 10 dakika limiti var), sürekli çalışması gereken servisler veya çok yüksek bellek ihtiyacı olan workload’lar için App Service veya Container Apps daha uygun olur.

Geliştirme Ortamını Hazırlama

Azure CLI ve Core Tools Kurulumu

Azure Functions ile çalışmak için birkaç araç lazım. Önce Azure CLI’yi kurun:

# Ubuntu/Debian
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# macOS
brew update && brew install azure-cli

# Kurulum doğrulama
az --version
az login

Azure Functions Core Tools olmadan local geliştirme yapamazsınız. Bu araç sayesinde fonksiyonlarınızı buluta deploy etmeden önce makinenizde test edebilirsiniz:

# Node.js üzerinden npm ile kurulum
npm install -g azure-functions-core-tools@4 --unsafe-perm true

# Ubuntu için Microsoft paket deposuyla
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /usr/share/keyrings/microsoft-prod.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/repos/azure-functions-ubuntu focal main" | sudo tee /etc/apt/sources.list.d/azure-functions.list
sudo apt-get update && sudo apt-get install azure-functions-core-tools-4

# Versiyon kontrolü
func --version

Python için virtual environment hazırlayalım çünkü bütün örnekler Python ile olacak:

python3 -m venv .venv
source .venv/bin/activate
pip install azure-functions azure-storage-blob azure-servicebus

İlk Function App’i Oluşturma

Azure Kaynakları Oluşturma

Önce Azure tarafında gerekli kaynakları oluşturalım. Her Function App bir Storage Account’a ihtiyaç duyar, bu tetikleyici durumlarını ve logları saklamak için kullanılır:

# Değişkenler
RESOURCE_GROUP="rg-functions-demo"
LOCATION="westeurope"
STORAGE_ACCOUNT="safunctionsdemo2024"
FUNCTION_APP="func-api-demo"
RUNTIME="python"
RUNTIME_VERSION="3.11"

# Resource Group oluştur
az group create 
  --name $RESOURCE_GROUP 
  --location $LOCATION

# Storage Account oluştur
az storage account create 
  --name $STORAGE_ACCOUNT 
  --location $LOCATION 
  --resource-group $RESOURCE_GROUP 
  --sku Standard_LRS 
  --kind StorageV2

# Function App oluştur (Consumption planı - kullandıkça öde)
az functionapp create 
  --resource-group $RESOURCE_GROUP 
  --consumption-plan-location $LOCATION 
  --runtime $RUNTIME 
  --runtime-version $RUNTIME_VERSION 
  --functions-version 4 
  --name $FUNCTION_APP 
  --storage-account $STORAGE_ACCOUNT 
  --os-type Linux

Local Proje Yapısı

# Yeni Functions projesi başlat
func init MyFunctionApp --python
cd MyFunctionApp

# Klasör yapısı şu şekilde olacak:
# MyFunctionApp/
# ├── host.json
# ├── local.settings.json
# ├── requirements.txt
# └── .venv/

host.json dosyası global ayarları içerir. Şunu ekleyelim:

cat > host.json << 'EOF'
{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    },
    "logLevel": {
      "default": "Information",
      "Host.Results": "Error",
      "Function": "Information",
      "Host.Aggregator": "Trace"
    }
  },
  "functionTimeout": "00:05:00",
  "extensions": {
    "queues": {
      "batchSize": 16,
      "maxDequeueCount": 5,
      "visibilityTimeout": "00:00:30"
    }
  }
}
EOF

Gerçek Dünya Senaryosu 1: HTTP Triggered REST API

Diyelim ki bir e-ticaret platformu için sipariş durumu sorgulama API’ı yazıyorsunuz. Tam bir App Service kurmak istemiyorsunuz, çünkü trafik düzensiz:

# HTTP trigger fonksiyonu oluştur
func new --name GetOrderStatus --template "HTTP trigger" --authlevel "function"

Oluşturulan function_app.py dosyasını şöyle düzenleyin:

cat > function_app.py << 'EOF'
import azure.functions as func
import logging
import json
import os
from datetime import datetime, timezone
from azure.cosmos import CosmosClient

app = func.FunctionApp()

# Cosmos DB bağlantısı (environment variable'dan)
COSMOS_URL = os.environ.get("COSMOS_URL")
COSMOS_KEY = os.environ.get("COSMOS_KEY")
DATABASE_NAME = "orders-db"
CONTAINER_NAME = "orders"


def get_cosmos_container():
    client = CosmosClient(COSMOS_URL, COSMOS_KEY)
    db = client.get_database_client(DATABASE_NAME)
    return db.get_container_client(CONTAINER_NAME)


@app.route(route="orders/{order_id}", methods=["GET"], auth_level=func.AuthLevel.FUNCTION)
def get_order_status(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("GetOrderStatus fonksiyonu tetiklendi.")

    order_id = req.route_params.get("order_id")

    if not order_id:
        return func.HttpResponse(
            json.dumps({"error": "order_id parametresi gerekli"}),
            status_code=400,
            mimetype="application/json"
        )

    try:
        container = get_cosmos_container()
        query = f"SELECT * FROM c WHERE c.orderId = '{order_id}'"
        items = list(container.query_items(query=query, enable_cross_partition_query=True))

        if not items:
            return func.HttpResponse(
                json.dumps({"error": "Sipariş bulunamadı", "orderId": order_id}),
                status_code=404,
                mimetype="application/json"
            )

        order = items[0]
        response_data = {
            "orderId": order["orderId"],
            "status": order["status"],
            "customerName": order["customerName"],
            "totalAmount": order["totalAmount"],
            "lastUpdated": order.get("lastUpdated", ""),
            "retrievedAt": datetime.now(timezone.utc).isoformat()
        }

        return func.HttpResponse(
            json.dumps(response_data),
            status_code=200,
            mimetype="application/json"
        )

    except Exception as e:
        logging.error(f"Hata oluştu: {str(e)}")
        return func.HttpResponse(
            json.dumps({"error": "Sunucu hatası", "message": str(e)}),
            status_code=500,
            mimetype="application/json"
        )


@app.route(route="orders/{order_id}/status", methods=["PATCH"], auth_level=func.AuthLevel.FUNCTION)
def update_order_status(req: func.HttpRequest) -> func.HttpResponse:
    order_id = req.route_params.get("order_id")

    try:
        req_body = req.get_json()
    except ValueError:
        return func.HttpResponse(
            json.dumps({"error": "Geçersiz JSON body"}),
            status_code=400,
            mimetype="application/json"
        )

    new_status = req_body.get("status")
    valid_statuses = ["pending", "processing", "shipped", "delivered", "cancelled"]

    if new_status not in valid_statuses:
        return func.HttpResponse(
            json.dumps({"error": f"Geçersiz status. Kabul edilenler: {', '.join(valid_statuses)}"}),
            status_code=400,
            mimetype="application/json"
        )

    try:
        container = get_cosmos_container()
        query = f"SELECT * FROM c WHERE c.orderId = '{order_id}'"
        items = list(container.query_items(query=query, enable_cross_partition_query=True))

        if not items:
            return func.HttpResponse(status_code=404)

        order = items[0]
        order["status"] = new_status
        order["lastUpdated"] = datetime.now(timezone.utc).isoformat()
        container.upsert_item(order)

        logging.info(f"Sipariş {order_id} durumu {new_status} olarak güncellendi.")
        return func.HttpResponse(
            json.dumps({"message": "Durum güncellendi", "orderId": order_id, "newStatus": new_status}),
            status_code=200,
            mimetype="application/json"
        )

    except Exception as e:
        logging.error(f"Güncelleme hatası: {str(e)}")
        return func.HttpResponse(status_code=500)

EOF

Gerçek Dünya Senaryosu 2: Timer Triggered Raporlama

Her gece saat 02:00’de çalışıp günlük sipariş özetini yönetime e-posta atan bir fonksiyon:

# Timer trigger fonksiyonu ekle
func new --name DailyOrderReport --template "Timer trigger"
cat >> function_app.py << 'EOF'


@app.timer_trigger(schedule="0 0 2 * * *", arg_name="myTimer", run_on_startup=False)
def daily_order_report(myTimer: func.TimerRequest) -> None:
    """
    Her gece 02:00'de çalışır (UTC).
    CRON formatı: saniye dakika saat gün ay haftanın-günü
    """
    if myTimer.past_due:
        logging.warning("Timer gecikmeli çalıştı! Sistem meşgul olabilir.")

    logging.info(f"Günlük rapor fonksiyonu başladı: {datetime.now(timezone.utc).isoformat()}")

    try:
        # Dün başlangıç/bitiş zamanlarını hesapla
        from datetime import timedelta
        now = datetime.now(timezone.utc)
        yesterday_start = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
        yesterday_end = yesterday_start.replace(hour=23, minute=59, second=59)

        # Cosmos'tan dünün siparişlerini çek
        container = get_cosmos_container()
        query = f"""
            SELECT VALUE COUNT(1) FROM c 
            WHERE c._ts >= {int(yesterday_start.timestamp())} 
            AND c._ts <= {int(yesterday_end.timestamp())}
        """
        total_count = list(container.query_items(query=query, enable_cross_partition_query=True))

        report_data = {
            "date": yesterday_start.date().isoformat(),
            "totalOrders": total_count[0] if total_count else 0,
            "generatedAt": now.isoformat()
        }

        # Email gönderme (SendGrid veya Communication Services ile)
        send_daily_report_email(report_data)
        logging.info(f"Günlük rapor gönderildi: {json.dumps(report_data)}")

    except Exception as e:
        logging.error(f"Rapor oluşturma hatası: {str(e)}")
        raise


def send_daily_report_email(report_data: dict) -> None:
    """E-posta gönderme logic'i buraya gelir."""
    # SendGrid, Azure Communication Services veya SMTP implementasyonu
    logging.info(f"E-posta gönderiliyor: {report_data}")

EOF

Gerçek Dünya Senaryosu 3: Blob Storage Tetikleyici

Bir müşteri fatura PDF yükleyince otomatik olarak işleyen fonksiyon. Muhasebe sistemine entegrasyon için yaygın kullanılan bir pattern:

cat >> function_app.py << 'EOF'


@app.blob_trigger(arg_name="myblob", path="invoices-incoming/{name}", connection="AzureWebJobsStorage")
def process_invoice_blob(myblob: func.InputStream) -> None:
    """
    'invoices-incoming' container'ına yeni blob eklenince tetiklenir.
    Fatura PDF'ini okur, metadata çıkarır ve veritabanına kaydeder.
    """
    blob_name = myblob.name
    blob_size = myblob.length

    logging.info(f"Yeni fatura alındı: {blob_name}, Boyut: {blob_size} bytes")

    # Sadece PDF'leri işle
    if not blob_name.lower().endswith(".pdf"):
        logging.warning(f"PDF olmayan dosya atlandı: {blob_name}")
        return

    try:
        blob_content = myblob.read()

        # Fatura ID'sini dosya adından çıkar (invoice_12345.pdf -> 12345)
        invoice_id = blob_name.split("_")[1].replace(".pdf", "") if "_" in blob_name else blob_name

        # Burada gerçek PDF parsing yapılabilir (PyPDF2, pdfplumber vb.)
        # Demo olarak metadata kaydedelim
        invoice_record = {
            "invoiceId": invoice_id,
            "fileName": blob_name,
            "fileSize": blob_size,
            "status": "received",
            "receivedAt": datetime.now(timezone.utc).isoformat(),
            "processedAt": None
        }

        container = get_cosmos_container()
        container.upsert_item(invoice_record)

        logging.info(f"Fatura kaydedildi: {invoice_id}")

        # İşlenmiş dosyayı başka bir container'a taşı
        move_to_processed(blob_content, blob_name)

    except Exception as e:
        logging.error(f"Fatura işleme hatası {blob_name}: {str(e)}")
        # Hata durumunda dead-letter queue'ya yaz
        raise


def move_to_processed(content: bytes, filename: str) -> None:
    """Dosyayı invoices-processed container'ına kopyalar."""
    from azure.storage.blob import BlobServiceClient
    storage_conn = os.environ.get("AzureWebJobsStorage")
    client = BlobServiceClient.from_connection_string(storage_conn)
    blob_client = client.get_blob_client(container="invoices-processed", blob=filename)
    blob_client.upload_blob(content, overwrite=True)
    logging.info(f"{filename} -> invoices-processed konteynırına taşındı.")

EOF

Local Geliştirme ve Test

Local.settings.json’a bağlantı bilgilerini ekleyin. Bu dosya asla Git’e commit edilmemeli:

cat > local.settings.json << 'EOF'
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "COSMOS_URL": "https://your-cosmos.documents.azure.com:443/",
    "COSMOS_KEY": "your-cosmos-key-here",
    "APPINSIGHTS_INSTRUMENTATIONKEY": "your-appinsights-key"
  }
}
EOF

# .gitignore'a ekle
echo "local.settings.json" >> .gitignore
echo ".env" >> .gitignore
echo ".venv/" >> .gitignore

# Local'de çalıştır
func start

# Ayrı terminalde test et
curl -X GET "http://localhost:7071/api/orders/ORD-12345?code=your_function_key"

curl -X PATCH "http://localhost:7071/api/orders/ORD-12345/status" 
  -H "Content-Type: application/json" 
  -d '{"status": "shipped"}'

Azure’a Deploy Etme

# Requirements.txt güncel olmalı
pip freeze > requirements.txt

# Azure'a deploy et
func azure functionapp publish $FUNCTION_APP --python

# Deploy sonrası fonksiyonları listele
az functionapp function list 
  --resource-group $RESOURCE_GROUP 
  --name $FUNCTION_APP 
  --output table

# Application Settings'i ekle (Cosmos bağlantısı için)
az functionapp config appsettings set 
  --resource-group $RESOURCE_GROUP 
  --name $FUNCTION_APP 
  --settings 
    "COSMOS_URL=https://your-cosmos.documents.azure.com:443/" 
    "[email protected](SecretUri=https://your-kv.vault.azure.net/secrets/cosmos-key/)"

Önemli not: Production’da connection string’leri ve key’leri doğrudan app settings’e yazmak yerine yukarıdaki örnekte gösterildiği gibi Azure Key Vault referansı kullanın. Bu sayede secret’lar hiçbir zaman düz metin olarak saklanmaz.

Monitoring ve Log Yönetimi

Fonksiyonlarınız çalışıyor ama ne yapıyor? Application Insights entegrasyonu şart:

# Application Insights oluştur
az monitor app-insights component create 
  --app "appi-functions-demo" 
  --location $LOCATION 
  --resource-group $RESOURCE_GROUP 
  --kind web 
  --application-type web

# Instrumentation key'i al
INSTRUMENTATION_KEY=$(az monitor app-insights component show 
  --app "appi-functions-demo" 
  --resource-group $RESOURCE_GROUP 
  --query "instrumentationKey" -o tsv)

# Function App'e bağla
az functionapp config appsettings set 
  --resource-group $RESOURCE_GROUP 
  --name $FUNCTION_APP 
  --settings "APPINSIGHTS_INSTRUMENTATIONKEY=$INSTRUMENTATION_KEY"

# Canlı logları izle
func azure functionapp logstream $FUNCTION_APP

# Ya da az CLI ile
az webapp log tail 
  --resource-group $RESOURCE_GROUP 
  --name $FUNCTION_APP

Application Insights’ta çalıştırabileceğiniz örnek KQL sorguları:

# Bu Kusto sorgularını Azure Portal > Application Insights > Logs bölümünde çalıştırın

# Son 1 saatteki başarısız istekler
# requests | where timestamp > ago(1h) | where success == false | summarize count() by name, resultCode

# Fonksiyon çalışma süreleri (ms cinsinden)
# requests | where timestamp > ago(24h) | summarize avg(duration), max(duration), min(duration) by name

# Hata logları
# traces | where timestamp > ago(1h) | where severityLevel >= 3 | project timestamp, message, customDimensions

Güvenlik ve Best Practices

Production’a gitmeden önce kontrol listesi:

  • Auth level ayarları: Anonymous asla production’da kullanılmaz. function veya admin level kullanın
  • CORS ayarları: Sadece izin verilen domain’leri ekleyin, wildcard (*) kullanmayın
  • Key Vault entegrasyonu: Tüm secret’lar Key Vault’ta olmalı
  • Managed Identity: Service-to-service auth için connection string yerine managed identity kullanın
  • VNet entegrasyonu: Premium plan ile fonksiyonları private VNet içine alabilirsiniz
# Managed Identity'yi etkinleştir
az functionapp identity assign 
  --resource-group $RESOURCE_GROUP 
  --name $FUNCTION_APP

# CORS ayarla (örneğin sadece frontend domaininize izin verin)
az functionapp cors add 
  --resource-group $RESOURCE_GROUP 
  --name $FUNCTION_APP 
  --allowed-origins "https://app.sirketiniz.com"

# Function key'i döndür (key rotation)
az functionapp function keys set 
  --resource-group $RESOURCE_GROUP 
  --name $FUNCTION_APP 
  --function-name GetOrderStatus 
  --key-name default 
  --key-value "$(openssl rand -base64 32 | tr -d '=+/' | head -c 32)"

Scaling ve Maliyet Optimizasyonu

Consumption planında otomatik scale olur ama bazı ayarları bilmek gerekiyor:

  • Soğuk start sorunu: Python ve Java fonksiyonları Consumption planında ilk istekte yavaş başlar. Kritik API’lar için Premium plan veya “Always Ready” özelliğini düşünün
  • Concurrency limiti: Tek bir instance aynı anda kaç istek işleyeceğini host.json ile ayarlayabilirsiniz
  • Zaman aşımı: Consumption planında maksimum 10 dakika, Premium planında 60 dakika (veya sınırsız)
  • Hafıza: Consumption planında 1.5 GB ile sınırlısınız
# Premium plana geçiş (her zaman sıcak, daha hızlı)
az functionapp plan create 
  --resource-group $RESOURCE_GROUP 
  --name "plan-functions-premium" 
  --location $LOCATION 
  --sku EP1 
  --is-linux

# Mevcut Function App'i premium plana taşı
az functionapp update 
  --resource-group $RESOURCE_GROUP 
  --name $FUNCTION_APP 
  --plan "plan-functions-premium"

Sonuç

Azure Functions, doğru kullanıldığında gerçekten güçlü bir araç. Özellikle olay güdümlü mimarilerde, API arka uçlarında ve zamanlanmış görevlerde altyapı karmaşıklığını dramatik biçimde azaltıyor. Bugün ele aldığımız konuları özetleyelim:

  • Geliştirme ortamını Azure CLI ve Core Tools ile hazırladık
  • HTTP trigger ile REST API yazdık ve gerçek sipariş yönetimi senaryosunu ele aldık
  • Timer trigger ile gece çalışan raporlama fonksiyonu oluşturduk
  • Blob trigger ile otomatik dosya işleme pipeline’ı kurdu
  • Local test, deploy ve monitoring konularını geçtik
  • Güvenlik best practice’lerini ve Key Vault entegrasyonunu inceledik

Production’a gitmeden önce mutlaka Application Insights’ı açık tutun, Key Vault entegrasyonunu uygulayın ve Consumption planının limitlerini iyi anlayın. Soğuk start problemini görmezden gelmek, müşteri şikayetine dönüşmeden önce test etmenin yolu budur.

Bir sonraki adım olarak Azure Durable Functions konusuna bakmanızı öneririm. Uzun süren, multi-step iş akışları için Durable Functions’ın orkestrasyon özellikleri hayat kurtarıcı. Özellikle insan onayı gerektiren approval workflow’ları ve fan-out/fan-in pattern’ları için vazgeçilmez oluyor.

Bir yanıt yazın

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