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.

Hinweis: Azure Functions Version 2-3 gibt es nur noch bis Dez 2023. Danach laufen sie "unsupported" weiter.
Bitte stellen Sie ihre Funktionen rechtzeitig auf Version 4 um
How to target Azure Functions runtime versions https://learn.microsoft.com/en-us/azure/azure-functions/set-runtime-version

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:

Wer deutlich umfangreichere Funktionen entwickelt oder nicht immer "Internet" hat, kann über die Azure Function Core Tools auch auf dem lokalen PC entwickeln.

Das habe ich aber nicht gemacht, denn dann müsste ich auch die Erreichbarkeit aus der Internet, z.B. für Webhook, über NGROK oder andere Tools einrichten.

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.

CMD-Shell

Im Menü der "Function App" gibt es auch einen "Konsole"-Bereich. Hinter der Azure Function steckt ja je nach gewählter Plattform ein Betriebssystem oder zumindest ein Rumpfsystem.

Allerdings ist das keine PowerShell sondern eine beschränkte CMD-Shell. Per "HELP" sehen Sie die beschränkten Befehle.

Erweiterung

Die Azure Functions haben einen gewissen Grundstock an Befehlen und Commandlets. Interessant wird aber speziell eine PowerShell natürlich erst durch die verschiedenen Module, z.B. für den Zugriff auf das AzureAD, Exchange Online, Teams Online u.a. Natürlich könne ich nun am Anfang der Funktion prüfen, ob diese Module vorhanden sind und ggfls. fehlende Module nachinstallieren. Das ist aber ineffektiv, da der erster Start dann doch Start verzögert ist und im Fehlerfall gar nicht weiter geht. Genau genommen möchte ich als verantwortliche Person doch eine statische Laufzeitumgebung bereitstellen, wie ich es früher mit einem eigenen Server auch gemacht habe. Auch das geht mit Azure Functions, denn ich kann eine Shell starten, um in diesem virtuellen Container zu arbeiten.

Ich habe nun keine interaktive PowerShell, um ein "Install-Module" auszuführen, aber es gibt zwei Optionen. Dazu nutze ich "kudo"-Umgebung, die ich über die "erweiterten Tools" erreiche:

Über die "Debug console" - "PowerShell" kann ich z.B. mit "Get-Command" die verfügbaren Commandlets einsehen aber auch Dateien hochladen.

Hier habe ich dann zwei Optionen:

  1. Datei "site/wwwroot/requirements.psd1"
    Ich kann die Module über die Konfigurationsdatei "requirements.psd1" im wwwroot-Verzeichnis die erforderlichen Module hinterlegen, die der function app host dann automatisch von der PowerShell Gallery lädt und einbindet. Hier die "Default"-Datei, welche noch nichts eingebunden aber "AZ" schon vorbereitet hat.

    Sie können einfach weitere Module als eigene Zeilen mit "'Modulname' = 'Minimumversion'" addieren.
  2. Import in "Site\wwwroot\modules"
    Der zweite Weg ist das Modul z.B. auf dem eigenen Computer zu installieren und dann das komplette Modulverzeichnis aus "C:\Program Files\WindowsPowerShell\Modules\<modulname>" in den Function Host zu importieren.

In beiden Fällen sollte ich über eine Anfrage in der Function selbst einmal prüfen, ob die Module auch vorhanden sind, z.B. über ein OutputBinding

Push-OutputBinding `
   -Name Response `
   -Value ([HttpResponseContext]@{
       StatusCode = [HttpStatusCode]::OK
       Body       = $(Get-Module -ListAvailable | Select-Object Name, Path)
})

Persistenz/Durable Functions

