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_duekontrolü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.
