Azure Durable Functions, deel 2: de eerste module

in het vorige deel hebben we gekeken naar de noodzaak voor een robuust framework die ons in staat stelt om ons te richten op de bedrijfsprocessen en niet op de randzaken. Nu kunnen we natuurlijk nooit helemaal om die randzaken heen, maar we kunnen wel proberen die zo veel mogelijk te vermijden. Het scenario wat ik schetste is die van de dotNed website en de stappen die we nemen om iemand in te schrijven voor een van onze Meetups.

In deze post gaan we een begin maken met de bouw van dit systeem.

Ik ga er even vanuit dat we al iets hebben wat de input van de gebruiker verzamelt. Dat kan een website zijn, een mobiele app, een UWP applicatie, wat dan ook, We hebben iets dat de data heeft en die moet nu verwerkt worden. De eerste stap die we nemen is het valideren van de input. Als het systeem input krijgt die geldig is, kan de rest van het systeem verder gaan. Mocht dit niet geldig zijn, dan negeren we de inschrijving en stoppen we de verwerking.

Uiteraard is het zo dat we in de echte wereld eerst controleren of er bijvoorbeeld wel een geldig email adres is gegeven voordat we dit doorgeven aan het back-end proces. Op die manier kunnen we de gebruiker op voorhand al melden dat er een probleem is. Maar we gaan er even vanuit dat we ook willen kijken of die email adres toebehoort aan iemand van het bestuur van Stichting dotNed zelf. Die gaan we niet inschrijven: die zijn standaard al aanwezig (nogmaals: fictief voorbeeld).

Serverless computing

We hebben dus iets wat de data verzamelt en we moeten nu het backend proces in werking stellen.

Het ligt voor de hand om Azure Functions hiervoor te gebruiken. Azure Functions zijn op zich staande modules die functionaliteit bevatten die we los van andere modules kunnen draaien. We praten hier dan ook vaak over “serverless computing”. Nu is dat onzin: de code draait wel degelijk op een server. In het geval van Azure Functions is dat zelfs een Windows of Linux Virtual Machine die op een fysieke Windows of Linux machine draait. Ergens moet er immers een CPU zijn die onze code draait.

Wat er bedoelt wordt met serverless computing is dat we ons niet druk hoeven te maken over die server. Die is er wel en doet z’n ding. We zien hem niet, we horen er niets van en we hoeven er niets aan te configureren. Het is alsof deze server er niet is. Vandaar de naam.

Ok. We kunnen dus een losse functie maken. Daarvoor heb je nog geen Azure subscription nodig; althans niet tijdens het ontwikkelen. Deze hele reeks zal ik gebruik maken van mijn lokale omgeving en de emulator, om pas in de laatste module naar Azure zelf te gaan.

Dus: zorg dat je Visual Studio geïnstalleerd heb samen met de Azure Workload. Start de Azure Cloud Storage Emulator alvast op. Start Visual Studio en dan kunnen we beginnen.

Het opstarten van de workflow

We zouden kunnen beginnen met de eerste functie te schrijven als Azure Function. Maar dat zou betekenen dat we alle plumbing er om heen ook moeten gaan schrijven. En dat gaan we niet doen.

Ons proces kunnen we zien als een workfow. Er zijn een aantal stappen die genomen moeten worden. Deze stappen hebben onderlinge afhankelijkheden. Vroeger zouden we daarvoor WF (Workflow Foundation) gebruiken maar die is niet echt meer van deze tijd. Dus we gaan het anders doen.

Wat we gaan doen is het maken van een zogenaamde orchestrator. Een orchestrator is een functie die er voor zorgt dat alle functies die onze functionaliteit bevatten in de juiste volgorde en onder de juiste condities aangeroepen gaan worden.

Nu zul je misschien denken: “Maar dat is toch juist weer plumbing, wat we niet zouden doen?” en dan heb je nog gelijk ook. Maar: we moeten wel iets aan plumbing doen, maar we houden het beperkt. Je zult zien dat deze code enorm leesbaar, simpel en klein is.

De orchestrator, die dus alles aanstuurt, is een Azure Function. Deze function echter moet wel gestart worden. Er moet een trigger zijn die hem afvuurt. Nu is het zo dat orchestrator functions niet zo maar van buiten af aangeroepen kunnen worden. De standaard security staat dat niet toe (we gaan het later over security hebben). Dus we moeten iets hebben wat deze orchestrator aanstuurt. En dat is de rol van een recht-toe, recht-aan azure function.

Die function wordt op zijn beurt weer aangeroepen door middel van een REST API.

Samengevat: een website (of app, of uwp applicatie, of wat dan ook) doet een REST call naar een starter functie. Deze starter functie start op zijn beurt de orchestrator, welke dan weer alle stappen doorloopt. Klinkt moeilijk of omslachtig? Valt mee!

De eerste functie

Ok. Tijd voor code. We hebben Visual Studio draaien. We gaan een nieuw project maken. Als je de Azure Workload geinstalleerd heb, kunnen we daarvoor kiezen:

Create New Project in Visual Studio

