Von Zeit zu Zeit probiere ich ganz gerne neue Dinge aus – in diesem Fall Dienste und Cloud-Services, die von der Cloud-Computing-Plattform Azure von Microsoft bereit gestellt werden. Azure ist dabei eine direkte Konkurrenz zu Amazon Web Services (AWS) oder der Google Cloud Platform (GCP).
Mit AWS hatte ich beruflich einige Erfahrungen machen dürfen, mit der Google App Engine hatte ich mich vor geraumer Zeit ebenfalls kurz beschäftigt, insofern kam mir das Test-Angebot von Microsoft sehr gelegen. Bei Registrierung erhält man bei Azure eine Gutschrift von momentan 170 EUR, die man innerhalb eines 30-tägigen Tests nutzen kann. Darüber hinaus fallen keine weiteren Kosten an, nicht zuletzt kann der Test jederzeit beendet werden, wobei das Restguthaben verfällt. Amazon und Google haben ähnliche Angebote, die bei der ersten Verwendung ihrer jeweiligen Cloud-Plattform in Anspruch genommen werden können, bei Microsoft konnte ich jedoch netterweise den Testzeitraum netterweise erneut buchen, die erste Begegnung mit Azure lag anscheinend weit genug zurück.
Willkommen bei Microsoft Azure!
Es dürfte jedoch unmöglich sein, innerhalb eines kurzen Zeitraums alle Services zu testen – dazu sind die Dienste mittlerweile einfach zu vielfältig. Der Betrieb von virtuellen Maschinen ist noch als einfacher Dienst anzusehen, ebenso die Bereitstellung einer Laufzeitumgebung von Container, auch wenn es dafür bereits mehrere, unterschiedliche Dienste gibt. Azure umfasst jedoch alles rund um Anwendungshosting, serverlose Funktionen, gemanagte Datenbanken (SQL und „NoSQL“), Storage, Authentifizierung, Überwachung, DevOps-Unterstützung usw., kurzum – bereits die Aufzählung aller Dienste würde den Blog-Eintrag hier sprengen.
Insofern habe ich mich zunächst mit dem auch von Microsoft als „schnellsten Weg zum Veröffentlichen Ihrer webbasierten Projekte“ (Azure Developer Guide) bezeichneten Dienst Azure App Service beschäftigt. Die folgenden Ausführungen stellen somit eine rein subjektive, persönliche Erfahrung – und ebenso Meinung – dar. In einem anderen Umfeld, unter anderen Voraussetzungen, anderen Bedingungen, anderen Vorerfahrungen mag man zu einer völlig gegensätzlichen Auffassung kommen. Dessen bin ich mir bewusst, und das ist auch völlig in Ordnung. Ein größeres, mittelständisches Unternehmen legt nun einmal andere Maßstäbe an als ein „Einzelkämpfer“, ergo Freelancer, oder jemand, der eine kleine Website für einen Verein oder seinen eigenen Web-Auftritt benötigt. Das beginnt beispielsweise bei den Preisen und hört bei der garantierten Verfügbarkeit von Diensten noch lange nicht auf.
Nach der Registrierung erhielt ich erst einmal diverse Bestätigungsmails, Erste-Hilfe-Texte und ähnliche Informationen. Bereits ein erster Blick in die Dokumentation (Übersicht) versprach einiges. Und um es vorweg zu nehmen – die Dokumentation ist gut. Sehr gut sogar. Die ersten Schritte werden einem dabei sehr leicht gemacht, man kann auch kostenlos Videotrainings erhalten, aber da dies wiederum bei einem Dritt-Dienstleister stattfindet und eine weitere Registrierung voraussetzt, habe ich davon erst einmal Abstand gehalten. Zwar vermute ich, dass zumindest ein Teil der Dokumentation maschinell übersetzt wurde – das lässt der Hinweise, dass man sich den englischsprachigen Text in einem Popup-Fenster anzeigen lassen kann, vermuten, aber nichtsdestotrotz sind auch diese Anleitungen durchweg gut, leicht zu lesen und umfassend genug, um die ersten Schritte zu gehen. Im Vergleich zu AWS, aber auch GCP hat mich die Qualität und Quantität der Dokumentation tatsächlich beeindruckt, Microsoft hat hier gute Arbeit geleistet. Zwar kommen mitunter sperrige Wörter dabei heraus – „Bereitstellungsbenutzer“ für „deployment user“, aber immerhin lassen sich die Texte dennoch gut verstehen. Und nach einem deutschen Wort für „Deployment“ habe ich tatsächlich schon länger gesucht…
Cloud-Shell und Azure CLI
Nach der Anmeldung habe ich mich erst einmal ein wenig in der Web-UI umgesehen. Eine Vielzahl von Diensten wartet auf deren Einrichtung, man erhält eine Kostenübersicht, kann das Dashboard individuell konfigurieren usw. – die Möglichkeiten sind fast schon unbegrenzt zu nennen. Interessanterweise bedienen sich die Anleitungen, ob „Schnellstarts“ oder Tutorials jedoch der Benutzung der Kommandozeile (Command Line Interface, CLI). Das wiederum empfinde ich ebenfalls als positiv, denn per CLI kann das konkrete auszuführende Kommando angegeben werden, so dass sich die Beschreibung nicht mit einer Darstellung von Klickpfaden beschäftigen muss, die u.U. bei anderen Sprachvarianten und steter Weiterentwicklung wieder ganz anders aussehen können als in der Anleitung. Im Folgenden werde ich übrigens auch CLI als Bezeichnung für die Kommandozeile nutzen und nicht „Befehlszeilenschnittstelle“. Die CLI ist nicht zuletzt im Web-Interface verfügbar, ich bevorzuge jedoch die Installation auf einem Entwicklungsrechner. Für die Installation gibt es wiederum gute Anleitungen, und neben Windows kann die Azure CLI 2.0 auch unter Linux (Pakete liegen vor für die gängigsten Linux-Varianten, darüber hinaus ist die Installation per Skript möglich, Voraussetzung ist Python) und MacOS oder in einer Docker-Umgebung erfolgen.
Zunächst habe ich jedoch testweise die CLI innerhalb der Web-UI von Azure gestartet.
Beim Start dieses „Azure Cloud Shell“ genannten Dienstes wurde ein Speicherkonto angelegt.
Dieser Speicher stellt einen Namespace zum Speichern von Datenobjekten bereit, etwa Blobs, Tabellen, Warteschlangen oder allgemein Dateien. Falls die Cloud Shell über eine Sitzung hinaus genutzt wird, können somit in einem „Clouddrive“ genannten und gemounteten Verzeichnis Dateien gespeichert werden. Da ich jedoch die Azure CLI von einer eigenen Entwicklungsmaschine aus nutzen wollte, war das Clouddrive eher uninteressant.
Installation der Azure-CLI
Der Entwicklungsserver bzw. die -VM läuft unter Ubuntu 18.04, dort sollte zunächst die Azure CLI installiert werden. Die Schritte zur Installation gibt Microsoft vor:
geschke@gohlis:~$ AZ_REPO=$(lsb_release -cs) geschke@gohlis:~$ echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | \ sudo tee /etc/apt/sources.list.d/azure-cli.list [sudo] Passwort für geschke: deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ bionic main
Natürlich – anstatt die Version der Linux-Distribution in einer Umgebungsvariable abzulegen, hätte man auch innerhalb des Kommandos Backticks nehmen können. Bei der Funktionalität gibt es hier jedoch keinen Unterschied. Danach ist noch der Signatur-Key einzurichten:
curl -L https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
Und schließlich kann die Azure-CLI installiert werden:
sudo apt-get update && sudo apt-get install azure-cli
Sollte die Installation funktioniert haben, kann die Azure CLI mit dem Kommando „az“ aufgerufen werden. Mit „az –help“ erhält man eine Übersicht über die zu verwaltenden Dienste, mit „az <dienstname> –help“ wiederum können weitere Hilfen zu den jeweiligen Services angefordert werden, etwa zeigt „az container –help“ die verfügbaren Kommandos zur Verwaltung von Container-Instanzen an.
Azure-CLI Login
In einem ersten Schritt muss man sich mit der Azure CLI auf dem eigenen Konto einloggen. Dies ist nur einmal notwendig – ich habe zwar nicht geprüft, inwiefern das Login über einen Reboot der Entwicklungs-VM hin noch gültig ist, aber auf jeden Fall übersteht der Azure-Login mehrfaches Ein- und Ausloggen auf derselben Maschine. Die Login-Prozedur ist auch insofern bemerkenswert, als dass nicht nur einfach Username und Passwort abgefragt werden.
geschke@gohlis:~/php-docs-hello-world$ az login To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code XXXXXXXXX to authenticate.
Nachdem das Login-Kommando eingegeben wurde, wartet die Azure-CLI darauf, dass man den angegebenen Code bei der genannten URL eingibt.
Sollte man sich im Browser bereits in der Azure-UI befinden, ich auch kein weiterer Login mehr notwendig – die Eingabe des Codes reicht aus.
Daraufhin gibt die CLI die Shell wieder frei und zeigt den erfolgreichen Login mit einer Ausgabe wie dieser an:
geschke@gohlis:~/php-docs-hello-world$ az login To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code XXXXXXXXX to authenticate. [ { "cloudName": "AzureCloud", "id": "eine id", "isDefault": true, "name": "Kostenlose Testversion", "state": "Enabled", "tenantId": "noch eine id", "user": { "name": "ralf@geschke.net", "type": "user" } } ]
Erste Schritte mit der Azure-CLI
Nach dem Login kann man sich beispielsweise die dem eigenen Konto zugeordneten Ressourcen ansehen. Da bislang nur der Standard-Speicher verwendet wurde, ist die Ausgabe noch recht kurz:
geschke@gohlis:~/php-docs-hello-world$ az resource list [ { "id": "/subscriptions/hier steht eine id/resourceGroups/cloud-shell-storage-westeurope/providers/Microsoft.Storage/storageAccounts/und noch eine id", "identity": null, "kind": "Storage", "location": "westeurope", "managedBy": null, "name": "und noch eine id", "plan": null, "properties": null, "resourceGroup": "cloud-shell-storage-westeurope", "sku": { "capacity": null, "family": null, "model": null, "name": "Standard_LRS", "size": null, "tier": "Standard" }, "tags": { "ms-resource-usage": "azure-cloud-shell" }, "type": "Microsoft.Storage/storageAccounts" } ]
Das Standard-Ausgabeformat ist tatsächlich reines JSON! Immerhin ist JSON noch recht gut lesbar. Das Format lässt sich jedoch umstellen, etwa in eine Tabellenansicht:
geschke@gohlis:~$ az resource list --out table Name ResourceGroup Location Type Status ------------------------ ------------------------------ ---------- ------------------------------------------- -------- eine lange id cloud-shell-storage-westeurope westeurope Microsoft.Storage/storageAccounts
Zwar übersichtlicher, aber mit weniger Details. Jedoch lassen sich auch Details aus dem JSON-Output heraus filtern, was bei späteren Beispielen verwendet wird.
Ein PHP-Skript im App Service
Zunächst habe ich mich eng an die Anleitung des Quickstarts „Erstellen einer PHP-Web-App in App Service mit PHP“ gehalten. Neben PHP stellt Microsoft auch Anleitungen für Apps mit Node.js, Ruby, Java, aber auch Docker bzw. Go bereit. Grundsätzlich ist davon auszugehen, dass beliebige Anwendungsstacks funktionieren, denn letztlich basiert der Azure App Service auf Docker-Containern, so dass auch die Nutzung eigener Images möglich ist. Insofern sind die von Microsoft bereit gestellten Laufzeitumgebungen nur eine Möglichkeit aus vielen – wenngleich eine praktische, da man sich dabei nicht um das zugrunde liegende System bzw. das Docker-Image kümmern muss.
Microsoft stellt ein Minimal-Beispiel bereit, das per Github herunter geladen werden kann:
geschke@gohlis:~$ git clone https://github.com/Azure-Samples/php-docs-hello-world Klone nach 'php-docs-hello-world' ... remote: Counting objects: 13, done. remote: Total 13 (delta 0), reused 0 (delta 0), pack-reused 13 Entpacke Objekte: 100% (13/13), Fertig. geschke@gohlis:~$ cd php-docs-hello-world/
Das Beispiel ist wirklich minimal – und zwar besteht es aus einer Zeile, in der PHP „Hello, World!“ per echo ausgibt. Dass dabei sogar eine Lizenzdatei (MIT License) mitgeliefert wird, hat mich dann doch ein wenig erstaunt.
Aber das Beispiel reicht aus, um die grundlegenden Schritte für das Deployment von Anwendungen per Azure App Service kennenzulernen.
Anlegen eines Deployment-Users
Zunächst muss ein „Bereitstellungsbenutzer“ angelegt werden, dessen Accountdaten beim Deployment – ergo der Bereitstellung – verwendet werden. Allgemein lautet der Befehl wie folgt:
az webapp deployment user set --user-name <username> --password <password>
Der erste Versuch mit realen Daten schlug jedoch fehl:
geschke@gohlis:~/php-docs-hello-world$ az webapp deployment user set --user-name webappuser --password pwd123 The publishing username has to be globally unique, and the name you provided has already been taken by another Azure customer. Please type in a different name.
Hier wird tatsächlich verlangt, dass der Username global eindeutig sein muss. Warum hier kein Namensraum transparent, also für den Azure-User unsichtbar, implizit hinzugefügt wird, erschließt sich mir nicht. Nach einer kleinen Anpassung klappte es jedoch mit dem Anlegen des Deployment-Users:
geschke@gohlis:~/php-docs-hello-world$ az webapp deployment user set --user-name geschkenetuser1 --password pwd123 { "id": null, "kind": null, "name": "web", "publishingPassword": null, "publishingPasswordHash": null, "publishingPasswordHashSalt": null, "publishingUserName": "geschkenetuser1", "type": "Microsoft.Web/publishingUsers/web", "userName": null }
Die JSON-Ausgabe mit „null“-Angaben sieht zwar seltsam aus, ist in diesem Fall aber vollkommen richtig und belegt den Erfolg der Aktion. Falls hingegen der Fehler 'Conflict'. Details: 409
zurück gegeben wird, muss der Username geändert werden, und bei einem Fehler 'Bad Request'. Details: 400
ein anderes bzw. sichereres Passwort gewählt werden.
Diese Angaben sind nur der Dokumentation zu entnehmen und leider nicht selbsterklärend. An dieser und an weiteren Stellen merkt man Azure eine gewisse Vergangenheit an.
Erstellen einer Ressourcengruppe
Der nächste Begriff, der einem bei Azure öfter begegnen wird, lautet „Ressourcengruppe“, oder „Resource Group“. Damit ist ein logischer Container gemeint, in dem sich verschiedene Ressourcen zusammenfassen lassen. Ressourcen sind dabei unterschiedliche Azure-Dienste wie Web-Apps, Speicherkonten, Datenbanken usw.. Die zu einer Ressourcengruppe gehörenden Dienste lassen sich gemeinsam verwalten, insbesondere auch wieder löschen. Azure bietet hier alle Freiheiten, d.h. es wird keine Struktur vorgegeben, eine sinnvolle Einteilung bleibt dem User überlassen. Einer Ressourcengruppe muss ein Standort zugeordnet werden, so dass die Ressourcengruppe dann jeweils auf einen Standort festgelegt wird. Mit Standorten sind letztlich die Orte der Rechenzentren von Azure gemeint. Die aktuellen Standorte können per az-Kommando abgefragt werden:
geschke@gohlis:~/php-docs-hello-world$ az appservice list-locations --sku S1 --linux-workers-enabled [ { "name": "Central US" }, { "name": "North Europe" }, { "name": "West Europe" }, [...Ausgabe gekürzt...] { "name": "Korea Central" } ]
Die meisten Parameter des az-Kommandos erklären sich selbst, mit --linux-workers-enabled
werde nur diejenigen Standorte angezeigt, die Linux-Container für den Azure App Service anbieten. Der Parameter --sku S1
hingegen gab mir zunächst Rätsel auf. Zwar ist die Bedeutung von S1 einfach zu finden, hierbei handelt es sich um einen Standard-Tarif, und zwar um den Serviceplan Standard die Instanz S1. Diese beinhaltet einen Rechenkern, 1,75 GB RAM und 50 GB Storage. Weitere Service-Pläne, deren Details inkl. Preise können der Übersicht entnommen werden. Da ich bislang wenig mit Shop-Systemen oder Logistik zu tun hatte, musste ich die Bedeutung von „SKU“ erst nachschlagen – die Abkürzung steht für „Stock Keeping Unit„, ursprünglich eine Identifikationsummer zur Inventarisierung von Artikeln an einem Standort. Das passt wiederum zur Azure-Welt, denn Ressourcen werden immer für einen Standort definiert bzw. angelegt. Natürlich ist es auch möglich, Ressourcen an mehreren Standorten anzulegen und zwecks verbesserter Ausfallsicherheit Anwendungen zu verteilen. Microsoft bietet dafür in der Dokumentation Szenarien und Beispiele.
Eine Ressourcengruppe wird allgemein erstellt mit dem Kommando:
az group create --name <myResourceGroup> --location "<Location>"
Für das Beispiel wird eine Ressourcengruppe am Standort „West Europe“ angelegt:
geschke@gohlis:~/php-docs-hello-world$ az group create --name geschkenetrgweb1 --location "West Europe" { "id": "/subscriptions/eine ganz lange ID/resourceGroups/geschkenetrgweb1", "location": "westeurope", "managedBy": null, "name": "geschkenetrgweb1", "properties": { "provisioningState": "Succeeded" }, "tags": null }
Die Rückgabe stimmt positiv, das Anlegen hat funktioniert.
Die Ressourcengruppe beinhaltet nur eine Angabe des Standortes, legt jedoch keine Details wie Rechenleistung oder Größe des RAM oder des Storage für den App Service fest.
Anlegen eines App Service Plans
Mit dem App Service Plan hingegen wird innerhalb der Ressourcengruppe festgelegt, welchen Tarif und somit welche Leistung die Instanz des App Service erhalten soll. Der Parameter „--sku
“ bestimmt dabei den jeweiligen Tarif. Im Beispiel ist dies wiederum der Serviceplan Standard mit der Instanz S1. Der allgemeine Befehl lautet:
az appservice plan create --name myAppServicePlan --resource-group myResourceGroup --sku S1 --is-linux
Konkret sieht die Ausführung wie folgt aus:
geschke@gohlis:~/php-docs-hello-world$ az appservice plan create --name geschkenetserplan1 --resource-group geschkenetrgweb1 --sku S1 --is-linux { "adminSiteName": null, "appServicePlanName": "geschkenetserplan1", "geoRegion": "West Europe", "hostingEnvironmentProfile": null, "id": "/subscriptions/eine id/resourceGroups/geschkenetrgweb1/providers/Microsoft.Web/serverfarms/geschkenetserplan1", "isSpot": false, "kind": "linux", "location": "West Europe", "maximumNumberOfWorkers": 10, "name": "geschkenetserplan1", "numberOfSites": 0, "perSiteScaling": false, "provisioningState": "Succeeded", "reserved": true, "resourceGroup": "geschkenetrgweb1", "sku": { "capabilities": null, "capacity": 1, "family": "S", "locations": null, "name": "S1", "size": "S1", "skuCapacity": null, "tier": "Standard" }, "spotExpirationTime": null, "status": "Ready", "subscription": "eine id", "tags": null, "targetWorkerCount": 0, "targetWorkerSizeId": 0, "type": "Microsoft.Web/serverfarms", "workerTierName": null }
Einrichtung einer Web-App
Nun kann endlich auch die Web-App eingerichtet werden. Dabei lässt sich die Laufzeitumgebung aus den verfügbaren Images wählen. Eine aktuelle Liste erhält man wie folgt:
geschke@gohlis:~/php-docs-hello-world$ az webapp list-runtimes --linux [ "RUBY|2.3", "NODE|4.4", [...weitere Node.js Versionen...] "NODE|8.11", "NODE|9.4", "NODE|10.1", "PHP|5.6", "PHP|7.0", "PHP|7.2", "DOTNETCORE|1.0", "DOTNETCORE|1.1", "DOTNETCORE|2.0", "TOMCAT|8.5-jre8", "TOMCAT|9.0-jre8", "JAVA|8-jre8", "GO|1" ]
Zwar geht das Beispiel von PHP 7.0 aus, interessanter erscheint mir hingegen PHP 7.2 – wenn schon, dann möchte ich auch die aktuellste PHP-Version einsetzen.
Nach einer Weile waren auch die Ressourcengruppe und der Service-Plan in der Web-UI zu finden. Beim Test fiel auf, dass es immer eine kleine Verzögerung gab zwischen dem Zeitpunkt des Anlegen bzw. der erfolgreichen Rückmeldung per Azure-CLI und der Anzeige innerhalb der Web-UI. Insbesondere im Dashboard werden neu hinzugefügte oder gelöschte Ressourcen nicht sofort dargestellt.
Die Web-App wird angelegt mit:
az webapp create --resource-group myResourceGroup --plan myAppServicePlan --name <app_name> --runtime "PHP|7.2" --deployment-local-git
Bzw. konkret:
geschke@gohlis:~/php-docs-hello-world$ az webapp create --resource-group geschkenetrgweb1 --plan geschkenetserplan1 --name geschkenetapptest01 --runtime "PHP|7.2" --deployment-local-git Local git is configured with url of 'https://geschkenetuser1@geschkenetapptest01.scm.azurewebsites.net/geschkenetapptest01.git' { "availabilityState": "Normal", "clientAffinityEnabled": true, "clientCertEnabled": false, "cloningInfo": null, "containerSize": 0, "dailyMemoryTimeQuota": 0, "defaultHostName": "geschkenetapptest01.azurewebsites.net", "deploymentLocalGitUrl": "https://geschkenetuser1@geschkenetapptest01.scm.azurewebsites.net/geschkenetapptest01.git", "enabled": true, "enabledHostNames": [ "geschkenetapptest01.azurewebsites.net", "geschkenetapptest01.scm.azurewebsites.net" ], [...Ausgabe gekürzt...]
Da die Ausgabe sehr umfangreich ist, habe ich auf eine vollständige Darstellung verzichtet. Interessant ist der Parameter „--deployment-local-git
„. In der Ausgabe des az-Kommandos ist eine Git-Remote-URL zu finden. Damit kann Git anschließend konfiguriert werden, so dass das Deployment einfach mittels „git push
“ erfolgt. Es empfiehlt sich somit, die genannte URL zu notieren.
Nach dem Erzeugen der Web-App ist bereits eine Beispiel-Seite unter der URL „http://<app_name>.azurewebsites.net“ erreichbar.
Dabei wird eine Standardseite angezeigt. Der Hostname findet sich auch in der Ausgabe des „az webapp create“-Kommandos. Ebenso finden sich alle Angaben inkl. Hostname, Git-URL usw. in der Web-UI.
Deployment der Web-App mit Git
Doch nun sollen eigene Inhalte – in dem Fall die Beispiel-Hello-World-PHP-Datei – übertragen werden. Dazu muss im lokalen Git-Repository ein neues Ziel für die Remote-Übertragung eingesetzt werden. Allgemein:
git remote add azure <deploymentLocalGitUrl-from-create-step>
Hier im Beispiel somit:
geschke@gohlis:~/php-docs-hello-world$ git remote add azure https://geschkenetuser1@geschkenetapptest01.scm.azurewebsites.net/geschkenetapptest01.git
Ein Hinweis am Rande – die in diesem Beitrag genannten URLs und Hostnamen sind nach dem Test natürlich wieder entfernt worden. Es wäre somit zwecklos, sich das Beispiel online ansehen zu wollen.
Beim Deployment mit „git push
“ wird das Passwort des im Vorfeld eingetragenen Deployment-Users benötigt. Die Bereitstellung an sich ist ein Standard-git-Befehl:
geschke@gohlis:~/php-docs-hello-world$ git push azure master Password for 'https://geschkenetuser1@geschkenetapptest01.scm.azurewebsites.net': Zähle Objekte: 13, Fertig. Komprimiere Objekte: 100% (11/11), Fertig. Schreibe Objekte: 100% (13/13), 2.07 KiB | 531.00 KiB/s, Fertig. Total 13 (delta 2), reused 0 (delta 0) remote: Updating branch 'master'. remote: Updating submodules. remote: Preparing deployment for commit id 'cc39b1e4cb'. remote: Generating deployment script. remote: Generating deployment script for Web Site remote: Generated deployment script files remote: Running deployment command... remote: Handling Basic Web Site deployment. remote: Kudu sync from: '/home/site/repository' to: '/home/site/wwwroot' remote: Copying file: '.gitignore' remote: Copying file: 'LICENSE' remote: Copying file: 'README.md' remote: Copying file: 'index.php' remote: Deleting file: 'hostingstart.html' remote: Ignoring: .git remote: Finished successfully. remote: Running post deployment command(s)... remote: Deployment successful. remote: App container will begin restart within 10 seconds. To https://geschkenetapptest01.scm.azurewebsites.net/geschkenetapptest01.git * [new branch] master -> master
Das war tatsächlich bereits alles! Dank des Deployments per git wäre auch die Integration in Continuous Integraton Umgebungen auf einfache Weise möglich. Nach einem Reload erscheint nun auch die berühmte Nachricht „Hello, World!“ als Ausgabe der index.php-Datei im Browser.
Das war zugegebenermaßen einfach. Aber lässt sich auch eine normale PHP-Anwendung in einem App Service deployen? Nun setzen die meisten PHP-Anwendungen irgend eine Art von Datenbank ein. Das wäre zwar auch irgendwie möglich, doch wollte ich das erste Beispiel eines realen Systems nicht zu kompliziert werden lassen. Daher habe ich mich dafür entschieden, eine Kopie eines CMS zu nutzen, das keine Datenbank voraussetzt. Grav ist ein solches dateibasiertes Content Management System.
Deployment des Grav-CMS
Die Konfiguration wird bei Grav in yml-Dateien gespeichert, die Inhalte liegen im Markdown-Format ebenfalls in Dateien vor. Somit kann ein bestehendes Grav-System relativ einfach ohne jeglichen Datenbank-Dump, Anpassung von Verbindungsparametern usw. kopiert werden. Da ich für eine meiner Seiten Grav einsetze, war die Kopie schnell erledigt. Die Dateien im aktuellen Demo-Verzeichnis hatte ich gelöscht und das komplette Grav-System fand schnell dort Platz. Per git waren alle Verzeichnisse und Dateien schnell committed. Nun hieß es, ein erneutes Deployment auszulösen, ergo ein „git push azure master
„-Kommando. Dabei fiel auf, dass git sehr lange für diese Aktion benötigte. Da ich seit geraumer Zeit mit git arbeite, war ich angesichts dessen bereits ein wenig verwundert. Die Menge von ca. 130 MB sollten eigentlich schnell verwaltet und übertragen worden sein.
Deployment-Probleme
Nach der längeren Deployment-Zeit folgte jedoch die Enttäuschung, zwar wurden die Dateien scheinbar erfolgreich übertragen, aber das Deployment an sich schlug fehl:
geschke@gohlis:~/php-docs-hello-world$ git push azure master Password for 'https://geschkenetuser1@geschkenetapptest01.scm.azurewebsites.net': Zähle Objekte: 3788, Fertig. Komprimiere Objekte: 100% (3571/3571), Fertig. Schreibe Objekte: 100% (3788/3788), 6.42 MiB | 582.00 KiB/s, Fertig. Total 3788 (delta 772), reused 0 (delta 0) remote: Resolving deltas: 100% (772/772), done. remote: Updating branch 'master'. remote: .................... remote: Updating submodules. remote: Preparing deployment for commit id 'e00bbbc8bb'. remote: Object reference not set to an instance of an object remote: App container will begin restart within 10 seconds. remote: Error - Changes committed to remote repository but deployment to website failed. To https://geschkenetapptest01.scm.azurewebsites.net/geschkenetapptest01.git d55ea78..e00bbbc master -> master
Mit der Fehlermeldung konnte ich ebenfalls herzlich wenig anfangen, dazu gleich mehr.
Um ein grundsätzliches Problem auszuschließen, habe ich daraufhin erstmal alles rückgängig gemacht, also die Inhalte von Grav gelöscht, eine im Vergleich zum Beispiel nicht minder triviale index.php-Datei bereit gestellt und erneut bereitgestellt. Tatsächlich hat das dann wieder funktioniert, ich habe mir dafür die Funktion phpinfo() ausgewählt, die alle Angaben zur verwendeten PHP-Version wie erwartet ausgegeben hat.
Um auszuschließen, dass der Fehler mit der Kopie des bestehenden Grav-Systems zusammenhängt, habe ich einen weiteren Versuch gestartet, diesmal mit einer neu heruntergeladenen Grav-Version. Also die Inhalte des zip-Files in das lokale Verzeichnis gepackt, git add ausgeführt, committed, zu guter Letzt wieder das Deployment mit git push azure master
angestoßen. Das Ergebnis war leider genauso ernüchternd wie beim ersten Versuch mit Grav, die Fehlermeldung war dieselbe.
Mit dem Ergebnis hatte ich wirklich nicht gerechnet. Bereits das Deployment war schief gelaufen! Es war nicht etwa so, dass Grav installiert worden war und beim Aufruf Fehler verursacht hätte – die Installation war einfach nicht vorhanden, die Bereitstellung war mit einem Fehler abgebrochen worden. Im Web-UI habe ich mich daraufhin noch ein wenig umgesehen, es ließ sich ein erneutes Deployment initiieren, aber das änderte nicht das Ergebnis – der Abbruch mit der o.g. Fehlermeldung.
Bevor ich die gesamten Tests abgebrochen und die Ressourcen wieder gelöscht habe, wollte ich noch ein wenig mehr wissen. Über diverse Wege konnte ich „Erweiterte Tools“ aufrufen
Von dort ließ sich ein Fenster öffnen, in dem ein Tool namens „Kudu“ zu finden war. Dabei handelt es sich um das Backend von Azure, was für das Deployment mit git zuständig ist.
Per Kudu konnte ich einen Blick auf die Logs werfen. Zunächst ließ sich dort die Fehlermeldung finden, die beim Deployment aufgetaucht ist.
Daneben gibt es noch Logs, die sich auf den laufenden Container beziehen:
2018-07-01 07:54:17.590 INFO - Starting container for site 2018-07-01 07:54:17.590 INFO - docker run -d -p 55638:8080 --name geschkenetapptest01_0 -e WEBSITE_SITE_NAME=geschkenetapptest01 -e WEBSITE_AUTH_ENABLED=False -e WEBSITE_ROLE_INSTANCE_ID=0 -e WEBSITE_INSTANCE_ID=IDxyzabd appsvc/php:7.2.5-apache_1805220521 2018-07-01 07:54:17.590 INFO - Logging is not enabled for this container. Please use https://aka.ms/linux-diagnostics to enable logging to see container logs here. 2018-07-01 07:54:20.824 INFO - Container geschkenetapptest01_0 for site geschkenetapptest01 initialized successfully. 2018-07-01 08:03:20.042 INFO - Starting container for site 2018-07-01 08:03:20.042 INFO - docker run -d -p 36833:8080 --name geschkenetapptest01_1 -e WEBSITE_SITE_NAME=geschkenetapptest01 -e WEBSITE_AUTH_ENABLED=False -e WEBSITE_ROLE_INSTANCE_ID=0 -e WEBSITE_INSTANCE_ID=IDxyzabc appsvc/php:7.2.5-apache_1805220521 2018-07-01 08:03:20.043 INFO - Logging is not enabled for this container. Please use https://aka.ms/linux-diagnostics to enable logging to see container logs here. 2018-07-01 08:03:22.317 INFO - Container geschkenetapptest01_1 for site geschkenetapptest01 initialized successfully.
Leider trug das nicht zur Problemlösung bei, aber es war eindeutig zu erkennen, dass sich hinter dem App Service eine ganz normale Docker-Umgebung verbirgt. Also keine „schwarze Magie“, sondern Docker. Der Hinweis auf weitere Diagnosemöglichkeiten versprach ebenfalls keine Hilfe, da das Problem ja nicht in der Anwendung lag, die während der Laufzeit etwaige Fehler verursachen würde, sondern daran, dass das Deployment gnadenlos gescheitert ist.
Eine Lösung für das Deployment-Problem?
Daraufhin habe ich mich per Google auf die Suche nach der Fehlermeldung begeben – was sollte „Object reference not set to an instance of an object“ bedeuten? In ähnlichen Fällen finden sich nicht nur Beschreibungen des Problems, denn die Wahrscheinlichkeit sollte hoch genug sein, dass ich nicht der Erste war, der auf den Fehler gestoßen ist, sondern auch Lösungen oder wenigstens Workarounds. In diesem Fall jedoch scheiterte auch dies. Interessanterweise ist Kudu in C# geschrieben, dazu passt es auch, dass ich nahezu ausschließlich Hinweise fand, die in Richtung Visual Studio deuteten, und zwar mit TFS (Team Foundation Server) und der Git-Integration. Handelt es sich um dieselbe Code-Basis? Ist es ein grundsätzliches Problem mit Kudu? Nach dem Durchforsten weiterer Fundstellen, die ebenso immer wieder auf Visual Studio, C#, Team Foundation Server usw. hinwiesen, habe ich beschlossen, den Test zu beenden und die Ressourcen wieder zu löschen. Denn wenn bereits ein relativ anspruchsloses System wie Grav nicht mit git deployed werden konnte, wollte ich mir komplexere Szenarien erst gar nicht aufbürden. Das Ergebnis war insofern insgesamt etwas enttäuschend. Denn so schön und einfach wie der Ansatz, per git push
zu deployen auch ist, es müsste dann schon funktionieren. Vielleicht ist unter den Lesern jemand, der sich besser mit der Azure-Welt auskennt und so zu einer Lösung beitragen kann – diesbezügliche Hinweise sind natürlich gern gesehen!
Wie bereits eingangs angedeutet, fühlte sich Azure an dieser Stelle noch etwa hakelig an. Zumindest merkte man, dass die Ursprünge von Azure aus der Windows-Server-Welt stammen. Das muss nicht zwangsläufig schlecht sein, sofern die Tools, wie in dem Fall Kudu, auch wie erwartet funktionieren.
Alles auf Null – es wird aufgeräumt!
Der letzte Schritt des ersten Tests bestand somit aus Aufräumen. Dank der Strukturierung anhand der Ressourcengruppen war dies schnell erledigt. Zum Löschen der Web-App, des Service-Plans und der Ressourcengruppe ist nur das Löschen der Ressourcengruppe notwendig. Alle zugeordneten Services werden damit ebenfalls gelöscht:
az group delete --name myResourceGroup
Hier also:
geschke@gohlis:~/php-docs-hello-world$ az group delete --name geschkenetrgweb1 Are you sure you want to perform this operation? (y/n): y
Fazit
Wie so häufig, liegen Licht und Schatten nah beieinander. Der Einstieg in Azure gelingt sehr leicht – dank guter Dokumentation und breiter Verfügbarkeit der Azure-CLI auf nahezu allen Plattformen. Man ist insofern nicht auf die Web-UI angewiesen, mit der sich natürlich ebenfalls alle Dienste einrichten lassen. Die erste Web-App konnte insofern schnell online gestellt werden. Das Deployment per git wäre eine schöne Lösung – wenn sie denn in jeder Situation funktionieren würde. Der Grund, der dies im Falle des Deployments von Grav verhindert hat, bleibt dabei ein Rätsel. Da der App Service jedoch nur ein kleiner Ausschnitt aus der Azure-Welt ist, ist es zu früh, ein pauschales Urteil zu fällen, insbesondere da das Deployment versagt hat und nicht die Laufzeitumgebung im Form von Docker. Denn von den Ressourcen her hätte die Instanz S1 keine Probleme mit Grav haben dürfen.
Ob die Preise konkurrenzfähig sind, darf jeder selbst entscheiden. Im Vergleich zu Lösungen wie Amazon Web Services oder Google Cloud Platform dürften die Unterschiede eher gering sein. Hochgerechnet auf einen Monat würde die Instanz S1 jedoch tatsächlich bei knapp 60 EUR liegen (aktuell 0.081 EUR / h, 24 Stunden, 30 Tage), was erheblich die Kosten einer VM mit vergleichbaren Leistungen überschreitet. Bei einer VM ist man natürlich selbst für die Verwaltung zuständig, nebst Installation von Docker, Durchführung von Updates etc..
Trotz der eher ernüchternden Erfahrung mit dem Deployment im App Service möchte ich jedoch nicht von Microsoft Azure pauschal abraten. Im Gegenteil – um einmal vorweg zu greifen, oder neudeutsch zu „spoilern“, die Erfahrungen mit den Container Instances waren wesentlich positiver. Dazu aber mehr in einem nächsten Artikel.