Met de komst van .NET 5 en nu met de LTS versie .NET 6 is er een nieuwe stap gezet voor Azure functions, Azure Functions isolated process. Dit ging echter niet zonder de nodige kritiek en struikelblokken omdat de architectuur van Azure Functions niet gemaakt was om out of the box support te bieden voor .NET 5 en later .NET 6. In dit artikel ga ik in op de veranderingen binnen de architectuur die nodig waren om support voor deze nieuwe versie van .NET te bieden. Daarnaast laat ik zien wat de grootste verschillen zijn tussen in-process (oude architectuur) en isolated process en hoe je met isolated process aan de slag kunt. Tenslotte kijken we nog naar Middleware, wat we al kennen van ASP.NET, maar nu ook beschikbaar is binnen Azure Functions doormiddel van de switch naar isolated process.
Historie
In de oude architectuur van Azure Functions werd er gebruik gemaak van in-process execution, wat betekend dat de code die je schrijft als class library gedraaid wordt binnen hetzelfde process als de host. In het begin leek dit veel voordelen te bieden omdat men zo toegang had tot de host-api en verschillende bindings.
Het grote nadeel wat meteen duidelijk werd met de komst van .NET 5 is dat een koppeling tussen de host en de function code ook betekend dat de .NET versie van onze function code en de host (.NET Core 3 en Azure Function Runtime v3) gelijk moet zijn. Toen .NET 5 aangekondigd werd was er dan ook in de eerste versies nog geen support binnen Azure Functions. Dit omdat support van .NET 5 betekende dat de host ook op .NET 5 moest draaien en dus een rewrite nodig had. Hierdoor ontstond binnen de .NET community veel omdat zelfs AWS had eerder support voor .NET 5 binnen Lambda Functions aangezien bij Lambda functions vanaf het begin de keuze was gemaakt om de host en de function code los te trekken van elkaar.
Door de ophef en omdat een rewrite van de host veel tijd in beslag zou nemen, is er uiteindelijk gekozen om ook binnen Azure Functions de host en function code los te trekken. En zo werd Azure Functions isolated process geboren. Met deze nieuwe en losse architectuur is het mogelijk om de host op .NET Core 3.x te laten draaien en je function code bijvoorbeeld op .NET 5 of .NET 6.
Inmiddels is .NET 6 gereleased maar voelen we nog steeds een aantal nadelen van de oude architectuur. Zo zijn bijvoorbeeld Durable Functions nog steeds niet beschikbaar binnen het isolated process. Deze worden verwacht in .NET 7, wat betekent dat men voor nu bij in-process moet blijven als je gebruik maakt van durable functions. Inmiddels is het wel mogelijk om .NET 6 te gebruiken binnen de in-process architectuur. Er is geen ondersteuning voor durable functions in .NET 5 omdat dit ook geen LTS versie is. Voorbeeld 1.
Verschillen
Azure Functions isolated process werd in het begin van van 2021 GA. Zoals je eerder al las is er bijvoorbeeld nog steeds geen support voor Durable Functions binnen Isolate Process. Buiten dit grote gemis zijn er ook andere verschillen waar je zeker rekening mee moet houden als je wilt upgraden of een nieuwe function wilt maken gebaseerd op dit nieuwe model. Ik som ze hieronder even kort voor je op.
Startup vs Program
Binnen in-process functions wordt er gebruik gemaakt van een startup class. Deze is niet nodig om de function te runnen, maar als je zaken zoals dependency injection wilt gebruiken dien je deze toe te voegen. Omdat de function code gedraaid wordt als een class library binnen de host, wijkt deze class af van wat men van andere .NET applicaties gewend is. Ga je hiermee voor het eerst aan de slag, dan kan dit heel verwarrend zijn.
Voorbeeld 2.
Bij een function met isolated process is dit vervangen door de Program.cs class die standaard wordt toegevoegd wanneer je een nieuw project aanmaakt. Als we deze class bekijken (voorbeeld 3) dan zien we dat dit heel erg veel lijkt op wat we gewend zijn van ASP.NET. Het is daarom ook meteen veel duidelijker hoe men met de HostBuilder aan de slag kan.
Dependency Injection
Zoals we net zagen was het bij een in-process function al niet heel intuïtief om een startup class toe te voegen, maar het voelde i.c.m. Dependency injection ook wat hacky. Niet als of het gemaakt was om hier goede support voor te bieden. Begrijp mij niet verkeerd, het werkt prima en deed wat het moest doen.
Willen we nu gebruik maken van dependency injection binnen een isolated process function dan kunnen we dit op dezelfde manier doen zoals we dit binnen ASP.NET doen. Dit voelt meteen een stuk beter aan. Voorbeeld 4.
ILogger
Bij een in-process function wordt de ILogger geinject binnen de function. Deze kun je dan meteen gebruiken om errors of informatie te loggen. Dit is binnen een function met isolated process niet zo. Binnen isolated process wordt een FunctionContext geinject. Deze context bevat veel informatie over de huidige execution van de function. Binnen deze context is ook de logger beschikbaar. Deze kan je verkrijgen door de GetLogger methode aan te roepen en hier een categorie aan mee te geven. In de meeste gevallen zal dit de naam van de function zijn waarin de logger gebruikt wordt. Voorbeeld 5.
Output bindings
Als we kijken naar output bindings heeft hier ook een shift plaatsgevonden, wat je code naar mijn mening een stuk duidelijker maakt. Als je in het verleden met in-process functions een bericht op een queue wilde plaatsen doormiddel van een output binding, dan werd dit gedaan door een IAsyncCollector te injecten in de Run methode. Voorbeeld 6.
Met isolated process functions is dit verplaats naar een attribute. Je function werkt dus nu altijd met return values zoals een array, string of serializable class. Wil je dus één item op de queue zetten, dan return je een string. Wil je er meerdere op de queue plaatsten, dan return je bijvoorbeeld een list. Voorbeeld 7. Dit maakt de code een stuk duidelijker tegenover het oude model.
Ook meerdere output bindings binnen één function worden ondersteund binnen een isolated process function. Stel we hebben als voorbeeld een string die we op een queue willen plaatsen en we willen een http response geven. Om hier mee aan de slag te gaan moeten we een wrapper class maken waar we de verschillende output types inzetten. Voorbeeld 8.
Zoals je ziet hebben we nu onze QueueOutput attribute verplaatst naar de wrapper class om aan te geven dat MyQueueItem naar een queue gestuurd moet worden. Bij een http output moeten we ervoor zorgen dat het return type HttpResponseData is. Op de class zelf hoeven we verder geen attributes te plaatsen. We kunnen nu deze class gebruiken binnen onze function. Wel moeten we het return type van onze function aanpassen naar onze nieuwe class MyResponseBindings. Voorbeeld 9.
Cold-starts
Als je gebruik maakt van een Azure Function op een consumption plan, krijg je te maken met cold starts. Dit komt door het feit dat je function in een sleep-mode gaat als er geen traffic is geweest binnen een bepaalde periode. Dit kan voor problemen zorgen afhankelijk van hoe de Azure Function gebruikt word.
Bij in-process functions worden de host en de function code in hetzelfde process gedraaid, omdat de host de function code laadt als class library. Als we over performance praten is dit een plus, aangezien er niet eerst een ander process gestart hoeft te worden om de function code in te draaien.
Bij Isolated functions is dit helaas verslechtert. Dit komt vooral door het feit dat je function code nu echt een los process is wat opgestart moet worden als de function getriggered wordt. Als je hier veel hinder door ondervindt zijn er een aantal zaken die je kunt doen om je function starttijd te verbeteren. Als je echt geen last van cold-starts wilt hebben kun je het beste je function plan omzetten naar een premium plan. Dit kost natuurlijk wel iets extra’s maar hierdoor kun je instellen hoeveel instanties er blijven draaien en dus niet gaan slapen. Een andere improvement die ook door Microsoft zelf wordt aangeraden is om je function te draaien op Linux. Door je function op Linux te draaien heb je minder overhead van het OS. Dit versnelt dus de opstarttijd van je function.
Middleware
Een nieuwe feature die functions met isolated model bieden is middleware. Middleware kennen we al van ASP.NET, maar is nu ook beschikbaar binnen Azure Functions. Binnen isolated functions wordt eerst internal function middleware aangeroepen m.b.t. output binding, function execution en vervolgens onze custom middleware. Net als bij ASP.NET werkt dit als een pipeline en kun je dus meerdere custom middlewares achter elkaar chainen. Voorbeeld 10.
We kunnen middleware gebruiken om te loggen, te authentiseren of om validaties uit te voeren. In het volgende voorbeeld gaan we een simpele middleware opzetten die de function naam logt zodra we een request ontvangen.
Als we dit binnen de function zelf doen zou dit er uit kunnen zien als in voorbeeld 11. Willen we deze log regel nu verplaatsen naar een middleware, dan moeten we deze eerst aanmaken. Hiervoor maken we een class die het interface IFunctionsWorkerMiddleware implementeert en daarmee ook de invoke methode die bij een inkomend request iets voor ons kan doen. Voorbeeld 12.
Omdat we hier weer een FunctionContext binnen krijgen betekent dit dat we onze logger ook weer op dezelfde manier kunnen ophalen en deze kunnen laten loggen.Vervolgens moeten we nog next aanroepen en de context meegeven om ervoor de zorgen dat de pipeline doorgaat. Voorbeeld 13.
Als laatste stap voegen we nog onze middleware toe tijdens het configureren van de HostBuilder. Voorbeeld 14.
Als we onze function nu runnen, zien we dat voor elke request die binnenkomt via de middleware een log regel wordt aangemaakt. We kunnen dus nu als laatste de log regel uit onze function verwijderen.
Voor- en nadelen
Met isolated process wordt de volgende stap gezet in het verhaal van Azure functions. Als developers willen we vaak snel aan de slag met nieuwe technieken zonder een afweging te maken of de stap naar een nieuwe techniek wel echt nodig is. Het is dus altijd goed om even te kijken wat nu de grootste voor- en nadelen zijn en of deze relevant zijn voor ons.
Voordelen
- Volledige controle over function startup process;
- Dependency Injection zoals we dit gewend zijn binnen .NET;
- Middleware support;
- Geen dependency conflicts omdat zoals de naam het zegt het worker process isolated is van de host.
Nadelen
- Nog geen support voor Durable Functions;
- Nog niet alle bindings zijn beschikbaar;
- Meer last van cold-starts;
- Veel breaking changes en bestaande code werkt hierdoor niet zonder een rewrite.
Samenvatting
Met Azure Functions isolated process is een mooie stap gemaakt naar een architectuur die klaar is voor de toekomst. Ga je beginnen met een nieuwe function en hebben de nadelen geen effect op jouw process? Dan zou ik zeker voor isolated process kiezen. Heb je al een bestaande function? Dan is het op het moment nog niet nodig om deze om te gaan schrijven, tenzij er een sterke behoefte is voor middleware support. in-process wordt nog ondersteund tot 8 november 2024.
Florian Schaal
Florian Schaal is een Senior Cloud Developer met ruim 10 jaar ervaring in het vak. Zijn specialisaties liggen voornamelijk op Azure, .NET en Docker, maar hij heeft ook ervaring met verschillende front-end frameworks en mobile development met Flutter. Florian heeft een passie voor software development en verdiept zich graag in nieuwe technieken.
Momenteel is Florian werkzaam bij Cloud Republic, waar hij klanten en teams helpt software te ontwikkelen die toekomstbestendig en onderhoudbaar is. Dit doet hij vanuit verschillende rollen, van Team-Lead tot Senior Cloud Developer. Naast zijn werk bij klanten host Florian ook de Dev Talks podcast met collega Dibran Mulder. (devtalks.nl).