Azure Functions – Trigger und Bindings

14. Mai 2019

Azure Function Eine Function besteht aus einer statischen Methode, deren erster Parameter durch ein Trigger-Attribut gekennzeichnet ist. War’s das oder geht da noch mehr? Und wozu?

 

Tatsächlich sind die Möglichkeiten komplexer und flexibler – die Signatur einer Function wird durch drei Dinge beschrieben:

  • den Trigger
  • die Daten, die hineingehen
  • die Daten, die aus der Function herausgehen

Diese Informationen werden der Function in Form von Methodenparametern und Attributen mitgegeben.

Hinweis: Ich beschränke mich wieder auf Functions in C#-Projekten. In anderen Fällen kommt eine Beschreibung in einer JSON-Datei (function.json) zum Tragen, die bei C#-Projekten generiert wird.

Trigger existieren für Systeme, die in der Lage sind, eine Function zu starten. Beispiele sind HTTP- oder Timer-Trigger.

Triggers are what cause a function to run. A trigger defines how a function is invoked and a function must have exactly one trigger. (doc)

Bindings beschäftigen sich mit der Frage, welche Daten der Function mitgegeben werden und welche Daten die Function verlassen.

Binding to a function is a way of declaratively connecting another resource to the function; bindings may be connected as input bindings, output bindings, or both. (doc)

Ein Trigger ist dabei in der Regel auch ein Input-Binding. Folgendes Bild zeigt diese Zusammenhänge schematisch:

 

image

 

Wichtig ist noch: Nicht alle Bindings unterstützen die Verwendung als Trigger, Input- und Output-Binding. Für Queues wird z.B. Trigger und Output unterstützt, aber kein (von einem Trigger unabhängiges) Input-Binding; Table Storage wird hingegen für Input- und Output-Bindings unterstützt, aber es gibt keinen Trigger. Die Dokumentation listet die Details auf.

Die Dokumentation nutzt die Begriffe “Trigger” und “Binding” leider nicht immer konsistent. Man muss aus dem Kontext herauslesen, ob es um die Trigger-Funktionalität – das Starten einer Function in Abgrenzung zum Binding der Daten – geht, oder ob mit Trigger bzw. Binding die gesamte Funktionalität – Start und Datenaustausch – gemeint ist.

Hier ist ein Beispiel aus der Dokumentation, das einen Queue-Trigger und ein Output-Binding nutzt, um einen Queue-Eintrag in eine Storage Table zu schreiben. Die Function selbst gibt nur die Daten zurück:

   1: public static class QueueTriggerTableOutput

   2: {

   3:     [FunctionName("QueueTriggerTableOutput")]

   4:     [return: Table("outTable", Connection = "MY_TABLE_STORAGE_ACCT_APP_SETTING")]

   5:     public static Person Run(

   6:         [QueueTrigger("myqueue-items", Connection = "MY_STORAGE_ACCT_APP_SETTING")]JObject order,

   7:         ILogger log)

   8:     {

   9:         return new Person() {

  10:                 PartitionKey = "Orders",

  11:                 RowKey = Guid.NewGuid().ToString(),

  12:                 Name = order["Name"].ToString(),

  13:                 MobileNumber = order["MobileNumber"].ToString() };

  14:     }

  15: }

  16:

  17: public class Person

  18: {

  19:     [...]

  20: }

Das Beispiel ist an Trivialität kaum zu überbieten, zeigt aber auch die Mächtigkeit dieses Konzeptes. Handgeschriebener Code zum Schreiben in den Table Storage benötigt schon ein paar Zeilen. Und ob der Tabellen-Name und der Connection-String genauso nachvollziehbar und konfigurierbar wären, sei dahingestellt.

Daraus ergeben sich die Vorteile dieses Ansatzes:

  • Transparenz: Die Signatur der Function dokumentiert eindeutig ihre Abhängigkeiten zur Außenwelt
  • Verfügbare Binding Extensions: Weniger Code den man selbst schreiben muss.
  • Transparente und konsistente Konfiguration

 

Aber es ist noch eine Steigerung möglich: Binding Expressions.

Binding Expressions

Binding Expressions lassen sich leicht übersehen – auch weil in der Dokumentation nicht immer an passender Stelle darauf hingewiesen wird –, können aber insbesondere für die Konfiguration sehr wichtig sein.