Klassisch wird eine Azure Function gestartet, tut etwas und am Ende wird alles wieder "Frei" gegeben. Nun gibt es aber auch Anforderungen, dass bestimmte Informationen auch folgenden Aufrufen bereitgestellt werden, ich denke da an:

  • OAHT Tokens
    Eine Function kann ja mit den übergebenen Werten z.B. per Graph weitere Aktionen auslösen. Dazu muss sich die Application aber authentifizieren. Auch das ist nicht schwer aber das Token gilt einige Stunden und sollte nicht mit jedem Aufruf immer wieder generiert werden. Es sollte aber schon "sicher" so abgelegt werden, dass nur die Function wieder zugriff darauf hat.
  • Remote Shells/Powershell Module
    In einer Azure Function kann ich z.B. weitere PowerShell-Module einbinden. Wenn meine Function z.B. eine Remote PowerShell zu Exchange Online aufbaut, um Benutzer zu provisionieren, dann kostet die Verbindung doch recht viel Zeit. Wünschenswert wäre da vielleicht schon eine Möglichkeit diese Aktionen in einen Teil auszulagern, der aktiv bleibt.
  • Session-States
    Azure Functions eigenen sich für Webhooks, z.B. Bots und auch hier kann es sein, dass der Code seine "Session" pflegen muss, um auch frühere Daten reagieren zu können

Es reicht also nicht, wenn eine Function gestartet wird und nach der Verarbeitung wieder alles weg wirft. Aber auch hierfür gibt es mehrere Ansätze einer Lösung

  • Selbst Speichern (Filesystem, CosmosDB, Azure Tables etc.)
    Über ein Eingabe und Ausgabe-Funktionen kann ich natürlich Wert an einem Ort ablegen und beim nächsten Aufruf wieder starten. Das geht sogar einfach per Export-CLIXML auf dem Dateisystem.
  • Durable Functions
    Azure bietet auch eine Möglichkeit bestimmte Funktionen "Dauerhaft" zu machen. Ich könnte also meinen Code für Exchange dorthin auslagern. Ob das aber gelingt, habe ich selbst noch nicht ausprobiert.

Allerdings muss ich bei all den Aktionen natürlich bedenken, dass es parallele aufrufe geben könnte, d.h. die Function mehrfach läuft. Meine Speicherungszugriffe etc. müssen darauf Rücksicht nehmen

Einschränkungen

Es gibt sicher die ein oder andere Limitierung, auf die sie schon gestoßen bin aber ich nicht. Aber bei meinen Tests mit SCIM und dem SCIM-Sample habe ich einige Zeit getüftelt, bis ich mir ein Verhalten erklären könnte. Ich wollte einen SCIM-Aufruf an eine Azure Funktion übergeben und die Function wurde nicht getriggert. Wenn ich die Funktion aber beendet habe, dann hat der SCIM-Aufruf einen "404 not found" geliefert. Die URL war im Prinzip richtig und per PowerShell konnte ich die URL ebenfalls ansprechen und das hinterlegte Skript starten. Irgendwas muss also "anders" sein. Ich habe dann die SCIM-Url auf eine PHP-Seite der MSXFAQ geleitet, welche einfach einen 200OK ausliefert und den Request mir per Mail gesendet hat.

Die im SCIM-Dialog hinterlegte URL habe ich aus dem Azure Function Portal kopiert und in der SCIM-Konfiguration hinterlegt.

https://www.msxfaq.de/scimtest/dump.php

Der SCIM-API hat aber nicht diese API über diese URL mit Parametern aufgerufen, sondern hat einen Pfad drangehängt.

GET /scimtest/dump.php/Users?filter=userName+eq+%220273af21-054f-4add-854b-01ef8cdc17df%22 HTTP/1.0
Host: www.msxfaq.de
X-Real-Ip: 20.190.160.160
X-Forwarded-For: 20.190.160.160
Connection: close
Adscimversion: 6.0

Anscheinend reagiert die Azure Funktion auf "https://msxfaq-scim.azurewebsites.net/api/scim?" und nicht auf https://msxfaq-scim.azurewebsites.net/api/scim/*

Ich habe bislang noch nicht herausgefunden, wie ich der Funktion selbst sagen soll, dass Sie auch "Unterverzeichnisse" erlaubt.

Als Lösung könnte ich für jeden Endpunkt eine eigene Function schreiben oder ist platziere einen Azure AD Application Proxy davor, der die Requests entsprechend umschreibt.

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