Azure Functions

Microsoft Graph nutzt Webhooks, um über Änderungen zu informieren. Also muss ich irgendwo eine Webseite bereitstellen, die solche Requests annimmt und weiter verarbeitet. Das kann PHP auf einem Apache sein oder auch Azure Functions, um die es auf dieser Seite geht. Über einen externen Trigger, z.B. einen HTTP-Zugriff, startet ein Powershell-Skript, welches verschiedene Aktionen ausführt.

Voraussetzung: Subscription

Azure Functions laufen natürlich auf Azure und damit ist nicht Office 365 oder Microsoft 365 gemeint, sondern das Hosting von unterschiedlichen Diensten in der Azure-Cloud. Sie brauchen dazu eine Azure Subscription. Allerdings gehört zu jeder Azure-Umgebung auch ein AzureAD, über welches die Zugriffsrechte verwaltet werden. Wer also schon einen Office 365 Tenant hat, hat auch ein AzureAD aber damit noch lange keine Subscription.

Eine Subscription können Sie aber schnell anlegen und es gibt auch eine "Trial"-Version, die später in eine "Pay per Use"-Version wechselt.

Azure Function anlegen

Ich melde mich dazu beim Azure Portal unter "https://portal.azure.com" an und wechsle in meine Subscription. Wer keine Azure Subscription hat, kann eine "kostenfreie" Eval-Version starten, die nach Verbrauch abrechnet. Aktuell sind Azure Functions bis zu einem gewissen Volumen kostenfrei:


Quelle: https://azure.microsoft.com/en-us/pricing/details/functions/

Den Storageaccount dürfen Sie nicht löschen. Insofern ist "kostenfrei" nur bedingt richtig

Storage account guidance
Every function app requires a storage account to operate. If that account is deleted your function app won't run. 
Quelle https://docs.microsoft.com/en-us/azure/azure-functions/storage-considerations#storage-account-guidance

Hier die Schritte der Anlage. Ich starte das Azure Portal auf "https://portal.azure.com" und wechsle in meine Subscription in Ressources

Dort lege ich eine neue Ressource an und suche einfach nach "Function":

Der Assistent führt mich dann durch den Anlageprozess und legt auch gleich weitere erforderliche Ressourcen an.

Einstellung Bild

Basics

Grundlegende Einstellungen zur Azure Function mit den Resource Group, dem Stack samt Version und der wichtigen Angabe des Datacenters (Datenschutz).

Hosting

Jede Azure Function braucht einen Storage Account und eine Plattform. Als kostenfreie Version muss ich beim Plan bei "Consumption (Serverless)" bleiben.

Networking

Hier kann ich nichts einstellen, wenn ich den Plan:Consumption nutze.

Monitoring

Application Insights lasse ich mal aktiviert, um später Aussagen über die Nutzung machen zu können. Bis zu einem gewissen Maß ist die Nutzung kostenfrei und wenn ich über die Limits komme, dann wird einfach nichts mehr protokolliert.

You can try out Application Insights integration with Azure Functions for free featuring a daily limit to how much data is processed for free. If you enable Applications Insights during development, you might hit this limit during testing. Azure provides portal and email notifications when you're approaching your daily limit. If you miss those alerts and hit the limit, new logs won't appear in Application Insights queries. 
Quelle: Application Insights pricing and limits https://docs.microsoft.com/en-us/azure/azure-functions/functions-monitoring

Tags

Ich bin ein Freund einer "eingebauten Dokumentation" und die Tags sind eine gute Idee ihre Ressourcen zu Personen, Projekten, Kostenstellen o.ä. zuzuordnen. Sie sollten sich aber Firmenweit auf eine "Namensgebung" für die Namen und Werte einigen:

Zusammenfassung

Leider speichert Microsoft diese Zusammenfassung nicht automatisch ab. Sie können und sollten Sie aber für sich und ihre Firma dokumentieren.

Danach hat der Assistent in meiner Subscription mehrere Ressourcen und die Azure Function angelegt.

Azure Function Code

Nach dem Abschluss muss ich natürlich noch eine Funktion hinterlegen, die letztlich ausgeführt wird. Auch hier hilft mir ein Assistent, der direkt aus der Seite erreichbar ist.

Für meine einfache Übung reicht mit die Entwicklungsumgebung im Portal.

Ich wähle "HTTP" als Trigger aus, gebe dem Webhook einen Namen "Graph_Webhook" und stellt den "Authorization Level" auf Anomymous. Der Callback per Graph authentifiziert sich nicht. Meine Funktion muss später anhand der Payload entscheiden, ob das ein legitimer Request ist.

Microsoft füllt das Skript schon mal mit einem Beispielcode.

Den kann ich auch einfach im Browser aufrufen:

Damit ist die grundlegende Funktion demonstriert.

Erstes Skript

Das Beispiel-Skript ist natürlich nur ein Anfang. Mich hat schon interessiert, welche Properties da so alles mit kommen. Daher habe ich den Code durch folgende Zeilen ersetzt, welcher einfach alle Header, den Body und die Parameter ausgibt:

using namespace System.Net

param($Request, $TriggerMetadata)

Write-Host "AzureFunction: Start"

Write-Host "AzureFunction: ---------------- Headers -----------------"
foreach ($header in $Request.Headers.Keys){
   Write-Host "$($header) = $($Request.Headers.($header))"
}
Write-Host "AzureFunction: ---------------- RawBody -----------------"
Write-Host $Request.RawBody

Write-Host "AzureFunction: ---------------- Body -----------------"
Write-Host "CallID1=$($Request.body.value.resourceData.id)"

Write-Host "AzureFunction: ---------------- Query -----------------"
Write-Host "Query validationToken = $($Request.Query[""validationToken""])"
foreach ($query in $Request.Query.keys){
   Write-Host "Querykey:$($query) = $($Request.Query.$($query))"
}

Write-Host "Sending $($Body)"
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = [HttpStatusCode]::OK
    ContentType = 'text/plain'
    Body = "<h1>Azure Function Demo</h1><p>Done</p>"
})

Write-Host "AzureFunction: End"

Test per Invoke-Restmethod

Ich habe mit folgendem PowerShell-Einzeiler einen Request simuliert.

Invoke-WebRequest `
   -Uri "https://msxfaqdev-graph.azurewebsites.net/api/Graph_Webhook" `
   -Method POST `
   -Body "{test}"

In der Rückgabe sehen Sie auch die Rückgabe des Content.

StatusCode        : 200
StatusDescription : OK
Content           : Graph_Webhook: Body Done
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Request-Context: appId=
                    Date: Fri, 12 Nov 2021 18:08:41 GMT
                    Content-Type: text/plain; charset=utf-8

                    Graph_Webhook: Body Done
Headers           : {[Transfer-Encoding, System.String[]], [Request-Context, System.String[]], [Date,
                    System.String[]], [Content-Type, System.String[]]}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 24
RelationLink      : {}

Im Debugging war folgendes zu sehen. Die Azure Function bekommt also durchaus jede Menge Header zur weiteren Auswertung:

2021-11-13T16:26:08.839 [Information] Executing 'Functions.Graph_Webhook' (Reason='This function was programmatically called via the host APIs.', Id=<guid>)
2021-11-13T16:26:08.866 [Information] INFORMATION: Graph_Webhook: Start
2021-11-13T16:26:08.868 [Information] INFORMATION: Graph_Webhook:Headers: connection = Keep-Alive
2021-11-13T16:26:08.868 [Information] INFORMATION: Graph_Webhook:Headers: content-length = 6
2021-11-13T16:26:08.888 [Information] INFORMATION: Graph_Webhook:Headers: content-type = application/x-www-form-urlencoded
2021-11-13T16:26:08.888 [Information] INFORMATION: Graph_Webhook:Headers: host = msxfaqdev-graph.azurewebsites.net
2021-11-13T16:26:08.888 [Information] INFORMATION: Graph_Webhook:Headers: max-forwards = 9
2021-11-13T16:26:08.888 [Information] INFORMATION: Graph_Webhook:Headers: user-agent = Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.19043; de-DE) PowerShell/7.2.0
2021-11-13T16:26:08.888 [Information] INFORMATION: Graph_Webhook:Headers: x-waws-unencoded-url = /api/Graph_Webhook
2021-11-13T16:26:08.888 [Information] INFORMATION: Graph_Webhook:Headers: client-ip = 10.0.32.18:27534
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: x-arr-log-id = 2fb06f5c-fbd9-417e-bb7d-09e2cd61470f
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: x-site-deployment-id = msxfaqdev-graph
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: was-default-hostname = msxfaqdev-graph.azurewebsites.net
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: x-original-url = /api/Graph_Webhook
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: x-forwarded-for = 94.31.83.223:42076
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: x-arr-ssl = 2048|256|C=US, O=Microsoft Corporation, CN=Microsoft RSA TLS CA 02|CN=*.azurewebsites.net
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: x-forwarded-proto = https
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: x-appservice-proto = https
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: x-forwarded-tlsversion = 1.2
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Headers: disguised-host = msxfaqdev-graph.azurewebsites.net
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook:Body = {test}
2021-11-13T16:26:08.889 [Information] INFORMATION: Graph_Webhook: End
2021-11-13T16:26:08.890 [Information] Executed 'Functions.Graph_Webhook' (Succeeded, Id=<guid>, Duration=73ms)

Test

Im Azure Portal gibt es sogar eine direkte Möglichkeit zum Testen der Function. Allein im Browser kann ich einen Request mit Parametern und Body-Payload zusammenstellen und an die Function senden:

Logging

Der einfachste Weg für einfache PowerShell Lösungen ist das Editieren und protokollieren im Browser. Es geht aber schon in Richtung Write-Host Debugging und eignet sich für eher einfache Dinge. Ansonsten sollten Sie sich schon mit Visual Studio Code oder Visual Studio auseinandersetzen.

How to debug Azure Functions with Visual Studio Code | Azure Tips and Tricks
https://www.youtube.com/watch?v=mdJ6kiqOruY

Azure Functions Local Debugging and More with Donna Malayeri
https://www.youtube.com/watch?v=fxMpdVnh_sc

Storage

Weiter oben haben ich schon geschrieben, dass eine Azure Function zwingend auch einen StorageAccount braucht. Bei der Anlage der Azure Function wird der Storage Account auch direkt mit angelegt und über den Microsoft Azure Storage Explorer kann ich einfach in den StorageAccount reinschauen:

Download
https://go.microsoft.com/fwlink/?LinkId=708343&clcid=0x407

Ich muss mich danach wieder mit einem AzureAD-Konto anmelden und dann durch die ermittelten Speicher laufen.

In den "Bloc Containers" liegen einige kleine Konfigurationsdateien. Interessant für die Azure Functions ist der Bereich "File Shares". Das sind quasi die "freigegebenen Bereiche", auf die auch Azure Functions zugreifen können. Ich würde allerdings nicht per Azure Functions in die gleiche Datei schreiben, denn Functions können ja parallel laufen und dann wäre wieder Locking ein Thema.

In dem Storage landen aber auch die Logs, die ich beim Aktivieren der Diagnose erhalte.

Wie geht es weiter?

Ein per HTTP aufrufbaren PowerShell-Code reißt mich nun nicht vom Hocker. Diese Seite war nur eine Vorarbeite auf darauf aufbauende Lösungen. Denn speziell mit Teams gibt es viele APIs, die einen "Callback" als Webhook einrichten. Ich brauche also einen eigenen Webserver, der zumindest von Teams oder anderen Diensten per HTTPS über das Internet erreichbar ist, die Rückmeldungen in Form von HTTP-POST annimmt und weiter verarbeitet.

Ich kann und will ja nicht meinen Arbeitsplatz per HTTPS aus dem Internet erreichbar machen. Für den Entwickler gibt es natürlich Lösungen wie NGROK (https://ngrok.com/) u.a., mit denen er einen externe URL auf seinen auf dem PC laufenden Code senden kann. Als Firma könnte ich noch den Azure AD Application Proxy einsetzen, um eine interne Lösung über Azure als Web Application Firewall erreichbar zu machen. In beiden Fällen brauche ich aber einen 24h laufenden Server, der die Anfragen dann annimmt.

Mit den Azure Functions kann ich "serverless" solche Funktionen entwickeln, bereitstellen und die gewünschten Daten z.B. in einer CosmosDB oder Azure Tables ablegen. Es reicht dann, wenn ein interne Prozess einfach zyklisch die Datenbank abfragt und entsprechende Daten exportiert, Reports anfertigt oder andere Aktionen auslöst. Ich denke da an:

  • Teams Bots
    Ein Bot ist ja ein Code, der auf die Aktivierung durch den Anwender wartet. Er registriert sich bei Teams mit einer URL und sobald ein Benutzer den Bot anspricht, ist es technisch ein HTTPS-Post gegen die vom Bot hinterlegte URL. Ihr Bot kann also auf einem eigenen Webserver laufen oder sie nutzen einfach eine Azure Function.
  • Graph Subscriptions
    Graph mag es nicht, wenn Sie immer wieder die Daten "pollen", sondern sie können Subscriptions anlegen, so dass Graph ihren Code per HTTP-POST informiert. Auch dazu müssen Sie einen Webserver betreiben
  • Provisioning per Skript
    Wie oft müssen Sie sich erst per PowerShell z.B. mit Exchange Online verbinden, um dann gewisse Aktionen über das WAN auszuführen. Könnte man bestimmte Aktionen nicht einfach als Azure Function umsetzen, so dass jemand die einfach per "Invoke-Webrequest" anstoßen kann und Authentifizierung u.a. in der Function erfolgt?

Entsprechende Skripte und dazu passende Beschreibungen habe ich schon fast fertig aber müssen erst noch etwas reifen.

Weitere Links