• 9 min read

Azure Functions: Die Tour

Es hat unser Team sehr gefreut, dass wir vor Kurzem eine Vorschauversion des neuen Azure Functions-Diensts auf der //build vorstellen konnten. Es gibt bereits einige Blogbeiträge zu diesem Dienst (z. B. die Einführung in Azure Functions), in diesem Beitrag möchten wir aber ein wenig hinter die Kulissen blicken und darüber sprechen, wie das Projekt begann und wie wir dahin kamen, wo wir heute sind.

Dieser Blogbeitrag wurde von Mathew Charles, Principal Software Engineer bei Microsoft, verfasst.​

Es hat unser Team sehr gefreut, dass wir vor Kurzem eine Vorschauversion des neuen Azure Functions-Diensts auf der //build vorstellen konnten. Es gibt bereits einige Blogbeiträge zu diesem Dienst (z. B. die Einführung in Azure Functions), in diesem Beitrag möchten wir aber ein wenig hinter die Kulissen blicken und darüber sprechen, wie das Projekt begann und wie wir dahin kamen, wo wir heute sind. Wir behandeln hier die Functions-Runtime, die dynamische Computeebene („serverlos“) sowie das Functions-Portal. Außerdem sehen wir uns ganz allgemein an, wie sich alle diese Komponenten entwickelt haben und zu einem einheitlichen Produkt geworden sind. Für das Team war dies eine spannende Erfahrung, die gerade erst begonnen hat.

Die Entwicklung dieses Projekts ist ein tolles Beispiel für die Erkennung von Synergien zwischen bestehenden Plattformteilen und deren Zusammenführung in einem neuen Produktangebot. Azure App Service enthielt bereits viele der Bausteine, die wir für die Umsetzung unserer Vision von Azure Functions benötigten. Durch die Nutzung dieser Ressourcen und die Einbringung neuer Innovationen und Funktionen konnten wir das Projekt relativ schnell in die Tat umsetzen.

WebJobs SDK

Bei der Rede von Chris auf der //build zur Einführung von Azure Functions erläuterte er bereits, wie Azure Functions auf dem Azure WebJobs SDK aufbaut. Das WebJobs SDK gibt es bereits seit einigen Jahren, und wir haben viele Kunden, die es sehr gerne verwenden, um Aufträge zur Back-End-Verarbeitung zu erstellen, die durch die unterschiedlichsten Ereignisquellen ausgelöst werden. Das WebJobs SDK besitzt ein einfaches deklaratives Programmiermodell, mit dem das Schreiben selbst komplexester Auftragsfunktionen mit minimalem Codeaufwand möglich ist. Hier ist ein Beispiel:

public static void ProcessOrder(
    [QueueTrigger(“orders”)] Order order,
    [Blob(“processed/{Id}”)] out string receipt,
TraceWriter log)
{
log.Verbose(string.Format(“Processing Order {0}”, order.Id));

    // Geschäftslogik

    receipt = “<ein Wert>”;
}

Wenn diese Funktion vom WebJobs SDK-JobHost in einer unveränderten .NET-Konsolenanwendung gehostet wird, wird sie automatisch durch jede neue Warteschlangennachricht ausgelöst, die der Azure-Warteschlange „orders“ hinzugefügt wird. Die Nutzlast der Warteschlange wird in eine Instanz des Auftrags-POCO deserialisiert. Außerdem wird die Funktion automatisch mithilfe der Id-Eigenschaft der eingehenden Nachricht als Teil des Blobpfads an ein Ausgabeblob gebunden. Durch dieses Programmiermodell müssen Sie sich für Ihre Auftragsfunktion lediglich auf die Geschäftslogik konzentrieren und sich keine Gedanken über die Speichervorgänge machen. Großartig.

Als Hostingmodell für solche Funktionen mit dem WebJobs SDK werden diese als Azure WebJobs-Instanz bereitgestellt. Dies funktioniert hervorragend bietet viel Flexibilität. Darüber hinaus ist dies eine sehr beliebte Funktion von Azure App Service.

Functions-Runtime

Ab etwa Mitte letzten Jahres diskutierten wir darüber, welche Vorteile dieses einfache Programmiermodell für andere Sprachen hätte – eine Frage, die auch von unseren Kunden gestellt wurde. Es gibt nicht nur .NET-C#-Programmierer, aber viele möchten trotzdem diese WebJobs SDK-Muster nutzen. Daher begannen wir mit einigen Prototypen für ein Modell, das es uns erlaubt, die vorhandene und bewährte .NET WebJobs SDK-Runtime zu nutzen, indem wir ihr ein neues JSON-Beschreibungsmodell für die Metadaten zugrunde legen. Als Ergebnis können Sie dieselbe Funktion wie oben nun auch in Node.js (und anderen Programmiersprachen) schreiben:

module.exports = function (context, order) {
context.log(‘Processing order’, order.id);

    // business logic

context.bindings.receipt = “<some value>”;
context.done();
}

Wie Sie sehen, ist diese Funktion strukturell mit der C#-Funktion oben identisch. Das liegt daran, dass sie derselben Runtimeimplementierung zugeordnet ist. Deklarative Codeattribute sind aber nur eine Möglichkeit zur Angabe von Metadaten. Uns fiel auch auf, dass wir dieselben Informationen in einer einfachen JSON-Beschreibungsdatei erfassen konnten. Dies ist die entsprechende Metadatendatei, in der die Bindungen für diese Funktion (d. h. alle Informationen aus den deklarativen Attributen im C#-Beispiel) beschrieben werden:

{
  “bindings”: [
    {
      “type”: “queueTrigger”,
      “name”: “order”,
      “direction”: “in”,
      “queueName”: “orders”
    },
    {
      “type”: “blob”,
      “name”: “receipt”,
      “direction”: “out”,
      “path”: “processed/{id}”
    }
  ]
}

Dem liegt die Idee zugrunde, dass wir diese Metadaten nutzen können, um einen speicherinternen Adapter zwischen unterschiedlichen Programmiersprachen und der .NET WebJobs SDK-Runtime zu generieren. Im Prinzip generieren wir die C#-Funktion, die Sie oben sehen. Der Inhalt der Methode dieser Funktion delegiert einfach die eigentliche Benutzerfunktion (also Ihre Node.js-Funktion). Eine Azure-Funktion kann dann einfach nur aus einer Metadatendatei („function.json“), in der die Funktionsbindungen beschrieben werden, sowie einer Sammlung mit mindestens einer Skriptdatei, in der die Funktion implementiert ist, bestehen. Dies ist das gleiche Beispiel wie oben mit derselben Metadatendatei, bei dem die Funktion als eine Windows-BAT-Datei erstellt wurde:

SET /p order=<%order%
echo Processing order ‘%order%’
echo ‘<ein Wert>’ > %receipt%

Dieselbe Metadatendatei kann für die Beschreibung einer Funktion in einer der sieben unterstützten Programmiersprachen verwendet werden. Natürlich hat jede Sprache ihre Eigenheiten, sodass sich einige besser für bestimmte Aufgaben eignen als andere. Der wichtigste Aspekt hierbei ist jedoch, dass wir dieselbe Runtime für das Auslösen/Binden in allen Programmiersprachen verwenden können und damit jede Sprache die Zuordnung zu diesem Modell auf eine andere Art erzielen kann. BAT-Dateien sind etwas limitiert, aber mithilfe von Umgebungsvariablen und Dateistreams können sie Eingaben empfangen und Ausgaben schreiben. Die Functions-Runtime ordnet die zugrunde liegenden Azure Storage-Artefakte dabei automatisch zu.

Da Azure Functions auf dem WebJobs-Kern-SDK beruht, müssen wir keine unterschiedlichen Versionen des WebJobs SDK für die einzelnen Programmiersprachen schreiben und verwalten – technisch ein enormer Vorteil. Wir verfügen über eine einzelne Kernruntime, die unsere gesamte Logik für das Binden/Auslösen behandelt. Wenn wir also Änderungen vornehmen, profitieren davon alle Functions-Kunden und natürlich unsere WebJobs SDK-Kunden. Dies bedeutet darüber hinaus auch, dass sämtliche Erweiterungen für das Auslösen und Binden, die für das Kern-SDK geschrieben werden, auch in Functions genutzt werden können. Wir werden auch weiterhin sehr viel Arbeit in das Kern-WebJobs SDK und die Erweiterungen investieren – sowohl für unsere Bestandskunden als auch für Azure Functions.

Webhook-Unterstützung

Ein weiterer wichtiger Bereich, an dem wir gearbeitet haben, sind Webhooks. Dass Funktionen durch Azure Storage-Ereignisse ausgelöst werden können, ist großartig, einige WebJobs-Kunden fragten jedoch auch nach der Möglichkeit, ihre Auftragsfunktionen über Webhookanforderungen auslösen zu können. Wir haben damit bereits im letzten Jahr experimentiert und eine Webhookerweiterung geschrieben, die sehr gut funktioniert hat. Sie hatte jedoch den Nachteil, dass WebJobs auf der Kudu SCM-Site ausgeführt werden, sodass grundlegende Anmeldeinformationen für die Authentifizierung erforderlich sind, um Anforderungen übermitteln zu können. Das macht diese Lösung für die meisten Webhook-Integrationsszenarien unbrauchbar, da Sie natürlich eine URL mit einem einfachen Authentifizierungscode ausgeben möchten, die lediglich Verbindungen mit diesem Endpunkt erlaubt.

Aus diesem Grund haben wir die Functions-Runtime als Websiteerweiterung gepackt, die im Stamm einer Web-App ausgeführt wird. Das bedeutet, dass sie sich NICHT hinter dem SCM-Endpunkt befindet und damit die erforderlichen Muster zur Authentifizierung ermöglicht werden. So können wir eine einfache Gruppe von Authentifizierungsendpunkten für Webhookfunktionen offenlegen. Wir haben auch die ASP.NET WebHooks-Bibliothek integriert, sodass wir eine Vielzahl der Webhookanbieter nutzen können, die von der Bibliothek unterstützt werden. Dadurch erhalten wir eine herausragende Unterstützung für Anbieter wie GitHub, Slack, Dropbox, Instagram usw.

Zu diesem Zeitpunkt verfügten wir also über eine flexible Functions-Runtime, die das Modell zum Auslösen/Binden des WebJobs SDK für sieben Programmiersprachen (Node.js, C#, F#, Bash, BAT, Python, PHP) unterstützt und über einen HTTP-Header verfügt, der eine Vielzahl von Webhook-Integrationsszenarien unterstützt.

Dynamisches Computing

Parallel zur beschriebenen Arbeit an der Runtime diskutierten wir auch über serverloses Computing und die Möglichkeiten in diesem Bereich. Es wurde schnell deutlich, dass es sehr viele Überschneidungen mit unserer Arbeit an WebJobs gab. Wir entwickelten eine flexible Functions-Runtime für mehrere Sprachen, die Benutzercode mit hoher Skalierung in einer Sandboxumgebung ausführen konnte. Für das traditionelle WebJobs-Modell mussten die Benutzer aber den Web-App-Host, auf dem diese WebJobs ausgeführt werden, erstellen und verwalten. Was, wenn es uns möglich wäre, diesen Teil ebenfalls zu abstrahieren, sodass die Benutzer nur noch die Funktionen selbst schreiben müssten, während wir alle Aspekte der Bereitstellung und Skalierung übernehmen? Das wäre dann im Grunde genommen ein WebJobs SDK as a Service. Heureka!

Wir haben ein Team zusammengestellt, das diesen Plan vom „dynamischen Computing“ prüfen sollte. An diesem Punkt wuchs unser Team sehr schnell von einigen wenigen Personen zu einem größeren Team – unsere Scrum-Meetings vergrößerten sich jeden Tag um 2–3 Personen. Unsere Schicht für dynamisches Computing skaliert Funktionen automatisch horizontal hoch, wenn die Nutzlast zunimmt, und herunter, wenn sie abnimmt. Dadurch müssen sich die Endbenutzer darüber überhaupt keine Gedanken mehr machen, denn ihnen wird nur noch die Computezeit in Rechnung gestellt, die sie auch tatsächlich verbrauchen. Der Bereich des dynamischen Computings ist innerhalb des Projekt sehr umfangreich und umfasst auch andere Aspekte des Diensts wie Überwachung und Diagnose, Telemetrie usw. Zu diesem Bereich wird es in Zukunft noch eigene Blogbeiträge geben.

Functions-Portal

Als Nächstes konzentrierten wir uns auf eine Portalumgebung, um das Erstellen und Verwalten dieser Funktionen noch einfacher zu machen. Beim herkömmlichen WebJobs SDK-Modell kompilieren Sie eine .NET-Konsolenanwendung (JobHost), die alle Ihre vorkompilierten Auftragsfunktionen enthält, und stellen sie bereit. Bei Azure Functions ist das Bereitstellungsmodell sehr viel einfacher. Die Functions-Runtime wurde direkt mit einem sehr einfachen Layout für das Dateisystem entworfen. Dieses umfasst auch eine übersichtliche Portaloberfläche, die über die Kudu-APIs mit diesen Dateien arbeitet. Wir hätten uns für einen einfachen Portal-Editor entscheiden können, mit dem Sie diese Dateien erstellen und bearbeiten könnten, um sie dann in den Funktionscontainer (die Web-App, in der die Funktionen ausgeführt werden) zu pushen. Das einfache Dateisystemmodell ermöglicht auch die Bereitstellung von Azure Functions über ARM-Vorlagen. Tatsächlich ist dies bereits heute möglich, aber noch nicht gut dokumentiert.

Das Team konnte stattdessen sehr schnell ein Portal einrichten und ausführen und schnell damit beginnen, das fast fertige Produkt zu testen. Mit dem Portal kam das erste Mal das Gefühl auf, dass sich die Dinge ineinander fügten. Wir konnten das Team erweitern, das unser Produkt getestet hat, und damit eine ganze Reihe von Diskussionen zur Benutzerfreundlichkeit und einige Verbesserungen anstoßen und gleichzeitig eine große Zahl von Fehlern beheben.  Zu Beginn der Arbeit am Portal hatten wir wie bei der Functions-Runtime ein oder zwei Personen, die daran gearbeitet haben, als sich aber die ersten Ergebnisse einstellten und sowohl der Projektumfang als auch die Pläne wuchsen, holten wir immer mehr Helfer an Bord. Die Scrum-Meetings wurden jetzt noch größer.

Vorlagen

Das einfache Dateisystemmodell für Functions ermöglichte es uns auch, das herausragende Vorlagenmodell zu entwickeln, das Sie heute im Functions-Portal sehen können. Wir begannen mit simplen Metadaten-/Skriptvorlagen für häufige Szenarien in unterschiedlichen Programmiersprachen: „QueueTrigger – Node“, „GitHub WebHook C#“ usw. Die Idee dahinter sind einfache „Rezepte“ oder Einstiegspunkte für Ihre Funktionen, die sofort und ohne Anpassungen ausgeführt werden können und die Sie dann an Ihre Anforderungen anpassen und ggf. erweitern können. Wir hoffen, in der Zukunft auch der Community das Erstellen solcher Vorlagen erlauben zu können, um ein richtiges Ökosystem zu erschaffen.

Erweiterbarkeit

Ein weiterer für uns sehr wichtiger Bereich bis zur Bekanntgabe von Azure Functions auf der //build war ein neuer Satz von WebJobs SDK-Erweiterungen, die wir in Functions verfügbar machen wollten. Wir haben das WebJobs SDK-Erweiterungsmodell letzten Herbst veröffentlicht. Damit können im Programmiermodell neue Quellen für Auslöser und Bindungen genutzt werden. Unser Team hatte bereits im WebJobs SDK-Erweiterungsrepository einige nützliche Erweiterungen für die Community bereitgestellt (z. B. TimerTrigger, FileTrigger, SendGrid-Bindung usw.). Außerdem haben einige Communitymitglieder mit der Erstellung eigener Erweiterungen begonnen. Da Functions auf dem SDK basiert, können alle diese Erweiterungen auch in Azure Functions verfügbar gemacht werden. Es gab sehr viele Erweiterungen, die wir gerne schreiben wollten, für die wir aber nicht genügend Zeit hatten. Durch unser größeres Team standen uns nun ausreichend Ressourcen für die Fertigstellung einiger Erweiterungen zur Verfügung. Wir konnten in den letzten Monaten die folgenden zusätzlichen Erweiterungen hinzufügen und auch in Functions verfügbar machen: EventHub, DocumentDb, NotificationHub, MobileApps und ApiHub. Dies ist jedoch nur ein Anfang – es sind noch viele weitere Erweiterungen geplant, und wir hoffen, dass auch von der Community noch viele Erweiterungen erstellt werden. Darüber hinaus arbeiten wir an einem einfachen Modell, das es Drittanbietern möglich machen soll, ihre Erweiterungen in Functions bereitzustellen. Sie sollten also auf dem Laufenden bleiben.

Als zusätzlichen Vorteil haben wir schon sehr früh entschieden, dass wir unsere Arbeit als Open Source bereitstellen wollten – wie schon beim Kern-WebJobs SDK und den WebJobs SDK-Erweiterungsrepositorys. Daher haben wir das WebJobs SDK-Skript erstellt, das die Functions-Runtime enthält. Ebenso ist auch das Functions-Portal Open Source: AzureFunctionsPortal.

Abschließend möchte ich noch einmal sagen, dass hier nur eine sehr oberflächliche Betrachtung der einzelnen Aspekte des Projekts (Functions-Runtime, Functions-Portal und dynamisches Computing) und ihrer Interaktion möglich ist. Wir werden in zukünftigen Beiträgen mehr auf Details dieser einzelnen Bereiche eingehen.