Nehmen wir als Beispiel einen Timer-Trigger:

   1: [FunctionName("MyTimerTrigger")]

   2: public static void Run([TimerTrigger("0 */5 * * * *")]TimerInfo myTimer, ILogger log)

Der Ausdruck “0 */5 * * * *” gibt in CRON-Syntax an, wann der Timer laufen soll (hier alle 5 Minuten).

Das Problem: Diese Angabe möchte man nicht unbedingt im Code verankern, sondern konfigurierbar machen.

Hier kommen Binding Expressions ins Spiel: Durch Platzhalter mit Prozent-Zeichen lassen sich Werte durch Konfigurationsschlüssel ersetzen, also z.B.:

 

   1: [FunctionName("MyTimerTrigger")]

   2: public static void Run([TimerTrigger("%MySchedule%")]TimerInfo myTimer, ILogger log)

 

Lokal werden die Konfigurationseinträge der Datei local.settings.json entnommen, in Azure in der ganz normalen Konfiguration im Portal.
Konfigurationseinträge sind aber nicht die einzige Möglichkeit, die Binding Expressions bieten: Bindings stellen darüber auch Meta-Informationen zur Verfügung. Zum Beispiel liefert das Blob-Binding neben dem Blob selbst auch den Dateinamen, Queue-Bindings stellen unterschiedliche Metadaten über den Queue-Eintrag zur Verfügung. Binding Expressions können GUIDs oder einen Zeitstempel erzeugen. Oder sie greifen auf die Daten selbst zu, falls diese aus JSON bestehen.

Custom Bindings

Microsoft stellt für Azure Functions v2 17 Bindings schlüsselfertig zur Verfügung, wobei etwa die Hälfte als Trigger oder Input-Binding dienen kann (nur 3 können beides), und alle bis auf 3 können als Output-Binding verwendet werden.

image

Die Bindings decken dabei ein breites Spektrum ab: HTTP, Storage, Timer, Datenbanken, Event Grid, Microsoft Graph und andere. Damit sollten sich die üblichen Anforderungen abdecken lassen. Und falls ein Binding nicht angeboten wird, kann man natürlich auch jeder Zeit über eigenen Code auf andere Datenquellen zugreifen.

Trotzdem kann der Wunsch entstehen, ein spezielles System – die firmeneigene Datenbank, SharePoint, SAP, Twitter, … – als Binding zur Verfügung zu stellen, damit sich nicht jeder Entwickler erneut damit auseinandersetzen muss.

Für eigene Input- und Output-Bindings ist das möglich: Es gibt die passende Dokumentation und man findet konkrete Beispiele (1, 2, 3).

Allerdings bezieht sich das nur auf Binding-Funktionalität, nicht auf die Funktion als Trigger.

Custom Trigger

Bei Triggern sieht die Situation leider nicht ganz so gut aus: Die vorhandenen Trigger sind recht flexibel, aber sie sind wie sie sind. Weder lassen sie sich erweitern, noch gibt es eine Möglichkeit, eigene Trigger bereitzustellen.

In einem Blog-Beitrag (der aktuell nicht mehr verfügbar ist, hier ist die letzte Version), heißt es dazu:

For custom triggers, we plan to offer integration with Azure Event Grid. Stay tuned for more details on this.

Ein Event Grid Trigger steht zur Verfügung, verschiebt das Problem im Grunde aber nur. Die einfachere Alternative ist vermutlich ein Timer-Trigger, der das fremde System regelmäßig abfragt – falls dieses nicht selbst in der Lage ist, sich zu melden.

 

Mit etwas Recherche findet man dann aber dann aber doch ein Beispiel, das auf dem Code von Azure Functions aufsetzt. Da dies aber der Aussage von Microsoft widerspricht und der Azure Functions Code zudem weitgehend undokumentiert ist, muss jeder selbst entscheiden, ob das eine Lösung für ihn darstellt.

Fazit

Sich mit Triggern und Bindings etwas tiefer auseinanderzusetzen, ist für das Verständnis und die effiziente Nutzung von Azure Functions nicht verkehrt. Die für mich wichtigste Erkenntnis steckte in den Binding Expressions und der Möglichkeit, Informationen aus den Binding-Attributen in die Konfiguration auszulagern.

Custom Bindings und Custom Trigger können ein Thema werden, wenn man Azure Functions im größeren Maße einsetzt und eigene Systeme anbinden muss.