Serverless ile Arka Plan İşi: Azure Functions Isolated Worker, Flex Consumption ve Blob Tetiklemeli Kapak Resmi İşleme
Kısa ve düzensiz işler için serverless doğru seçimdir: Bookstore'un kapak küçük resimleri, Flex Consumption'da çalışan, Event Grid tetiklemeli bir .NET 10 isolated worker function ile üretiliyor. In-process modelin sonunu, Azure.Functions.Sdk'yı, hosting planlarını, ETag ile idempotency'yi, Azurite ve Core Tools ile yerelde elle gönderilen Event Grid olayını, poison kuyruğunu, managed identity'li bağlantıyı ve Lambda karşılıklarını anlatıyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.404), Azure.Functions.Sdk 1.1.0, Microsoft.Azure.Functions.Worker 2.52.0, Blob Storage extension 6.8.2, SkiaSharp 4.156.0, Azure Functions Core Tools 4.16.1, Azurite 3.37.0 ve Azure CLI 2.92.0 ile denenmiştir. Function yerelde Azurite'e karşı çalıştırıldı; Event Grid olayı elle gönderildi, Azure'a deploy yapılmadı. Plan ve limit bilgileri Microsoft Learn'den (Kasım 2026).
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) mağaza çalışanı bir kitabın kapak resmini yüklediğinde, katalog sayfası için 320 piksel genişliğinde bir küçük resim üretilmesi gerekiyor. Bunu yeni bir blob gelince çalışan bir Azure Function yapıyor. İki soru: Event Grid aynı "blob oluşturuldu" olayını iki kez teslim ederse function kaç kez çalışır ve kaç küçük resim yazılır? Biri yanlışlıkla covers container'ına bir metin dosyası yüklerse ne olur, function sonsuza kadar mı dener? İkisini de yerelde denedik.
Serverless'ın Anlamı ve Sınırları
1. yazıda serverless'ı sunucuyu düşünmediğin, iş yokken sıfıra inen ve kullanım kadar ödenen model diye tanımladık. Function as a Service bunun en ince taneli hâli: dağıttığın şey bir uygulama değil, olaylara bağlanmış fonksiyonlar. Kapak resmi işleme bu modele tam uyuyor: ayda birkaç yüz kez, düzensiz zamanlarda, birkaç yüz milisaniyelik bir iş.
Sınırları da net. Birincisi süre: Learn'e göre Flex Consumption'da bir çalıştırmanın varsayılan zaman aşımı 30 dakika ve üst sınır yok, ama ölçek küçülürken çalışan işe 60 dakikalık, platform güncellemesinde 10 dakikalık süre tanınıyor; HTTP tetiklemeli bir function ise ne ayarlarsan ayarla en fazla 230 saniyede cevap vermek zorunda. AWS'nin (Amazon Web Services; Amazon'un sunduğu bulut platformu) Lambda servisinde tek çalıştırma en fazla 15 dakika sürebilir. İkincisi cold start: sıfırdan kalkan instance'ın ilk isteği yavaştır; 4. yazıda API için ölçtüğümüz durumun aynısı. Üçüncüsü state: iki çalıştırma arasında bellekte bir şey taşıyamazsın, her şey dışarıda durmalı. Dördüncüsü bağımlılık: trigger'lar, binding'ler ve hosting modeli sağlayıcıya özgü, başka bir buluta taşımak kodu yeniden yazmak demek.
Isolated Worker Modeli ve Azure.Functions.Sdk
.NET function'ları yıllarca iki modelde yazıldı. In-process modelde kod, Functions host'unun süreci içinde çalışıyordu. Isolated worker modelde ise kodun kendi .NET sürecinde çalışır ve host'la gRPC üzerinden konuşur; .NET sürümünü host'tan bağımsız seçebilir, normal bir .NET uygulamasındaki gibi dependency injection ve middleware kullanabilirsin. Learn'e göre in-process modelin desteği 10 Kasım 2026'da sona erdi; .NET 10 yalnız isolated worker modelde çalışıyor ve Flex Consumption in-process modeli hiç desteklemiyor. İnternette bulduğun [FunctionName] attribute'lu ve Microsoft.NET.Sdk.Functions paketli örnekler bu eski modele ait.
Proje dosyası da sadeleşti. Yeni Azure.Functions.Sdk, eski Microsoft.Azure.Functions.Worker.Sdk paketinin yerini alan bir MSBuild proje SDK'sı: function metadata'sını üreten source generator'ları ekliyor, AzureFunctionsVersion'ı v4 yapıyor ve Core Tools kuruluysa dotnet run ile host'u başlatıyor. Bookstore'un yeni projesi:
<Project Sdk="Azure.Functions.Sdk">
<PropertyGroup>
<RootNamespace>Bookstore.Functions</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs" />
<PackageReference Include="SkiaSharp" />
<!-- Flex Consumption Linux'ta çalışır: ek sistem paketi istemeyen yerel Skia kütüphanesi -->
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" />
</ItemGroup>
</Project>
Proje dosyasında SDK sürümü yok, çünkü depo kökündeki global.json'a eklendi:
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
},
"test": {
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
"Azure.Functions.Sdk": "1.1.0"
}
}
Paket sürümleri, depodaki diğer projeler gibi Directory.Packages.props'ta merkezi olarak tutuluyor. Learn, .NET 10 için Microsoft.Azure.Functions.Worker'ın en az 2.50.0 olmasını öneriyor; biz 2.52.0 kullanıyoruz. Program.cs birkaç satır:
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
FunctionsApplicationBuilder builder = FunctionsApplication.CreateBuilder(args);
builder.Build().Run();
Trigger'lar, Binding'ler ve Hosting Planları
Her function'ın tek bir trigger'ı vardır: HTTP isteği, zamanlayıcı (cron ifadesi), Storage ya da Service Bus kuyruğundaki mesaj, yeni bir blob. Binding'ler ise function'ın başka servislerle konuşmasını kolaylaştıran giriş ve çıkışlardır. Isolated modelde binding'ler doğrudan Azure SDK tiplerini verebiliyor: BlobClient ya da BlobContainerClient alıp istediğin her işlemi yapabilirsin.
Blob trigger'ın iki uygulaması var. Eski polling tabanlı olan, container'ı belirli aralıklarla tarar ve storage log'larına bakar; Learn, Consumption planında uygulama boştayken yeni blob'un işlenmesinin 10 dakikaya kadar gecikebileceğini ve log'ların "best effort" yazıldığını söylüyor. Event Grid tabanlı olan ise storage hesabındaki bir event subscription ile anında tetiklenir ve Flex Consumption yalnız bunu destekliyor. Bookstore'da Event Grid tabanlı olanı kullanıyoruz.
Hosting planları dört tane. Flex Consumption, yeni serverless uygulamalar için önerilen plan: yalnız Linux, kod olarak deploy, 512 MB, 2.048 MB ya da 4.096 MB instance boyutu, VNet entegrasyonu ve cold start'ı azaltmak için always ready instance'lar. Premium plan sürekli sıcak tutulan instance'larla çalışır ve uzun ya da sürekli işler için uygundur. Dedicated plan, function'ı bir App Service planında diğer web uygulamalarıyla birlikte çalıştırır. Dördüncüsü Container Apps: function'ı bir container image olarak 5. yazıdaki environment'a koyarsın. Eski Consumption planının Linux sürümü 30 Eylül 2028'de emekliye ayrılıyor ve .NET 10 uygulamalarını zaten çalıştıramıyor; Windows sürümü şimdilik etkilenmiyor.
Kapak Küçük Resmi: Function'ın Kodu
Resmi küçültme işini function'dan ayırdık ki test edilebilsin. Görüntü işleme için MIT (serbestçe kullanılabilen açık kaynak lisansı) lisanslı SkiaSharp kullanıyoruz; Flex Consumption Linux üzerinde çalıştığı için ek sistem paketi istemeyen SkiaSharp.NativeAssets.Linux.NoDependencies paketi de projede:
using SkiaSharp;
namespace Bookstore.Functions;
public static class CoverThumbnail
{
public const int Width = 320;
// En boy oranını korur; küçük bir kapağı asla büyütmez
public static (int Width, int Height) FitToWidth(int width, int height, int maxWidth)
{
if (width <= 0 || height <= 0)
{
throw new ArgumentOutOfRangeException(nameof(width), "Image size must be positive.");
}
if (width <= maxWidth)
{
return (width, height);
}
return (maxWidth, (int)Math.Round(height * (double)maxWidth / width));
}
public static byte[] CreateWebp(Stream source, int maxWidth = Width, int quality = 80)
{
using SKBitmap original = SKBitmap.Decode(source)
?? throw new InvalidDataException("The blob is not an image Skia can decode.");
(int width, int height) = FitToWidth(original.Width, original.Height, maxWidth);
using SKBitmap resized = original.Resize(new SKImageInfo(width, height), new SKSamplingOptions(SKCubicResampler.Mitchell));
using SKImage image = SKImage.FromBitmap(resized);
using SKData data = image.Encode(SKEncodedImageFormat.Webp, quality);
return data.ToArray();
}
}
Function'ın kendisi:
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace Bookstore.Functions;
public sealed class CoverThumbnailFunction(ILogger<CoverThumbnailFunction> logger)
{
// Flex Consumption yalnız Event Grid tabanlı blob trigger'ı destekler
[Function(nameof(CoverThumbnail))]
public async Task CoverThumbnail(
[BlobTrigger("covers/{name}", Source = BlobTriggerSource.EventGrid, Connection = "CoversStorage")] BlobClient cover,
[BlobInput("covers-thumbnails", Connection = "CoversStorage")] BlobContainerClient thumbnails,
string name,
CancellationToken cancellationToken)
{
BlobProperties source = await cover.GetPropertiesAsync(cancellationToken: cancellationToken);
string sourceETag = source.ETag.ToString();
BlobClient thumbnail = thumbnails.GetBlobClient(Path.ChangeExtension(name, ".webp"));
// Event Grid en az bir kez teslim eder: aynı sürüm iki kez işlenmemeli
try
{
BlobProperties existing = await thumbnail.GetPropertiesAsync(cancellationToken: cancellationToken);
if (existing.Metadata.TryGetValue("sourceetag", out string? processed) && processed == sourceETag)
{
logger.LogInformation("Thumbnail for {Name} is up to date (ETag {ETag}), skipping", name, sourceETag);
return;
}
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
}
await using Stream image = await cover.OpenReadAsync(cancellationToken: cancellationToken);
byte[] webp = Bookstore.Functions.CoverThumbnail.CreateWebp(image);
await thumbnails.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
await thumbnail.UploadAsync(new BinaryData(webp), new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders { ContentType = "image/webp", CacheControl = "public, max-age=31536000" },
Metadata = new Dictionary<string, string> { ["sourceetag"] = sourceETag },
}, cancellationToken);
logger.LogInformation("Created {Thumbnail} ({Bytes} bytes) from {Name} ({SourceBytes} bytes)",
thumbnail.Name, webp.Length, name, source.ContentLength);
}
}
Üç kararı açalım. Source = BlobTriggerSource.EventGrid, trigger'ın Event Grid tabanlı olduğunu söyler. Connection = "CoversStorage" bir connection string değil, bir ayar önekidir: yerelde CoversStorage=UseDevelopmentStorage=true, Azure'da ise CoversStorage__blobServiceUri gibi managed identity ayarları. En önemlisi idempotency (aynı işi iki kez yapmanın bir kez yapmakla aynı sonucu vermesi): Event Grid olayları en az bir kez teslim eder, yani aynı olay iki kez gelebilir. Küçük resmin metadata'sına kaynak blob'un ETag'ini yazıyoruz; aynı ETag için ikinci çalıştırma hiçbir şey yazmadan çıkıyor. Kapak değişirse ETag değişir ve küçük resim yeniden üretilir. Background Job'lar ve Scheduled Job'lar serisinin "Background Job'larda Retry ve Idempotency: At-Least-Once Gerçeğiyle Yaşamak" yazısı aynı konuyu job'lar için ayrıntılı anlatıyor.
host.json'da log seviyelerini kısıp eşzamanlı çalıştırmayı sınırladık; yerel ayarlar için repoya bir örnek dosya koyduk, asıl local.settings.json .gitignore'da:
{
"version": "2.0",
"logging": {
"logLevel": {
"default": "Warning",
"Function": "Information",
"Host.Results": "Information"
}
},
"extensions": {
"blobs": {
"maxDegreeOfParallelism": 4
}
}
}
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"CoversStorage": "UseDevelopmentStorage=true"
}
}
Küçültme mantığının testleri ayrı bir projede, tests/Bookstore.Functions.Tests'te:
using SkiaSharp;
namespace Bookstore.Functions.Tests;
public class CoverThumbnailTests
{
[Theory]
[InlineData(1200, 1800, 320, 480)] // tipik dikey kapak
[InlineData(1600, 900, 320, 180)] // yatay kutu set görseli
[InlineData(200, 300, 200, 300)] // hedeften küçük: değişmez
public void FitToWidth_keeps_the_aspect_ratio(int width, int height, int expectedWidth, int expectedHeight)
{
Assert.Equal((expectedWidth, expectedHeight), CoverThumbnail.FitToWidth(width, height, 320));
}
[Fact]
public void CreateWebp_resizes_a_jpeg_cover()
{
using SKBitmap bitmap = new(1200, 1800);
using (SKCanvas canvas = new(bitmap))
{
canvas.Clear(SKColors.SteelBlue);
}
using SKData jpeg = SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Jpeg, 90);
using MemoryStream source = new(jpeg.ToArray());
byte[] webp = CoverThumbnail.CreateWebp(source);
using SKBitmap thumbnail = SKBitmap.Decode(webp);
Assert.Equal((320, 480), (thumbnail.Width, thumbnail.Height));
}
[Fact]
public void CreateWebp_rejects_a_file_that_is_not_an_image()
{
using MemoryStream source = new("not an image"u8.ToArray());
Assert.Throws<InvalidDataException>(() => CoverThumbnail.CreateWebp(source));
}
}
Çözümün tamamında dotnet test: 18 API testi ve 5 function testi.
Test run summary: Passed!
total: 23
failed: 0
succeeded: 23
skipped: 0
Yerelde Çalıştırmak: Azurite, Core Tools ve Event Grid'i Taklit Etmek
Yerelde üç parça gerekiyor: storage için Azurite emülatörü, Functions host'u için Azure Functions Core Tools ve bir olay. Core Tools'u Windows'ta Learn'deki MSI (Windows kurulum paketi biçimi) paketiyle ya da npm install -g azure-functions-core-tools@4 ile kurabilirsin. Azurite'i container olarak başlattık; --skipApiVersionCheck, SDK'nın Azurite'ten daha yeni bir REST (HTTP üzerinden kaynak odaklı API tarzı) sürümü istemesi durumunda isteklerin reddedilmemesi için:
$ docker run -d --name bookstore-azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 \
mcr.microsoft.com/azure-storage/azurite \
azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --skipApiVersionCheck
Dune'un (ISBN 9780441172719) 1200×1800 piksellik kapağını Azure CLI ile yükledik. UseDevelopmentStorage=true, Azurite'in bilinen yerel hesabını kullanmanın kısa yolu; key'i elle yazmana gerek yok:
$ az storage container create --name covers --connection-string "UseDevelopmentStorage=true"
{
"created": true
}
$ az storage blob upload --container-name covers --name 9780441172719.jpg \
--file 9780441172719.jpg --content-type image/jpeg --connection-string "UseDevelopmentStorage=true"
Sonra function projesinde dotnet run. Azure.Functions.Sdk, Core Tools'u bulup host'u başlatıyor:
Azure Functions Core Tools
Core Tools Version: 4.16.1+f1ade740bcec588a2f068c53a46fb3e0acf525c5 (64-bit)
Function Runtime Version: 4.1054.250.26428
[2026-11-14T19:59:05.448Z] Worker process started and initialized.
Functions:
CoverThumbnail: blobTrigger
Açılışta Azure SDK'nın henüz var olmayan kuyruklar için yazdığı birkaç "404 The specified queue does not exist" satırı görürsün; extension kuyrukları ilk ihtiyaçta oluşturuyor, bu satırlar hata değil. Blob yüklenmiş olsa da function çalışmadı, çünkü yerelde Event Grid yok. Event Grid'in yapacağını elle yapıyoruz: blob extension'ın webhook'una bir BlobCreated olayı gönderiyoruz. Olayın gövdesi, Event Grid şemasında tek elemanlı bir dizi; data.url blob'un adresi, aeg-event-type: Notification header'ı da bunun bir bildirim olduğunu söylüyor:
$ curl -s -i -X POST "http://localhost:7071/runtime/webhooks/blobs?functionName=Host.Functions.CoverThumbnail" \
-H "Content-Type: application/json" -H "aeg-event-type: Notification" --data @blobcreated.json
HTTP/1.1 202 Accepted
[2026-11-14T19:59:19.223Z] Executing 'Functions.CoverThumbnail' (Reason='New blob detected(EventGrid): covers/9780441172719.jpg', Id=a9d640f0-f8ff-41b2-90f5-28433f10a9b0)
[2026-11-14T19:59:19.224Z] Trigger Details: MessageId: f2395ee5-c4b8-4f08-9ef6-a4c2089ae7a9, DequeueCount: 1, InsertedOn: 2026-11-14T19:59:19.000+00:00, BlobCreated: 2026-11-14T19:58:38.000+00:00, BlobLastModified: 2026-11-14T19:58:38.000+00:00
[2026-11-14T19:59:19.606Z] Created 9780441172719.webp (754 bytes) from 9780441172719.jpg (57117 bytes)
[2026-11-14T19:59:19.611Z] Executed 'Functions.CoverThumbnail' (Succeeded, Id=a9d640f0-f8ff-41b2-90f5-28433f10a9b0, Duration=399ms)
"Trigger Details" satırındaki MessageId ve DequeueCount ilginç: extension, webhook'a gelen olayı önce storage'daki iç bir kuyruğa yazıyor, function'ı oradan çalıştırıyor. Azure'da bu yüzden function'ın kimliğine blob rolünün yanında kuyruk rolü de gerekiyor. Aynı olayı ikinci kez gönderince:
[2026-11-14T19:59:29.523Z] Executing 'Functions.CoverThumbnail' (Reason='New blob detected(EventGrid): covers/9780441172719.jpg', Id=deecfbed-381e-4457-b045-2544b6ca8eb2)
[2026-11-14T19:59:29.532Z] Thumbnail for 9780441172719.jpg is up to date (ETag "0x27523C388165980"), skipping
[2026-11-14T19:59:29.532Z] Executed 'Functions.CoverThumbnail' (Succeeded, Id=deecfbed-381e-4457-b045-2544b6ca8eb2, Duration=11ms)
Tahmin sorusunun ilk cevabı: function iki kez çalışıyor, küçük resim bir kez yazılıyor. Polling tabanlı trigger'daki blob receipt mekanizması aynı blob sürümünün ikinci kez işlenmesini engelliyordu; Event Grid tabanlı trigger'da çift teslimatı kodun karşılaması gerekiyor. Sonuç Azurite'te:
$ az storage blob list --container-name covers-thumbnails --include m --connection-string "UseDevelopmentStorage=true" \
--query "[].{name:name, size:properties.contentLength, sourceetag:metadata.sourceetag}" -o table
Name Size Sourceetag
------------------ ------ -------------------
9780441172719.webp 754 "0x27523C388165980"
Şimdi ikinci soru. covers'a 12 baytlık readme.txt yükleyip olayını gönderdik:
[2026-11-14T19:59:43.650Z] Exception: System.IO.InvalidDataException: The blob is not an image Skia can decode.
[2026-11-14T19:59:43.853Z] Executed 'Functions.CoverThumbnail' (Failed, Id=6f0eea91-afc8-4ef7-85b6-3d243f1bde23, Duration=226ms)
...
[2026-11-14T19:59:44.260Z] Trigger Details: MessageId: e6cc6eb8-47ed-4b02-89b1-719b91eccbe5, DequeueCount: 5, ...
[2026-11-14T19:59:44.270Z] Message has reached MaxDequeueCount of 5. Moving message to queue 'webjobs-blobtrigger-poison'.
Function beş kez deniyor, sonra mesajı webjobs-blobtrigger-poison kuyruğuna taşıyıp vazgeçiyor. Kuyruktaki mesaj, hangi blob'un neden işlenemediğini araştırmak için yeterli bilgiyi taşıyor:
{
"Type": "BlobTrigger",
"FunctionId": "Host.Functions.CoverThumbnail",
"BlobType": "BlockBlob",
"ContainerName": "covers",
"BlobName": "readme.txt",
"ETag": "\"\""
}
Beş deneme, Learn'deki varsayılan; host.json'daki poisonBlobThreshold ile değiştirilebiliyor. Bir resmin çözülememesi tekrar denemeyle düzelmeyeceği için bu durumda beş deneme boşa; daha iyi olan, event subscription'a .jpg ve .png için subject filtresi koymak ve yükleme endpoint'inde dosya tipini doğrulamak (7. yazı). Poison kuyruğunu da bir alarma bağla; dolduğunu kimse fark etmezse kapaklar sessizce küçük resimsiz kalır.
Azure'a Almak: Managed Identity'li Bağlantı ve Event Subscription
Azure tarafı bir script. Function app'i Flex Consumption'da açıyor, 3. yazıdaki identity'yi bağlıyor, gerekli rolleri veriyor ve storage hesabında bir event subscription oluşturuyor:
#!/usr/bin/env bash
# Flex Consumption'da Bookstore.Functions: kapak küçük resimleri, identity tabanlı storage erişimi, Event Grid aboneliği.
set -euo pipefail
: "${AZURE_SUBSCRIPTION_ID:?Set AZURE_SUBSCRIPTION_ID to the target subscription}"
ENVIRONMENT="${1:?Usage: 06-functions.sh <dev|staging|prod>}"
RG="rg-bookstore-${ENVIRONMENT}-weu"
FUNC="func-bookstore-${ENVIRONMENT}-weu"
IDENTITY="id-bookstore-${ENVIRONMENT}-weu"
STORAGE="stbookstore${ENVIRONMENT}weu"
HOST_STORAGE="stbookstorefn${ENVIRONMENT}weu"
az account set --subscription "$AZURE_SUBSCRIPTION_ID"
IDENTITY_ID=$(az identity show --name "$IDENTITY" --resource-group "$RG" --query id --output tsv)
CLIENT_ID=$(az identity show --name "$IDENTITY" --resource-group "$RG" --query clientId --output tsv)
PRINCIPAL_ID=$(az identity show --name "$IDENTITY" --resource-group "$RG" --query principalId --output tsv)
STORAGE_ID=$(az storage account show --name "$STORAGE" --resource-group "$RG" --query id --output tsv)
# Functions host'u kendi durumunu (deploy paketi, trigger kuyrukları) ayrı bir hesapta tutar
az storage account create --name "$HOST_STORAGE" --resource-group "$RG" --location westeurope \
--sku Standard_LRS --allow-blob-public-access false
az functionapp create --name "$FUNC" --resource-group "$RG" \
--storage-account "$HOST_STORAGE" --flexconsumption-location westeurope \
--runtime dotnet-isolated --runtime-version 10.0 \
--instance-memory 2048 --maximum-instance-count 20 \
--assign-identity "$IDENTITY_ID"
# Trigger covers'ı okur, covers-thumbnails'e yazar; blob trigger o hesapta kuyruk da kullanır
az storage container-rm create --storage-account "$STORAGE" --name covers-thumbnails
for ROLE in "Storage Blob Data Owner" "Storage Queue Data Contributor"; do
az role assignment create --assignee-object-id "$PRINCIPAL_ID" --assignee-principal-type ServicePrincipal \
--role "$ROLE" --scope "$STORAGE_ID"
done
# Identity tabanlı bağlantı: connection string yok, yalnız hesabın servis adresleri ve identity
az functionapp config appsettings set --name "$FUNC" --resource-group "$RG" --settings \
"CoversStorage__blobServiceUri=https://${STORAGE}.blob.core.windows.net" \
"CoversStorage__queueServiceUri=https://${STORAGE}.queue.core.windows.net" \
"CoversStorage__credential=managedidentity" \
"CoversStorage__clientId=${CLIENT_ID}"
(cd src/Bookstore.Functions && func azure functionapp publish "$FUNC")
# Event Grid, blob extension'ın webhook'unu çağırır; anahtar onu korur
KEY=$(az functionapp keys list --name "$FUNC" --resource-group "$RG" --query "systemKeys.blobs_extension" --output tsv)
ENDPOINT="https://${FUNC}.azurewebsites.net/runtime/webhooks/blobs?functionName=Host.Functions.CoverThumbnail&code=${KEY}"
az eventgrid event-subscription create --name covers-created \
--source-resource-id "$STORAGE_ID" --endpoint "$ENDPOINT" \
--included-event-types Microsoft.Storage.BlobCreated \
--subject-begins-with /blobServices/default/containers/covers/blobs/
Roller Learn'deki tablodan: blob trigger için hem "Storage Blob Data Owner" hem "Storage Queue Data Contributor" gerekiyor, çünkü extension iç kuyruğu ve poison kuyruğunu aynı hesapta tutuyor; bu yüzden CoversStorage__queueServiceUri ayarı da zorunlu. 3. yazıda Bookstore.Api'ye yalnız covers container'ında yazma yetkisi vermiştik; function için hesap scope'unda daha geniş bir rol gerekiyor, bu yüzden function'a ayrı bir identity vermek (ör. id-bookstore-fn-dev-weu) daha temiz bir seçenek. Script'i kısa tutmak için aynı identity'yi kullandık; bu bir ödünleşim ve Küçük Bir Deneme'de düzeltiyoruz. Webhook adresindeki code parametresi, function app'in blobs_extension sistem anahtarı; Event Grid'den başkasının bu adrese olay atmasını engelliyor. Script'i ShellCheck ile denetledik ama Azure'a karşı çalıştırmadık.
Ne Zaman Functions Değil
Functions'ın ekonomisi düzensiz ve kısa işler için çalışır. Kampanya gecesi binlerce siparişi işleyen saatlerce süren bir rapor, sürekli dolu bir kuyruğu tüketen bir worker ya da bellekte büyük bir model tutan bir servis için daha iyi seçenekler var: Container Apps job'ları (zamanlanmış ya da olayla tetiklenen, bitince kapanan container'lar), API'nin içinde çalışan bir BackgroundService ya da Hangfire gibi kalıcı bir job sistemi. Background Job'lar ve Scheduled Job'lar serisi bu seçenekleri karşılaştırıyor. Sürekli yük altında Flex Consumption'ın saniye başı fiyatı, sabit bir plandan pahalıya gelebilir. Aşağıdaki tablo Lambda ile karşılaştırma; S3 (Simple Storage Service; AWS'nin object storage servisi) Blob Storage'ın karşılığı.
| Konu | Azure Functions (Flex Consumption) | AWS Lambda |
|---|---|---|
| Yeni blob tetikleyicisi | Blob trigger, Event Grid kaynaklı | S3 event notification ya da EventBridge |
| Çalışma süresi sınırı | Varsayılan 30 dakika, üst sınır yok; HTTP cevabı 230 saniye | En fazla 15 dakika |
| Bellek | 512, 2.048 ya da 4.096 MB instance | 128 MB ile 10.240 MB arası |
| Cold start'ı azaltmak | Always ready instance'lar | Provisioned concurrency |
| Storage'a erişim | Managed identity ve identity tabanlı bağlantı | Execution role |
| Başarısız olay | Poison kuyruğu | Dead-letter queue ya da on-failure destination |
Üretimde Dikkat
Host storage'ı ile iş storage'ını ayır. Functions host'u deploy paketi ve iç kuyruklar için kendi storage hesabını kullanır; script'teki stbookstorefn... bu yüzden ayrı. Kapakların durduğu hesaba host'un yükünü bindirmek, iki işin birbirini etkilemesine yol açar.
HttpClient'ı ve SDK client'larını her çalıştırmada yeniden oluşturma. Function her olayda çalışır; her seferinde yeni bir HttpClient açmak soket tükenmesine (socket exhaustion) götürür. Client'ları DI'a (dependency injection; nesneleri dışarıdan veren yapı) bir kez kaydet ya da binding'in verdiği client'ı kullan; Bookstore'un function'ı ikincisini yapıyor.
Çalıştırmaları izle. Başarısız çalıştırma sayısı, poison kuyruğunun uzunluğu ve süre dağılımı, Observability serisinde anlatılan Application Insights ve OpenTelemetry kurulumuyla görünür olmalı.
Sık Düşülen Tuzaklar
In-process modelli eski örnekleri kopyalamak. [FunctionName], Microsoft.NET.Sdk.Functions ve ILogger log parametresi gördüğün örnek, 10 Kasım 2026'da desteği biten modele ait; .NET 10'da ve Flex Consumption'da çalışmaz.
Flex Consumption'da polling tabanlı blob trigger kullanmak. Flex yalnız Event Grid kaynaklı trigger'ı destekliyor; event subscription kurulmazsa function hiç tetiklenmez ve hata da vermez.
Idempotency'yi "nasılsa bir kez çalışır" diye atlamak. Event Grid ve kuyruklar en az bir kez teslim eder; çift çalıştırma ender değil, normal.
Büyük blob'u string ya da byte[] olarak bind etmek. Learn, bu tiplerde blob'un tamamının belleğe birden çok kez yüklendiğini söylüyor; büyük dosyalar için Stream ya da BlobClient kullan.
Küçük Bir Deneme
★☆☆ host.json'a "poisonBlobThreshold": 1 ekleyip metin dosyası denemesini tekrarla; poison kuyruğuna kaçıncı denemede düşüyor?
★★☆ Function'a ikinci bir boyut ekle: 640 piksellik bir küçük resim de üret. Idempotency kontrolünü iki çıktı için de doğru çalışacak şekilde düzenle ve testlerini yaz.
★★★ Script'i function için ayrı bir user-assigned identity (id-bookstore-fn-dev-weu) kullanacak şekilde değiştir. Bookstore.Api ile function'ın yetkilerini bir tabloda karşılaştır ve hangi rolün hangi scope'ta olduğunu gerekçesiyle yaz.
Bir sonraki yazıda kapakların durduğu yere iniyoruz: Blob Storage'da container'lar, redundancy ve access tier seçenekleri, user delegation SAS (Shared Access Signature; süresi ve yetkisi sınırlı erişim adresi) ile geçici erişim ve Bookstore.Api'ye kapak yükleme endpoint'i.