Als je een nieuw project aanmaakt, kun je zoeken naar de template Azure Functions. Die kiezen we. Klik Next. Geef het project een naam (de mijne heet MeetingRegistration in de gelijknamige solution). Klik Create.

Create new Function in Visual Studio

In dit scherm maken we een keuze voor een aantal dingen.

  1. Welke versie
  2. Welke trigger
  3. Welk Storage Account
  4. Welke authenticatie

De versie is V2. V1 is outdated en V3 is in de maak maar nog niet beschikbaar op het moment dat ik dit schrijf. Ik zal in de laatste post laten zien wat de verschillen zijn, maar voor nu kiezen we V2.

De trigger is wat de functie start. We hebben de keuze uit een aantal triggers; in dit geval wil ik dat we via een REST API de functie kunnen starten. We kiezen dus voor een HTTP Trigger

Storage account is niet nodig: we draaien lokaal voorlopig (je hebt de Microsoft Azure Storage Emulator al draaien, toch?). Je kunt kiezen voor een echt Storage Account maar je hebt het niet nodig

Authenticatie die ik op functie level. We hebben drie keuzes:

  1. Function. Dat betekent dat iedere Azure Functie een eigen secret key krijgt die je mee moet geven als je de functie wilt aanroepen. Je kunt later in de Azure Portal per functie meerdere keys aanmaken, revoken en recyclen. Je kunt dus goed bepalen wie wat mag doen.
  2. Anonymous. Spreekt voor zich: geen authtenticatie. Iedereen mag alle functies in deze function app aanroepen.
  3. Admin. Je krijg 1 key voor alle functions in de function app. Als je bijvoorbeeld alle admin-functionaliteit in een aparte function app maak, kun je op deze manier de rechten toekennen aan de hele app.

We kiezen dus voor function-level. Klik Create

Visual Studio gaat aan de gang en maakt onze eerste functie voor ons, met de enorm fantasieloze naam Function1.

Hoewel deze reeks artikelen gaan over Durable Functions en dit absoluut niet een Durable Function is, wil ik toch even door de structuur heen gaan van deze code. Immers, Durable Functions zijn Azure Functions dus een goed begrip van hoe deze dingen werken helpt.

public static class Function1
{
    [FunctionName("Function1")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        string name = req.Query["name"];

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        name = name ?? data?.name;

        return name != null
            ? (ActionResult)new OkObjectResult($"Hello, {name}")
            : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
    }
}

Laten we er eens doorheen gaan.

Als eerste hebben we de class Function1. Functions zijn gewoon methods in een C# class, dus er moet ergens een class zijn. We doen er niets mee, maar hij moet er wel zijn. Het is een static class en heet Function1. Tja, wat moet ik daar nog meer over zeggen.

Op regel 4 staat de signature van onze functie. De naam hier is niet relevant. Hij heet ‘Run’ maar we hadden hem ook Fietsbel kunnen noemen. Azure roept de functies aan op basis van de naam die gegeven is met het attribuut “FunctionName” op regel 3. In ons geval dus Function1 (ja, we gaan zo alles renamen, maar nu laten we het even zo).

