Serverless Computing mit Azure Functions

Posted on | 1263 words | ~6 mins

Auf der kontinuierlichen Suche nach sinnvollen Optimierungs- und Standardisierungsmöglichkeiten für unsere Anwendungsentwicklung beschäftigen wir uns unter anderem mit dem Thema Serverless Computing. Während wir die Backends unserer Mission Critical Systeme derzeit mit Spring Boot entwickeln und in Azure Kubernetes Services deployen, tun sich mit Serverless Computing derzeit neue potenzielle Vereinfachungen für unsere Software Development Teams auf.

Wie Ihr aus unseren anderen Blogposts bereits erfahren habt, setzen wir hinsichtlich unserer Cloud-Strategie auf Microsoft Azure. In diesem Zusammenhang haben wir uns Azure Functions genauer angesehen und hinsichtlich sinnvoller Einsatzgebiete untersucht.

Was ist Serverless?

“Serverless” bezeichnet ein Entwicklungsmodell, bei dem die Infrastruktur für die Ausführung von Code nicht vom Entwickler verwaltet werden muss, sondern bei dem die Bereitstellung und Verwaltung zentral durch einen Cloud Anbieter erfolgt. Den Entwicklungsteams spart das Aufwand und Fachwissen zur Konfiguration, Bereitstellung und Skalierung der notwendigen Infrastruktur wie zum Beispiel beim Betrieb von dedizierten Servern oder eines Container-Orchestrators. Serverless-Apps werden hierbei bedarfsgerecht gestartet und skaliert. Entsprechend erfolgt die Abrechnung nur gegen die tatsächlichen Ausführungszeiten.

Azure bietet mit Functions, Container Apps und App Services gleich mehrere Produkte auf dem Feld des Serveless Computing an. Im Folgenden werden wir näher auf Azure Functions eingehen.

Was sind Azure Functions?

Azure Functions ist eine Serverless Computing Lösung von Azure, die eine ereignisgesteuerte Umgebung für die Ausführung von Code anbietet. Ereignisgesteuert bedeutet in diesem Fall, dass ein sogenannter Trigger auslösen muss, damit eine Function ausgeführt wird. Ein solcher Trigger kann zum Beispiel ein eingehender HTTP Request, eine neu im Blob Storage hinterlegte Datei, ein Event in einem Service Bus oder eine definierte Uhrzeit sein.

Azure Functions bietet aus unserer Sicht eine Reihe attraktiver Features:

  • Viele unterstütze Programmiersprachen: Javascript, Typescript, Powershell, Python, Java und C# werden nativ unterstützt. Mittels benutzerdefinierter Handler können auch andere Programmiersprachen wie zum Beispiel Go verwendet werden.
  • Lokale Entwicklung: Mit Azure Functions Core Tools können Functions lokal entwickelt, getestet und debugged werden.
  • Einfaches Lernen: Der Einstieg in die Functions-spezifischen Konzepte ist sehr intuitiv und einfach, da der Fokus auf der Logik und nicht auf der Infrastruktur liegt.
  • Skalierbarkeit: Die Skalierung der parallel ausgeführten Instanzen des Codes erfolgt automatisch. Sobald hohe Lasten auftreten, werden zusätzliche Instanzen des Functions-Host bereitgestellt. Bei abnehmender Last erfolgt ein automatisiertes Downsizing.
  • Kosten: Es werden nur die Ressourcen berechnet, die tatsächlich benötigt werden. Die Kosten sind also abhängig von der Anzahl der parallelen Ausführungen und der konsumierten Laufzeit. Detaillierte Informationen für die Kostenberechnung stellt Microsoft hier zur Verfügung.
  • Leichte Integration mit anderen Azure Diensten: Azure Functions können mit anderen Azure Diensten wie Azure Storage, Azure Cosmos DB, Azure Event Grid, Azure Service Bus, Azure Event Hubs, Azure Key Vault integriert werden, um diese bspw. für Trigger zu nutzen.

Die erwähnte Integration mit anderen Azure Diensten wird als Bindung bezeichnet. Diese Bindungen können als Eingabe, Ausgabe oder beides gestaltet sein, und die entsprechenden Daten werden den Functions als Parameter übergeben. Mit Bindungen und Triggern können hartkodierte Zugriffe vermieden werden. Zum Beispiel kann eine Function gestartet werden, wenn eine neue Datei in einen Blob Storage gelegt wird, anstatt in regelmäßigen Abständen auf neue Dateien zu prüfen.

Was sind Anwendungsfälle für Azure Functions?

Azure Functions ist insbesondere geeignet für kleine, abgeschlossene Aufgaben wie z.B. das Verarbeiten eines Anhangs in einer E-Mail oder das Herunterladen eines Dokuments aus einer Datenbank. So können Anwendungen schneller und effizienter entwickelt werden, da nicht der gesamte Code einer monolithischen Anwendung bearbeitet werden muss, wenn nur Änderungen an einzelnen abgeschlossenen Aufgaben vorgenommen werden sollen.

Beispielhafte Use Cases, die sich für Azure Functions eignen, sind:

  • Zeitgesteuerte Aufgaben
    • Jeden Sonntag um 10:00 Uhr unbenutzte Ressourcen bereinigen.
    • Alle 5 Minuten einen Smoke Test einer Webanwendung durchführen.
  • Reaktion auf Ereignisse
    • Wenn ein neues Bild in einem Blob Storage Container hochgeladen wird, soll dieses Bild verarbeitet und in einem anderen Container gespeichert werden.
    • Wenn eine neue Version eines Keyvault hochgeladen wird, soll diese Version zu einem anderen Dienst synchronisiert werden.

Im Folgenden zeigen wir die Umsetzungsdetails an einem konkreten Beispiel: Es handelt sich um eine Function, die mit einem HTTP Request aufgerufen wird, eine Datei aus einem Blob Storage liest und den Inhalt als Response zurückgibt.

HTTP Trigger

{
    "authLevel": "anonymous",
    "type": "httpTrigger",
    "direction": "in",
    "route": "samples/{name}",
    "name": "req"
}
  • authLevel definiert die Authentifizierungsstufe für diesen Endpunkt. Folgende Werte können verwendet werden:
    • anonymous - Kein API-Schlüssel ist notwendig.
    • function - Es ist ein funktionsspezifischer API-Schlüssel notwendig.
    • admin - Der Hauptschlüssel der Function ist hierfür notwendig.
  • type definiert den Typ des Triggers. Mehr Infos zu den Triggern findet Ihr hier.
  • direction definiert die Richtung der Bindung. In diesem Beispiel ist die Bindung vom Typ in, da die Function mit einem HTTP-Aufruf ausgelöst wird.
  • Für einen HTTP-Trigger kann eine route definiert werden. In diesem Beispiel wird ein Dateiname übergeben.
  • name definiert den Namen der Bindung. Dieser Name wird später in der Function verwendet.

Blob Storage Bindung

{
    "name": "myBlob",
    "type": "blobTrigger",
    "direction": "in",
    "path": "samples/{name}",
    "connection": "AzureWebJobsStorage"
}
  • name definiert den Namen der Bindung. Dieser Name wird später in der Function verwendet.
  • type definiert den Typ des Triggers.
  • direction definiert die Richtung der Bindung. In diesem Beispiel ist die Bindung vom Typ in, da die Datei aus dem Blob Storage gelesen werden muss.
  • path definiert den Pfad der Datei im Blob Storage. Bei dem Starten der Function wird die Datei mit dem Namen, deren Name aus dem Http Trigger kommt, aus dem Container samples vom Blob Storage gelesen.

HTTP Output Bindung

{
    "type": "http",
    "direction": "out",
    "name": "res"
}
  • type definiert den Typ des Triggers. Hier wird ein HTTP Response definiert.
  • direction definiert die Richtung der Bindung. Die Richtung der Bindung Typ out definiert, dass die Function einen HTTP-Response zurückgeben muss.
  • name definiert den Namen der Bindung. Dieser Name wird später in der Function verwendet.

Das finale JSON sieht wie folgt aus:

{
  "bindings": [
     {
        "authLevel": "anonymous",
        "type": "httpTrigger",
        "direction": "in",
        "route": "samples/{name}",
        "name": "req"
    },
    {
        "name": "myBlob",
        "type": "blobTrigger",
        "direction": "in",
        "path": "samples/{name}",
        "connection": "AzureWebJobsStorage"
    },
    {
        "type": "http",
        "direction": "out",
        "name": "res"
    }
  ]
}

Die Function sieht wie folgt aus:

module.exports = async function(context, req) {
    context.res = {
        // status defaults to 200 */
        body: context.bindings.myBlob
    };
};
  • Die Datei aus dem Blob Storage wird mit dem Namen myBlob in der context.bindings Variable zur Verfügung gestellt.
  • Mit context.res wird die HTTP-Response definiert. Der Body der Response wird mit dem Inhalt der Datei aus dem Blob Storage befüllt.

So könnte eine Function als einfacher HTTP-Server verwendet werden, der statisch ausliefert, was im Blob Storage gespeichert wird.

Fazit

Azure Functions sind aus unserer Sicht eine einfache Möglichkeit, um Code auszuführen, ohne sich um die hierfür notwendige Infrastruktur kümmern zu müssen. Dadurch können Entwicklerteams schnell Ergebnisse produzieren, und auf diese Weise die Time to Market erheblich beschleunigen. Durch die enge Integration von Azure Functions mit anderen Azure-Diensten und Drittanbietern kann zudem Zeit und Code für die Umsetzung von Pushs und Polls eingespart werden, was gleichzeitig zu einer wünschenswerten Komplexitätsreduktion des Codes führt.

Während Azure Functions auf der einen Seite Komplexität reduzieren kann, kommt Azure-seitig „unter der Haube“ eine Menge Komplexität hinzu. EntwicklerInnen, die zuvor klassische Anwendungen als Microservices programmiert haben, müssen sich umgewöhnen und die Paradigmen und Rahmenbedingungen von Serverless Computing im Allgemeinen sowie Azure Functions im Speziellen neu erlernen. Auch in unseren Teams Plattform und Architektur beschäftigen wir uns derzeit mit weiteren Details der Azure Functions, um die Einbindung der Features weiter zu optimieren, wie zum Beispiel den Auswirkungen der “On-Demand”-Bereitstellung von Functions auf die Antwortzeiten.

Für uns haben wir bereits einige lohnenswerte Einsatzfelder für Azure Functions identifiziert, wie zum Beispiel das Einsammeln, Aggregieren und Auswerten von Ergebnissen automatisierter Testfälle.

Wir werden in unserem Tech Blog von unseren Erfahrungen und neuen Einsatzgebieten berichten.