MS Certificering: AZ-204, overnieuw beginnen, deel 02

Ingediend door Dirk Hornstra op 21-dec-2021 15:05

Sinds 15 december (of misschien iets eerder) heeft Azure bij AZ-204 (link) 12 nieuwe learning-blocks neergezet. Zuur, omdat ik net de voorgaande learning-blocks afgerond had. Dus opnieuw begonnen. Vorige keer Azure App Service web apps doorlopen: link, vandaag wordt het tijd voor het 2e learning-block: Implement Azure Functions, link.

Module 1: Explore Azure Functions, link.

We gaan eerst Azure Functions vergelijken met Azure Logic Apps. Omdat Logic Apps een soort "flow" is, worden hier Durable Functions tegenover gezet (omdat je daar ook een "flow" hebt).

Durable Functions zijn code-first, hebben een aantal (maar niet heel veel) ingebouwde bindings, je kunt je code schrijven voor eigen bindings, elke activiteit is een Azure Function (je moet dus code schrijven voor activiteiten), je kunt monitoren via Azure Application Insights, beheer doen via REST API, Visual Studio en je kunt het zowel lokaal laten draaien als in de cloud.

Logic Apps zijn design-first, hebben heel veel connectors beschikbaar, Enterprise Integration Pack voor B2B scenario's en ook hier kun je custom connectors bouwen. Ook heb je een grote collectie van acties al beschikbaar (hoef je dus niet zelf meer te maken). Monitoring doe je via de portal en Azure Monitoring logs. Beheer via portal, REST API, PowerShell en Visual Studio. Je kunt het alleen in de cloud laten draaien.

Hiernaast heb je ook nog WebJobs. Ook die zijn code-first. Azure Functions is gebouwd op de WebJobs SDK, dus deelt hier veel zaken mee. Zaken die Functions wel ondersteunen (en WebJobs niet): serverless app model met automatisch schalen, ontwikkelen en testen in de browser, betaal voor het echte gebruik, integratie met Logic Apps.
Het lijstje met triggers komt deels overeen, Timer, Azure Storage queues and blobs, Azure Service Bus queues and topics, Azure Cosmos DB, Azure Event Hubs.

WebJobs heeft ook nog File system beschikbaar, dat heeft Functions niet. Functions heeft echter nog wel HTTP/WebHook (GitHub, Slack) en Azure Event Grid.

Als je Azure Functions wilt gaan draaien, dan heb je de keuze uit 3 type service-plans (en 2 hosting-options). Hierbij het overzicht:

Plan Benefits
Consumption plan This is the default hosting plan. It scales automatically and you only pay for compute resources when your functions are running. Instances of the Functions host are dynamically added and removed based on the number of incoming events.
Functions Premium plan Automatically scales based on demand using pre-warmed workers which run applications with no delay after being idle, runs on more powerful instances, and connects to virtual networks.
App service plan Run your functions within an App Service plan at regular App Service plan rates. Best for long-running scenarios where Durable Functions can't be used.
Hosting option Details
ASE App Service Environment (ASE) is an App Service feature that provides a fully isolated and dedicated environment for securely running App Service apps at high scale.
Kubernetes Kubernetes provides a fully isolated and dedicated environment running on top of the Kubernetes platform. For more information visit Azure Functions on Kubernetes with KEDA.

Als je Azure Functions aanmaakt, dan heb je daar ook altijd een Storage Account voor nodig, omdat daar zaken (zoals triggers en logging) opgeslagen worden.

Het schalen gebeurt op basis van het aantal events. Elke instantie in het consumption plan heeft 1.5 GB geheugen en 1 CPU. Een proces, de scale controller, houdt in de gaten of er uit of ingeschaald moet worden. Is er een Queue storage trigger, dan wordt gekeken hoe groot de queue is en wanneer het oudste bericht aangemaakt is.

Een functie app kan uitschalen tot maximaal 200 instanties (consumption plan, bij Premium plan 100). Sommige instanties verwerken soms meerdere berichten gelijktijdig, dus het aantal berichten dat verwerkt wordt kan hoger zijn. Voor HTTP triggers wordt maximaal 1 instantie per seconde aangemaakt. Is het geen HTTP trigger, dan wordt maximaal 1 per 30 seconden aangemaakt. Heb je een Premium plan, dan gaat het sneller.

Wil je het maximum beperken (omdat je weet dat het proces wat actie moet ondernemen het niet bij kan houden, dan kun je via de parameter functionAppScaleLimit zelf instellen met een waarde tussen de 1 en de maximum waarde. Zet je deze op 0, dan is er geen limiet.

Module 2: Develop Azure Functions, link.

Een Function bestaat uit 2 onderdelen, jouw code die uitgevoerd moet worden en een function.json. Hierin staat de trigger, de bindings en andere configuratie die nodig is voor je app.


{
    "disabled":false,
    "bindings":[
        // ... bindings here
        {
            "type": "bindingType",
            "direction": "in",
            "name": "myParamName",
            // ... more depending on binding
        }
    ]
}

Je geeft dus het type aan (als je een queueTrigger hebt, dan zal dat daar staan), richting (dus in of out) en de naam die je in je code kunt gebruiken. Later wordt nog gezegd dat er sommige bindings zijn die "inout" hebben.

Je hebt nog een aantal bestanden/instellingen. Zo heb je de host.json, waar de runtime-configuratie in staat. En hier staat verder toegelicht hoe dit per programmeertaal ingericht wordt:


Informatie over hoe je lokaal kunt ontwikkelen kun je hier nalezen: link.

C# en Java plaatst in function.json niet het datatype, omdat dit binnen de code zelf gedaan wordt (alle variabelen hebben een type: string, bool, int, etc.). JavaScript/PowerShell/Python/TypeScript werkt wel met function.json.

We krijgen een voorbeeld van een function.json waarbij er data vanuit een Queue binnen komt en het doorgestuurd wordt naar een Tabel.


{
  "bindings": [
    {
      "type": "queueTrigger",
      "direction": "in",
      "name": "order",
      "queueName": "myqueue-items",
      "connection": "MY_STORAGE_ACCT_APP_SETTING"
    },
    {
      "type": "table",
      "direction": "out",
      "name": "$return",
      "tableName": "outTable",
      "connection": "MY_TABLE_STORAGE_ACCT_APP_SETTING"
    }
  ]
}

We krijgen een voorbeeld in C#:


#r "Newtonsoft.Json"

using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;

// From an incoming queue message that is a JSON object, add fields and write to Table storage
// The method return value creates a new row in Table Storage
public static Person Run(JObject order, ILogger log)
{
    return new Person() {
            PartitionKey = "Orders",
            RowKey = Guid.NewGuid().ToString(),  
            Name = order["Name"].ToString(),
            MobileNumber = order["MobileNumber"].ToString() };  
}

public class Person
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Name { get; set; }
    public string MobileNumber { get; set; }
}

// maar ook nog op een alternatieve manier

public static class QueueTriggerTableOutput
{
    [FunctionName("QueueTriggerTableOutput")]
    [return: Table("outTable", Connection = "MY_TABLE_STORAGE_ACCT_APP_SETTING")]
    public static Person Run(
        [QueueTrigger("myqueue-items", Connection = "MY_STORAGE_ACCT_APP_SETTING")]JObject order,
        ILogger log)
    {
        return new Person() {
                PartitionKey = "Orders",
                RowKey = Guid.NewGuid().ToString(),
                Name = order["Name"].ToString(),
                MobileNumber = order["MobileNumber"].ToString() };
    }
}

public class Person
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Name { get; set; }
    public string MobileNumber { get; set; }
}

Voor meer informatie kun je hier kijken:


Om zaken dynamisch te houden, kun je een connectie-string niet in de function.json plaatsen. Deze wordt altijd uitgelezen uit de "omgevings-variabelen". Die stel je in via Application Settings (link) of via een lokale settings file (als je op jouw eigen pc aan het ontwikkelen bent): link. Sommige connectie-strings worden niet via een secret maar via een Identity ingesteld. Durable Functions ondersteunen geen Identity. Als iets Identity-gebaseerd is, dan werkt het met een managed identity: link. De identity die aan het systeem gekoppeld zit wordt gebruikt, maar kan ook met credential en clientID aangepast worden.

Ook hier geldt (ik heb het volgens mij eerder al genoemd) dat als je een object hebt, bijvoorbeeld connection je met object connection__serviceUri de serviceUri-eigenschap van connection instelt.

Hierna volgt weer een oefening die je met een free account gaat doorlopen. Hier maak je een Azure Function aan en dat ontwikkel je in Visual Studio Code. Hier heb je een aantal tools voor nodig:

Je maakt een project aan, start dit, zet het op Azure en voert het daar uit. Niet heel spannend.

Module 3: Implement Durable Functions, link.

Als je Azure Functions "met state" wilt gebruiken kies je voor Durable Functions. Je hebt hier orchestrator functions die de boel aan sturen. Je hebt entity functions om de "state" bij te houden.

De volgende talen worden ondersteund: C#, JavaScript (alleen versie 2.x Azure Functions runtime en 1.7.0 of hoger van Durable Functions extension), Python (2.3.1 of hoger van Durable Functions extension), F# (alleen versie 1.x van Azure Functions runtime), PowerShell (preview, 3.x Azure Functions runtime en PowerShell 7, 2.2.2 of hoger van Durable Functions extension).

De verschillende "application patterns" die je tegen kunt komen:

Function chaining
Sequentieel worden alle functies doorlopen, output proces A is input proces B, output proces B is input proces C.


[FunctionName("Chaining")]
public static async Task<object> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    try
    {
        var x = await context.CallActivityAsync<object>("F1", null);
        var y = await context.CallActivityAsync<object>("F2", x);
        var z = await context.CallActivityAsync<object>("F3", y);
        return  await context.CallActivityAsync<object>("F4", z);
    }
    catch (Exception)
    {
        // Error handling or compensation goes here.
    }
}

Fan-out/fan-in

Meerdere processen lopen parallel, wachten tot ze allemaal afgerond zijn.


[FunctionName("FanOutFanIn")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var parallelTasks = new List<Task<int>>();

    // Get a list of N work items to process in parallel.
    object[] workBatch = await context.CallActivityAsync<object[]>("F1", null);
    for (int i = 0; i < workBatch.Length; i++)
    {
        Task<int> task = context.CallActivityAsync<int>("F2", workBatch[i]);
        parallelTasks.Add(task);
    }

    await Task.WhenAll(parallelTasks);

    // Aggregate all N outputs and send the result to F3.
    int sum = parallelTasks.Sum(t => t.Result);
    await context.CallActivityAsync("F3", sum);
}

Async HTTP API's

Er is een externe taak die lang duurt. Die krijgt een HTTP endpoint om het resultaat naar toe te sturen. Een ander proces pollt dat endpoint om te controleren of de taak nog loopt. Dit patroon zit in Durable Functions "ingebakken".

> curl -X POST https://myfunc.azurewebsites.net/orchestrators/DoWork -H "Content-Length: 0" -i
HTTP/1.1 202 Accepted
Content-Type: application/json
Location: https://myfunc.azurewebsites.net/runtime/webhooks/durabletask/b79baf67f717453ca9e86c5da21e03ec

{"id":"b79baf67f717453ca9e86c5da21e03ec", ...}

> curl https://myfunc.azurewebsites.net/runtime/webhooks/durabletask/b79baf67f717453ca9e86c5da21e03ec -i
HTTP/1.1 202 Accepted
Content-Type: application/json
Location: https://myfunc.azurewebsites.net/runtime/webhooks/durabletask/b79baf67f717453ca9e86c5da21e03ec

{"runtimeStatus":"Running","lastUpdatedTime":"2019-03-16T21:20:47Z", ...}

> curl https://myfunc.azurewebsites.net/runtime/webhooks/durabletask/b79baf67f717453ca9e86c5da21e03ec -i
HTTP/1.1 200 OK
Content-Length: 175
Content-Type: application/json

{"runtimeStatus":"Completed","lastUpdatedTime":"2019-03-16T21:20:57Z", ...}

---