Onze Run method is public, static en async (en geeft dus een Task terug. Om precies te zijn geeft het een Task<IActionResult> terug met info voor de aanroepende client.

We hebben een paar parameters. Het is belangrijk om te weten dat het framework onder Azure Functions gebruik maakt van Dependency Injection. Dus alles wat we nodig hebben kan worden geinjecteerd en hoeven we niet zelf aan te maken. Een mooi voorbeeld hiervan zie je op regel 6: we krijgen een logger van het type ILogger mee, waarmee we kunnen, uh, loggen.

Daarvoor echter op regel 5 staat een andere parameter van het type HttpRequest met een aantal attributen.

Dit zie je terug bij alle Azure Functions, dus later ook bij de Durable Functions. Er moet een trigger aangegeven worden welke het framework verteld hoe deze function in het leven geroepen wordt. In ons geval is dat een HttpTrigger. Deze heeft een paar parameters: welke authorization level we gebruiken (Function dus), welke REST Verbs we accepteren (get en post) en of we een bepaalde route gebruiken (nee dus).

De body van de code is niet zo relevant. Wat er gebeurt is dat er gekeken wordt of er queryparameters zijn (om precies te zijn met de naam ‘Name’ ) of dat er wellicht in het geval van een Post een body is met daarin een Name attribuut. Als dat niet zo is, melden we dat aan de gebruiker. Als dat wel zo is, zeggen we vriendelijk hallo.

De eerste run

Laten we eens kijken wat er gebeurt als we dit gaan runnen.

Druk op F5.

Nadat Visual Studio alle packages gedownload heeft, zal de runtime opstarten. Dit is dus iets wat normaal in Azure hoort te gebeuren maar dat kan dus ook lokaal.

Je krijgt info te zien over wat er allemaal gebeurt, en uiteindelijk krijg je te zien hoe we deze function moeten gaan aanroepen:

Runtime Azure Functions

Zoals je ziet, verteld de runtime ons keurig netjes dat we een browser kunnen openen en die laten wijzen naar http://localhost:7071/api/Function1. En oh ja, hij accepteert GET en POST. Laten we de GET eens testen. Dat kan gewoon in de browser, maar we moeten wel even die Name parameter meegeven.

Eerste running function

Ok. Ik geef toe: het is niet echt indrukwekkend. Maar dat komt nog wel. We hebben in ieder geval een werkend systeem!

In de command window (die waarin die URL vernoemd werd) kun je op CTRL-C drukken om de runtime af te sluiten. Ik raad je aan om dat ook steeds te doen: er kunnen dingen blijven hangen die we anders handmatig moeten op ruimen.

Laten we de functie even iets mooier maken en wat moderniseren. We nemen de volgende stappen:

  1. Hernoemen van de class (en de code-file)
  2. Hernoemen van de C# Method
  3. Hernoemen van de Azure Function
  4. Aanpassen van de in- en output
  5. Weggooien van de “hello-world” code.
  6. Verwijderen van de POST
  7. Aanpassen van de route zodat we parameters mee kunnen geven

Mijn versie ziet er dan zo uit:

public static class PublicFunctions
{
    [FunctionName("StartRegistration")]
    public static async Task<HttpResponseMessage> StartRegistration(
        [HttpTrigger(
            AuthorizationLevel.Function, 
            "get", 
            Route = "dotNed/register/{name}/{email}/{wantsTweet}")] 
        HttpRequestMessage req,
        string name, 
        string email,
        bool wantsTweet,
        ILogger log)
    {
        log.LogWarning($"Received: {name} {email} {wantsTweet}.");

        return req.CreateResponse(System.Net.HttpStatusCode.OK);

    }
}

Ok. Wat heb ik gedaan?

Ik heb de class PublicFunctions genaamd. De reden: we gaan meerdere functies maken. En zoals ik al zei: Orchestration Functions kun je niet van buiten af aanroepen, dat moet een andere Azure Function doen. En dit is er een van, dus die moet publiek zijn.

Ik heb de function name aangepast: deze is nu StartRegistration, net als de method name van de C# method zelf. Dat is wel zo duidelijk, denk ik.

De return type is veranderd in Task<HttpResponseMessage> wat in lijn ligt met de input trigger parameter van het type HttpRequestMessage. Deze is iets flexibeler en biedt mogelijkheden die we later nodig gaan hebben.

Ik heb de POST optie weggehaald en een Route toegevoegd. Deze Route is nu “dotNed/register/{name}/{email}/{wantsTweet}”

Normaal gesproken zouden we dit via een POST doen en dan in de body van de request meegeven als JSON object, maar aangezien we op deze manier dit keurig van de browser kunnen intypen zonder gebruik te maken van tools als Postman bijvoorbeeld, heb ik hier voor gekozen. Als jij liever met POST werkt en dit via een JSON body mee wilt geven, is dat natuurlijk ook prima.

In ons geval gebruik ik dus de route en de waardes worden geïnjecteerd in de method call: de parameters string name, string email en bool wantsTweet (of de gebruiker wil of er getweet wordt). Ik weet het: we missen details als welke meeting en zo, maar vergeet dat maar even hier.

We loggen de informatie maar dit keer heb ik log. Warning gebruikt. De reden is dat Warnings in een andere kleur weergegeven worden in de output zodat het opvalt tussen alle debug output van de runtime. Als laatste geven we een 202 OK terug.

Laten we dit eens gaan runnen.

Output runtime

Je ziet dat de URL die we moeten gebruiken nu meer informatie geeft. Ik kopieer dit en plak dit in een browser window, vervang de {name}, {email} en {wantsTweet} door ‘echte’ waardes en bekijk het resultaat.

Browser window

Het resultaat hier is niet echt spannend. Zelfs onze vriendelijke Hello is nu verdwenen. Het output window van de runtime daarentegen is wat spannender geworden:

Output van de nieuwe functie

We zien in het midden ongeveer keurig in het geel (omdat we een Warning geven in plaats van Information) dat onze Function de data binnen heeft gekregen en het begrepen heeft.

Volgende stappen

We hebben nu een werkende Azure Function. Ok. Hij doet niet zo veel, maar dit wordt zo de trigger voor onze orchestration.

En dat is precies het onderwerp van de volgende post!

Tot dan!

Gepubliceerd door Dennis Vroegop

Passionate Azure Cloud Solutions Architect. I am a enthusiastic guitar player (though not as good at it as I'd like to be) and in my daytime job I teach software developers to be better at their job. Married to my wonderful wife Diana with whom I try to raise our daughter Emma.

One thought on “Azure Durable Functions, deel 2: de eerste module

Geef een reactie

Vul je gegevens in of klik op een icoon om in te loggen.

WordPress.com logo

Je reageert onder je WordPress.com account. Log uit /  Bijwerken )

Google photo

Je reageert onder je Google account. Log uit /  Bijwerken )

Twitter-afbeelding

Je reageert onder je Twitter account. Log uit /  Bijwerken )

Facebook foto

Je reageert onder je Facebook account. Log uit /  Bijwerken )

Verbinden met %s

Deze site gebruikt Akismet om spam te bestrijden. Ontdek hoe de data van je reactie verwerkt wordt.

%d bloggers liken dit: