Azure Functions Timer Trigger ile Zamanlanmış Görevler

Sunucusuz mimariye geçiş yapan birçok ekibin ilk sorduğu sorulardan biri şu oluyor: “Peki zamanlanmış görevleri nasıl çalıştıracağız?” Klasik dünyada cron job vardı, Windows’ta Task Scheduler vardı, belki bir veritabanı job’ı vardı. Azure Functions’ın Timer Trigger özelliği tam da bu ihtiyacı karşılıyor ve bunu yaparken sunucu yönetimi derdini tamamen ortadan kaldırıyor. Bu yazıda Timer Trigger’ı sıfırdan kurarak, gerçek dünya senaryolarında nasıl kullandığımı anlatacağım.

Timer Trigger Nedir ve Neden Kullanmalısınız?

Azure Functions Timer Trigger, NCRONTAB ifadelerine dayanan bir zamanlama mekanizması sunar. Klasik Linux cron’dan biraz farklı olduğu için başta kafayı karıştırabiliyor. Linux cron 5 alan kullanırken, Azure Functions NCRONTAB formatı 6 alan kullanıyor. Fazladan gelen alan saniye bilgisini tutuyor.

Neden tercih etmeli? Birkaç somut neden:

  • Sıfır altyapı yönetimi: VM açmanıza, patch uygulamanıza, monitoring kurmanıza gerek yok
  • Otomatik ölçekleme: Görev yoğunluk yaratırsa Azure kendisi hallediyor
  • Entegre izleme: Application Insights ile kutunun içinden geliyor
  • Maliyet avantajı: Consumption planında sadece çalıştığı süre için ödeme yapıyorsunuz
  • Dağıtık kilit mekanizması: Birden fazla instance çalışsa bile görev yalnızca bir kez tetikleniyor

NCRONTAB Formatını Anlamak

Altı alanlı format şu şekilde sıralanıyor:

{saniye} {dakika} {saat} {gün} {ay} {haftanın günü}

Birkaç örnek vermek gerekirse:

# Her 5 dakikada bir çalış
0 */5 * * * *

# Her gün sabah 03:00'da çalış
0 0 3 * * *

# Her pazartesi sabah 08:30'da çalış
0 30 8 * * 1

# Her ayın 1'inde saat 00:00'da çalış
0 0 0 1 * *

# Hafta içi her gün 09:00-18:00 arası her saat başı
0 0 9-18 * * 1-5

# Her 30 saniyede bir (test için kullanışlı)
*/30 * * * * *

Bir uyarı vermem gerekiyor: UTC saat dilimi varsayılan olarak kullanılıyor. Türkiye saatinde çalışmasını istiyorsanız ya host.json üzerinden timezone ayarı yapmanız ya da UTC farkını hesaba katmanız gerekiyor.

Geliştirme Ortamı Kurulumu

Önce gerekli araçları kuralım. Ben burada Azure CLI ve Azure Functions Core Tools kullanacağım.

# Azure Functions Core Tools kurulumu (Ubuntu/Debian)
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal main" > /etc/apt/sources.list.d/dotnet.list'
sudo apt-get update
sudo apt-get install azure-functions-core-tools-4

# Doğrulama
func --version

# Azure CLI ile giriş
az login

# Yeni bir Functions projesi oluşturma (Python)
func init TimerFunctionProject --python
cd TimerFunctionProject

# Timer trigger ile yeni fonksiyon oluşturma
func new --name DailyCleanup --template "Timer trigger"

Proje oluştuktan sonra DailyCleanup/__init__.py dosyasına bakacak olursanız temel bir iskelet göreceksiniz. Şimdi bunu gerçek bir senaryoya dönüştürelim.

Senaryo 1: Günlük Log Temizleme Görevi

Bu senaryo çoğu ortamda ihtiyaç duyulan klasik bir görev. Azure Blob Storage’daki eski log dosyalarını temizlemek istiyoruz.

# DailyCleanup/__init__.py
import logging
import os
from datetime import datetime, timedelta, timezone
import azure.functions as func
from azure.storage.blob import BlobServiceClient

def main(mytimer: func.TimerRequest) -> None:
    utc_timestamp = datetime.now(timezone.utc).isoformat()
    
    if mytimer.past_due:
        logging.warning('Timer tetiklemesi gecikti! Zaman: %s', utc_timestamp)
    
    logging.info('Log temizleme görevi başladı: %s', utc_timestamp)
    
    connection_string = os.environ["AzureWebJobsStorage"]
    container_name = os.environ["LOG_CONTAINER_NAME"]
    retention_days = int(os.environ.get("LOG_RETENTION_DAYS", "30"))
    
    blob_service_client = BlobServiceClient.from_connection_string(connection_string)
    container_client = blob_service_client.get_container_client(container_name)
    
    cutoff_date = datetime.now(timezone.utc) - timedelta(days=retention_days)
    deleted_count = 0
    total_size_freed = 0
    
    try:
        blobs = container_client.list_blobs()
        for blob in blobs:
            if blob.last_modified < cutoff_date:
                blob_client = container_client.get_blob_client(blob.name)
                props = blob_client.get_blob_properties()
                total_size_freed += props.size
                blob_client.delete_blob()
                deleted_count += 1
                logging.info('Silinen blob: %s (Boyut: %d bytes)', blob.name, props.size)
        
        logging.info(
            'Temizleme tamamlandı. Silinen dosya: %d, Kazanılan alan: %.2f MB',
            deleted_count,
            total_size_freed / (1024 * 1024)
        )
    except Exception as e:
        logging.error('Temizleme sırasında hata oluştu: %s', str(e))
        raise

Bu fonksiyonun tetikleyici ayarını function.json dosyasında yapıyoruz:

{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "name": "mytimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 1 * * *"
    }
  ]
}

Her gece saat 01:00 UTC’de çalışacak şekilde ayarladık. Türkiye saati için gece 04:00 oluyor, iş yükünün düşük olduğu bir saat.

Senaryo 2: Veritabanı Yedekleme Görevi

Production ortamında sıkça ihtiyaç duyduğumuz bir başka senaryo: Azure SQL Database’in yedeğini alıp Blob Storage’a atmak.

# DatabaseBackup/__init__.py
import logging
import os
import pyodbc
import gzip
import io
from datetime import datetime, timezone
import azure.functions as func
from azure.storage.blob import BlobServiceClient, ContentSettings

def main(mytimer: func.TimerRequest) -> None:
    start_time = datetime.now(timezone.utc)
    logging.info('Veritabanı yedekleme başladı: %s', start_time.isoformat())
    
    server = os.environ["DB_SERVER"]
    database = os.environ["DB_NAME"]
    username = os.environ["DB_USERNAME"]
    password = os.environ["DB_PASSWORD"]
    backup_container = os.environ["BACKUP_CONTAINER"]
    storage_connection = os.environ["AzureWebJobsStorage"]
    
    conn_str = (
        f"DRIVER={{ODBC Driver 18 for SQL Server}};"
        f"SERVER={server};"
        f"DATABASE={database};"
        f"UID={username};"
        f"PWD={password};"
        f"Encrypt=yes;"
        f"TrustServerCertificate=no;"
    )
    
    try:
        conn = pyodbc.connect(conn_str)
        cursor = conn.cursor()
        
        # Tablo listesini al
        cursor.execute("""
            SELECT TABLE_NAME 
            FROM INFORMATION_SCHEMA.TABLES 
            WHERE TABLE_TYPE = 'BASE TABLE'
        """)
        tables = cursor.fetchall()
        
        timestamp = start_time.strftime("%Y%m%d_%H%M%S")
        backup_data = []
        
        for table in tables:
            table_name = table[0]
            cursor.execute(f"SELECT * FROM [{table_name}]")
            rows = cursor.fetchall()
            columns = [desc[0] for desc in cursor.description]
            
            backup_data.append(f"-- Tablo: {table_name}n")
            backup_data.append(f"-- Satır sayısı: {len(rows)}n")
            
            for row in rows:
                values = ", ".join([f"'{str(v)}'" if v is not None else "NULL" for v in row])
                backup_data.append(f"INSERT INTO [{table_name}] VALUES ({values});n")
        
        # Sıkıştır ve yükle
        backup_content = "".join(backup_data).encode("utf-8")
        compressed_buffer = io.BytesIO()
        
        with gzip.GzipFile(fileobj=compressed_buffer, mode='wb') as gz:
            gz.write(backup_content)
        
        compressed_buffer.seek(0)
        blob_name = f"backup_{database}_{timestamp}.sql.gz"
        
        blob_service = BlobServiceClient.from_connection_string(storage_connection)
        blob_client = blob_service.get_blob_client(container=backup_container, blob=blob_name)
        
        content_settings = ContentSettings(content_type="application/gzip")
        blob_client.upload_blob(
            compressed_buffer,
            content_settings=content_settings,
            overwrite=True
        )
        
        end_time = datetime.now(timezone.utc)
        duration = (end_time - start_time).total_seconds()
        
        logging.info(
            'Yedekleme tamamlandı. Dosya: %s, Süre: %.2f saniye',
            blob_name,
            duration
        )
        
        conn.close()
        
    except Exception as e:
        logging.error('Yedekleme hatası: %s', str(e))
        raise

Senaryo 3: Harici API’den Veri Çekme ve İşleme

Birçok entegrasyon senaryosunda düzenli aralıklarla harici bir API’den veri çekip işlemeniz gerekiyor. Örneğin döviz kurlarını saatlik olarak çekip bir tabloya yazmak.

# ExchangeRateSync/__init__.py
import logging
import os
import json
import requests
from datetime import datetime, timezone
import azure.functions as func
from azure.data.tables import TableServiceClient, TableEntity

def main(mytimer: func.TimerRequest) -> None:
    logging.info('Döviz kuru senkronizasyonu başladı')
    
    api_key = os.environ["EXCHANGE_API_KEY"]
    base_currency = os.environ.get("BASE_CURRENCY", "USD")
    target_currencies = os.environ.get("TARGET_CURRENCIES", "TRY,EUR,GBP").split(",")
    storage_connection = os.environ["AzureWebJobsStorage"]
    
    api_url = f"https://api.exchangerate-api.com/v4/latest/{base_currency}"
    headers = {"Authorization": f"Bearer {api_key}"}
    
    try:
        response = requests.get(api_url, headers=headers, timeout=10)
        response.raise_for_status()
        data = response.json()
        
        rates = data.get("rates", {})
        timestamp = datetime.now(timezone.utc)
        partition_key = timestamp.strftime("%Y%m%d")
        row_key_prefix = timestamp.strftime("%H%M%S")
        
        table_service = TableServiceClient.from_connection_string(storage_connection)
        table_client = table_service.get_table_client("ExchangeRates")
        
        # Tablo yoksa oluştur
        try:
            table_client.create_table()
        except Exception:
            pass  # Zaten varsa sorun yok
        
        for currency in target_currencies:
            currency = currency.strip()
            if currency in rates:
                entity = TableEntity()
                entity["PartitionKey"] = partition_key
                entity["RowKey"] = f"{row_key_prefix}_{currency}"
                entity["BaseCurrency"] = base_currency
                entity["TargetCurrency"] = currency
                entity["Rate"] = rates[currency]
                entity["Timestamp"] = timestamp.isoformat()
                
                table_client.upsert_entity(entity)
                logging.info('%s/%s kuru kaydedildi: %.4f', base_currency, currency, rates[currency])
        
        logging.info('Döviz kuru senkronizasyonu başarıyla tamamlandı')
        
    except requests.exceptions.Timeout:
        logging.error('API isteği zaman aşımına uğradı')
        raise
    except requests.exceptions.HTTPError as e:
        logging.error('API HTTP hatası: %s', str(e))
        raise
    except Exception as e:
        logging.error('Beklenmeyen hata: %s', str(e))
        raise

Bu fonksiyon için function.json:

{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "name": "mytimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 * * * *"
    }
  ]
}

Her saat başı çalışacak şekilde ayarlandı.

Timezone Ayarı ve host.json Yapılandırması

Türkiye saatine göre çalışması gereken görevler için host.json dosyasını düzenlemek gerekiyor:

{
  "version": "2.0",
  "extensions": {
    "timers": {
      "timeZone": "Turkey Standard Time"
    }
  },
  "functionTimeout": "00:10:00",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "maxTelemetryItemsPerSecond": 20
      }
    },
    "logLevel": {
      "default": "Information",
      "Host.Results": "Error",
      "Function": "Information"
    }
  },
  "retry": {
    "strategy": "exponentialBackoff",
    "maxRetryCount": 3,
    "minimumInterval": "00:00:10",
    "maximumInterval": "00:01:00"
  }
}

timeZone değeri için Windows saat dilimi adlarını kullanmak gerekiyor. Linux’taki IANA formatı (Europe/Istanbul) bazı durumlarda çalışsa da Windows planında her zaman güvenilir değil. Turkey Standard Time değeri güvenli seçenek.

Senaryo 4: E-posta Raporu Gönderme

Haftalık özet raporları otomatik olarak göndermek yaygın bir ihtiyaç. Aşağıdaki örnek Azure Communication Services kullanıyor:

# WeeklyReport/__init__.py
import logging
import os
from datetime import datetime, timedelta, timezone
import azure.functions as func
from azure.communication.email import EmailClient

def build_html_report(stats: dict) -> str:
    return f"""
    <html>
    <body>
        <h2>Haftalık Sistem Raporu</h2>
        <p>Rapor dönemi: {stats['start_date']} - {stats['end_date']}</p>
        <ul>
            <li>Toplam API isteği: {stats['total_requests']}</li>
            <li>Başarılı istek oranı: %{stats['success_rate']:.1f}</li>
            <li>Ortalama yanıt süresi: {stats['avg_response_ms']} ms</li>
            <li>Toplam hata sayısı: {stats['error_count']}</li>
        </ul>
        <p>Bu rapor otomatik olarak oluşturulmuştur.</p>
    </body>
    </html>
    """

def get_weekly_stats() -> dict:
    end_date = datetime.now(timezone.utc)
    start_date = end_date - timedelta(days=7)
    
    # Gerçek senaryoda Application Insights veya veritabanından çekilir
    return {
        "start_date": start_date.strftime("%Y-%m-%d"),
        "end_date": end_date.strftime("%Y-%m-%d"),
        "total_requests": 142857,
        "success_rate": 99.7,
        "avg_response_ms": 245,
        "error_count": 428
    }

def main(mytimer: func.TimerRequest) -> None:
    logging.info('Haftalık rapor gönderimi başladı')
    
    comm_connection = os.environ["COMMUNICATION_SERVICES_CONNECTION"]
    sender_address = os.environ["SENDER_EMAIL"]
    recipient_emails = os.environ["REPORT_RECIPIENTS"].split(",")
    
    stats = get_weekly_stats()
    html_content = build_html_report(stats)
    
    email_client = EmailClient.from_connection_string(comm_connection)
    
    recipients = [{"address": email.strip()} for email in recipient_emails]
    
    message = {
        "senderAddress": sender_address,
        "recipients": {
            "to": recipients
        },
        "content": {
            "subject": f"Haftalık Sistem Raporu - {stats['end_date']}",
            "html": html_content
        }
    }
    
    try:
        poller = email_client.begin_send(message)
        result = poller.result()
        logging.info('Rapor başarıyla gönderildi. MessageId: %s', result.message_id)
    except Exception as e:
        logging.error('Rapor gönderilemedi: %s', str(e))
        raise

Bu fonksiyon için her pazartesi sabah 08:00 Türkiye saatinde çalışacak ayar:

{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "name": "mytimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 8 * * 1"
    }
  ]
}

Dağıtım ve Ortam Değişkenleri

Fonksiyonları Azure’a dağıtmak için şu adımları izliyoruz:

# Resource group oluşturma
az group create 
  --name rg-timer-functions 
  --location westeurope

# Storage account oluşturma
az storage account create 
  --name sttimerfunctions001 
  --resource-group rg-timer-functions 
  --location westeurope 
  --sku Standard_LRS

# Function App oluşturma (Consumption planı)
az functionapp create 
  --resource-group rg-timer-functions 
  --consumption-plan-location westeurope 
  --runtime python 
  --runtime-version 3.11 
  --functions-version 4 
  --name func-timer-production 
  --storage-account sttimerfunctions001 
  --os-type linux

# Ortam değişkenleri ayarlama
az functionapp config appsettings set 
  --resource-group rg-timer-functions 
  --name func-timer-production 
  --settings 
    "LOG_CONTAINER_NAME=application-logs" 
    "LOG_RETENTION_DAYS=30" 
    "DB_SERVER=myserver.database.windows.net" 
    "DB_NAME=productiondb"

# Fonksiyonları dağıtma
func azure functionapp publish func-timer-production

Hassas bilgileri (şifreler, API anahtarları) düz metin olarak appsettings’e koymak yerine Azure Key Vault referansı kullanmanızı şiddetle tavsiye ederim:

# Key Vault oluşturma
az keyvault create 
  --name kv-timer-functions 
  --resource-group rg-timer-functions 
  --location westeurope

# Secret ekleme
az keyvault secret set 
  --vault-name kv-timer-functions 
  --name "DatabasePassword" 
  --value "SuperGizliSifre123!"

# Function App'e Key Vault referansı
az functionapp config appsettings set 
  --resource-group rg-timer-functions 
  --name func-timer-production 
  --settings 
    "[email protected](SecretUri=https://kv-timer-functions.vault.azure.net/secrets/DatabasePassword/)"

Hata Yönetimi ve İzleme

mytimer.past_due özelliğini kontrol etmek genellikle göz ardı ediliyor ama önemli. Fonksiyon herhangi bir sebeple geç tetiklendiyse (cold start, platform bakımı vb.) bunu tespit edip ona göre davranabilirsiniz.

Application Insights ile özel metrikleri takip etmek için şu yaklaşımı kullanıyorum:

# monitoring_helper.py
import logging
import os
from applicationinsights import TelemetryClient

def get_telemetry_client():
    instrumentation_key = os.environ.get("APPINSIGHTS_INSTRUMENTATIONKEY")
    if instrumentation_key:
        return TelemetryClient(instrumentation_key)
    return None

def track_timer_execution(function_name: str, duration_seconds: float, success: bool, properties: dict = None):
    client = get_telemetry_client()
    if client:
        client.track_metric(
            name=f"TimerFunction.Duration",
            value=duration_seconds,
            properties={"FunctionName": function_name}
        )
        client.track_event(
            name="TimerFunctionExecuted",
            properties={
                "FunctionName": function_name,
                "Success": str(success),
                **(properties or {})
            }
        )
        client.flush()

Sık Karşılaşılan Sorunlar ve Çözümleri

Fonksiyon birden fazla kez tetikleniyor: Consumption planında birden fazla instance çalışıyorsa bu sorunla karşılaşabilirsiniz. Azure Functions varsayılan olarak blob tabanlı bir kilit mekanizması kullanıyor ve tek instance çalıştırılmasını garanti ediyor. Ancak WEBSITE_RUN_FROM_PACKAGE ayarının doğru yapılandırılmış olduğundan emin olun.

Uzun süren görevler timeout oluyor: host.json dosyasında functionTimeout varsayılan değeri Consumption planında 5 dakika, Premium ve Dedicated planlarda 30 dakika. Daha uzun görevler için Premium plan veya Azure Durable Functions kullanmak gerekiyor.

Past_due uyarısı çok sık geliyor: Platform sorunları veya cold start kaynaklı olabilir. Premium plana geçmek ve “Always On” ayarını aktif etmek bu sorunu büyük ölçüde gideriyor.

Timezone karışıklığı: Her zaman UTC ile çalışın ve bunu takım içinde standart hale getirin. Timezone dönüşümlerini host.json’da yapın, kod içinde yapmaktan kaçının.

Yerel Test

Üretime göndermeden önce yerel ortamda test etmek için:

# local.settings.json oluştur
cat > local.settings.json << 'EOF'
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "LOG_CONTAINER_NAME": "test-logs",
    "LOG_RETENTION_DAYS": "7"
  }
}
EOF

# Azurite başlat (yerel storage emülatörü)
docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite

# Fonksiyonları başlat
func start

# Manuel tetikleme (test için)
curl -X POST "http://localhost:7071/admin/functions/DailyCleanup" 
  -H "Content-Type: application/json" 
  -d "{}"

Admin endpoint ile herhangi bir timer trigger fonksiyonunu zamanını beklemeden manuel olarak tetikleyebilirsiniz. Test süreçlerinde hayat kurtarıcı.

Sonuç

Azure Functions Timer Trigger, sunucu yönetimi yükünü üzerinizden alırken zamanlanmış görevlerinizi güvenilir biçimde çalıştırmak için güçlü bir araç. NCRONTAB formatının 6 alan kullandığını ve UTC’yi varsayılan aldığını aklınızda tutarsanız, şaşırtıcı derecede az kodla karmaşık zamanlamaları hayata geçirebilirsiniz.

Gerçek ortamlarda dikkat etmeniz gereken birkaç kritik nokta:

  • Hassas bilgileri Key Vault ile yönetin, appsettings’e düz metin koymayın
  • past_due kontrolünü mutlaka ekleyin, sessizce geçmeyin
  • Uzun süren işler için timeout limitini aşmadan önce Durable Functions’a geçmeyi düşünün
  • Application Insights entegrasyonunu baştan kurun, sorun çıktığında aramaya çalışmayın
  • Local test için Azurite ve admin endpoint kombinasyonunu alışkanlık haline getirin

Serverless dünyasında “set and forget” yaklaşımı cazip geliyor ama izleme ve alerting olmadan bu kolaylık bir süre sonra bumeranga dönüşebiliyor. Fonksiyonlarınızı dağıtırken monitoring’i de beraberinde götürün.

Bir yanıt yazın

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