public static class HttpStart
{
    [FunctionName("HttpStart")]
    public static async Task<HttpResponseMessage> Run(
        [HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
        [DurableClient] IDurableClient starter,
        string functionName,
        ILogger log)
    {
        // Function input comes from the request content.
        object eventData = await req.Content.ReadAsAsync<object>();
        string instanceId = await starter.StartNewAsync(functionName, eventData);

        log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

        return starter.CreateCheckStatusResponse(req, instanceId);
    }
}

Monitor

Dit is een flexibel, herhalend proces in een workflow. Met een timer trigger controleer je regelmatig de huidige stand van zaken (monitoren).


[FunctionName("MonitorJobStatus")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    int jobId = context.GetInput<int>();
    int pollingInterval = GetPollingInterval();
    DateTime expiryTime = GetExpiryTime();

    while (context.CurrentUtcDateTime < expiryTime)
    {
        var jobStatus = await context.CallActivityAsync<string>("GetJobStatus", jobId);
        if (jobStatus == "Completed")
        {
            // Perform an action when a condition is met.
            await context.CallActivityAsync("SendAlert", machineId);
            break;
        }

        // Orchestration sleeps until this time.
        var nextCheck = context.CurrentUtcDateTime.AddSeconds(pollingInterval);
        await context.CreateTimer(nextCheck, CancellationToken.None);
    }

    // Perform more work here, or let the orchestration end.
}

Human Interaction

Soms moet iemand handmatig goedkeuring geven (of iets afkeuren). Hier kun je timers aan hangen (als iemand niet binnen 48 uur iets goed- of afgekeurd heeft, dan wordt het verzoek automatisch afgekeurd.


[FunctionName("ApprovalWorkflow")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    await context.CallActivityAsync("RequestApproval", null);
    using (var timeoutCts = new CancellationTokenSource())
    {
        DateTime dueTime = context.CurrentUtcDateTime.AddHours(72);
        Task durableTimeout = context.CreateTimer(dueTime, timeoutCts.Token);

        Task<bool> approvalEvent = context.WaitForExternalEvent<bool>("ApprovalEvent");
        if (approvalEvent == await Task.WhenAny(approvalEvent, durableTimeout))
        {
            timeoutCts.Cancel();
            await context.CallActivityAsync("ProcessApproval", approvalEvent.Result);
        }
        else
        {
            await context.CallActivityAsync("Escalate", null);
        }
    }
}

Informatie over de verschillen tussen versie 1.x en 2.x: link.

Er zijn op dit moment 4 "Durable Function types", orchestrator, activity, entity en client.

Orchestrator-functions geven aan "hoe" en in welke volgorde de code uitgevoerd moet worden. Dit moet deterministisch zijn (voer je het 20x uit, dan moet het 20x op dezelfde manier gedaan worden). Meer informatie daarover is hier na te lezen: link.

Een activity function is een code waarin iets gedaan wordt. Deze worden vaak gebruikt om CPU intensieve taken uit te voeren, netwerk calls uit te voeren. En soms ook data teruggeven aan de orchestrator functie.

Een activity trigger definieert een activity function. In .NET krijg je de DurableActivityContext binnen als parameter.

Entity functions worden gebruikt om de huidige staat uit te lezen en kleine onderdelen daarvan bij te werken. Deze functies werken met een speciale trigger, de entity trigger. Deze functies kunnen aangeroepen worden door client en orchestrator functions. Er zijn geen eisen hoe de code opgezet moet worden.

Entities kunnen bereikt worden via een unieke ID, het entity ID. Dat is een aantal strings welke een entity uniek maakt.
Acties op entities moeten (dus) met deze Entity ID uitgevoerd worden en de Operation name (string met de naam van de actie die uitgevoerd moet worden).

Elke functie die niet een orchestrator function is kan een client function zijn. Doordat een client function altijd gebruik maakt van de durable client output binding maakt iets een client function.

Als je een durable function wil testen, kun je het best gebruik maken van een manual trigger function.

Een task hub is een logische container waarin zaken worden opgeslagen. Functies kunnen met elkaar communiceren als ze in dezelfde task hub zitten. Als meerdere durable functions een storage account delen, dan moet elke function app in de configuratie een eigen task hub naam hebben. Een storage account kan meerdere task hubs hebben.

Waar bestaat een task hub uit? Uit één of meer control queues, één work-item queue, één historie-tabel, één instantie-tabel, één opslagcontainer met één of meer lease blobs, een storage container met grote "message payloads", indien van toepassing.

De naam van een hub bevat alleen alphanumerieke karakters, start met een letter, minimaal 3, maximaal 45 karakters. En deze wordt in het host.json bestand ingesteld:


{
  "version": "2.0",
  "extensions": {
    "durableTask": {
      "hubName": "MyTaskHub"
    }
  }
}

Elke orchestration instantie heeft een ID, de instance ID. Is een gegenereerde GUID waarde, maar kan ook een string zijn die je zelf instelt. Moet wel uniek zijn binnen een task hub. GUID gebruiken is de aanbevolen actie.

Orchestration functions zijn betrouwbaar, ze volgen het event sourcing design pattern. Via een append-only store wordt geregistreerd welke acties doorlopen worden. Via await of yield gaat de focus weer naar de Durable Task Framework dispatcher die zaken kan opstarten.

Features en patterns van orchestration functions:

Pattern/Feature Description
Sub-orchestrations Orchestrator functions can call activity functions, but also other orchestrator functions. For example, you can build a larger orchestration out of a library of orchestrator functions. Or, you can run multiple instances of an orchestrator function in parallel.
Durable timers Orchestrations can schedule durable timers to implement delays or to set up timeout handling on async actions. Use durable timers in orchestrator functions instead of Thread.Sleep and Task.Delay (C#) or setTimeout() and setInterval() (JavaScript).
External events Orchestrator functions can wait for external events to update an orchestration instance. This Durable Functions feature often is useful for handling a human interaction or other external callbacks.
Error handling Orchestrator functions can use the error-handling features of the programming language. Existing patterns like try/catch are supported in orchestration code.
Critical sections Orchestration instances are single-threaded so it isn't necessary to worry about race conditions within an orchestration. However, race conditions are possible when orchestrations interact with external systems. To mitigate race conditions when interacting with external systems, orchestrator functions can define critical sections using a LockAsync method in .NET.
Calling HTTP endpoints Orchestrator functions aren't permitted to do I/O. The typical workaround for this limitation is to wrap any code that needs to do I/O in an activity function. Orchestrations that interact with external systems frequently use activity functions to make HTTP calls and return the result to the orchestration.
Passing multiple parameters It isn't possible to pass multiple parameters to an activity function directly. The recommendation is to pass in an array of objects or to use ValueTuples objects in .NET.


Je kunt Timers gebruiken. En dat moet je ook doen, geen Thread.Sleep. Je kunt dit in C# doen met CreateTimer() en in Javascript met createTimer().

Hier zie je een voorbeeld hoe 10 dagen lang elke dag een factuur wordt verstuurd:


[FunctionName("BillingIssuer")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    for (int i = 0; i < 10; i++)
    {
        DateTime deadline = context.CurrentUtcDateTime.Add(TimeSpan.FromDays(1));
        await context.CreateTimer(deadline, CancellationToken.None);
        await context.CallActivityAsync("SendBillingEvent");
    }
}

En die kun je dus ook gebruiken om een time-out te genereren, als iets maximaal 1 minuut mag duren en na die minuut wil je door, dan gebruik je dit:

[FunctionName("TryGetQuote")]
public static async Task<bool> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    TimeSpan timeout = TimeSpan.FromSeconds(30);
    DateTime deadline = context.CurrentUtcDateTime.Add(timeout);

    using (var cts = new CancellationTokenSource())
    {
        Task activityTask = context.CallActivityAsync("GetQuote");
        Task timeoutTask = context.CreateTimer(deadline, cts.Token);

        Task winner = await Task.WhenAny(activityTask, timeoutTask);
        if (winner == activityTask)
        {
            // success case
            cts.Cancel();
            return true;
        }
        else
        {
            // timeout case
            return false;
        }
    }
}

Gebruik in .NET een CancellationTokenSource om een timer te annuleren of roep cancel() in Javascript aan op de TimerTask als je code niet langer gaat wachten.
Het Durable Task Framework zet een orchestration status niet op "completed" als niet alle taken of uitgevoerd zijn of geannuleerd.

Orchestration functions hebben de mogelijkheid om te wachten en luisteren naar externe gebeurtenissen. In .NET WaitForExternalEvent, Javascript: waitForExternalEvent en Python: wait_for_external_event

Voorbeeld in C#:


// wachten op extern event
[FunctionName("BudgetApproval")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    bool approved = await context.WaitForExternalEvent<bool>("Approval");
    if (approved)
    {
        // approval granted - do the approved action
    }
    else
    {
        // approval denied - send a notification
    }
}

// zelf een event genereren
[FunctionName("ApprovalQueueProcessor")]
public static async Task Run(
    [QueueTrigger("approval-queue")] string instanceId,
    [DurableClient] IDurableOrchestrationClient client)
{
    await client.RaiseEventAsync(instanceId, "Approval", true);
}

In C# kun je met RaiseEventAsync, Javascript: raiseEvent de gebeurtenis opstarten waar het externe proces op staat te wachten. eventName en eventData worden als parameters mee genomen. De data moet JSON serialiseerbaar zijn.