MidПрактика3 min

Azure Functions на практике

Создание, деплой и использование Azure Functions: Service Bus и Timer триггеры

Практическое руководство по созданию, настройке и деплою Azure Functions для обработки событий и фоновых задач.

Создание Function App

# Storage Account (обязателен для Functions)
az storage account create \
  --name storderappfunc \
  --resource-group rg-orderapp-dev \
  --location westeurope \
  --sku Standard_LRS

# Function App на Consumption Plan
az functionapp create \
  --name func-orderapp-dev \
  --resource-group rg-orderapp-dev \
  --storage-account storderappfunc \
  --consumption-plan-location westeurope \
  --runtime dotnet-isolated \
  --runtime-version 8 \
  --functions-version 4 \
  --assign-identity  # Включить Managed Identity

Пример: Service Bus триггер

Функция обрабатывает события из очереди Service Bus:

public class OrderConfirmedFunction
{
    private readonly IEmailService _emailService;
    private readonly ILogger<OrderConfirmedFunction> _logger;

    public OrderConfirmedFunction(
        IEmailService emailService,
        ILogger<OrderConfirmedFunction> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }

    [Function("OrderConfirmedHandler")]
    public async Task Run(
        [ServiceBusTrigger("order-confirmed", Connection = "ServiceBus")]
        ServiceBusReceivedMessage message,
        ServiceBusMessageActions messageActions)
    {
        try
        {
            var evt = message.Body.ToObjectFromJson<OrderConfirmedMessage>();
            _logger.LogInformation("Processing order {OrderId}", evt.OrderId);

            await _emailService.SendOrderConfirmationAsync(
                evt.CustomerEmail, evt.OrderId, evt.TotalAmount);

            await messageActions.CompleteMessageAsync(message);
            _logger.LogInformation("Order {OrderId} email sent", evt.OrderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed: {MessageId}", message.MessageId);
            throw; // Message returns to queue for retry
        }
    }
}

public record OrderConfirmedMessage(
    Guid OrderId,
    string CustomerEmail,
    decimal TotalAmount);

Пример: Timer триггер

Функция генерирует ежедневный отчет:

[Function("DailyOrderReport")]
public async Task RunReport(
    [TimerTrigger("0 0 8 * * *")] TimerInfo timer)
{
    _logger.LogInformation("Generating daily report at {Time}", DateTime.UtcNow);

    var yesterday = DateTime.UtcNow.Date.AddDays(-1);
    var orders = await _orderRepository.GetOrdersByDateAsync(yesterday);

    var report = new DailyReport
    {
        Date = yesterday,
        TotalOrders = orders.Count,
        TotalRevenue = orders.Sum(o => o.TotalAmount),
        AverageOrderValue = orders.Average(o => o.TotalAmount)
    };

    // Save report to Blob Storage
    var blobClient = _blobContainer.GetBlobClient($"reports/{yesterday:yyyy-MM-dd}.json");
    await blobClient.UploadAsync(
        BinaryData.FromObjectAsJson(report), overwrite: true);
}

Пример: Blob триггер

Функция обрабатывает загруженные файлы:

[Function("ProcessUploadedDocument")]
public async Task RunBlobTrigger(
    [BlobTrigger("uploads/{name}", Connection = "Storage")]
    Stream stream,
    string name)
{
    _logger.LogInformation("Processing file: {Name}", name);

    if (name.EndsWith(".csv"))
    {
        using var reader = new StreamReader(stream);
        var content = await reader.ReadToEndAsync();
        // Parse CSV and process...
    }
}

Program.cs для Functions

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices((context, services) =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();

        // Register services from Application and Infrastructure layers
        services.AddApplication();
        services.AddInfrastructure(context.Configuration);
    })
    .Build();

host.Run();

Деплой Functions

# Публикация
cd src/OrderManagement.Functions
dotnet publish -c Release -o ./publish
cd publish
zip -r ../functions.zip .

# Деплой
az functionapp deployment source config-zip \
  --resource-group rg-orderapp-dev \
  --name func-orderapp-dev \
  --src ../functions.zip

# Проверка статуса
az functionapp show \
  --name func-orderapp-dev \
  --resource-group rg-orderapp-dev \
  --query "state"

Durable Functions: практический пример

Оркестрация обработки заказа с retry и компенсацией:

[Function("OrderProcessingOrchestrator")]
public static async Task<string> RunOrchestrator(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var orderId = context.GetInput<Guid>();

    // Step 1: Validate
    await context.CallActivityAsync("ValidateOrder", orderId);

    // Step 2: Reserve inventory
    await context.CallActivityAsync("ReserveInventory", orderId);

    // Step 3: Process payment (with retry policy)
    var retryOptions = new TaskOptions(
        new TaskRetryOptions(new RetryPolicy(3, TimeSpan.FromSeconds(5))));

    var paymentResult = await context.CallActivityAsync<PaymentResult>(
        "ProcessPayment", orderId, retryOptions);

    if (!paymentResult.Success)
    {
        // Compensation: release inventory
        await context.CallActivityAsync("ReleaseInventory", orderId);
        return "Payment failed";
    }

    // Step 4: Send notifications (parallel)
    var emailTask = context.CallActivityAsync("SendEmail", orderId);
    var smsTask = context.CallActivityAsync("SendSms", orderId);
    await Task.WhenAll(emailTask, smsTask);

    return "Order processed successfully";
}

Проверь себя

Для чего Azure Functions требуется Storage Account?

В Durable Functions, что произойдет, если ProcessPayment вернет неуспех?

Какой флаг при создании Function App включает Managed Identity?