Symfony2 auf der Google AppEngine für PHP – kann man machen…

Vor einiger Zeit habe ich über die Installation einer Silex-Anwendung auf der Google AppEngine für PHP (GAE) geschrieben. Inzwischen bietet die Google AppEngine neben Java, Python und Go für PHP vollwertige Unterstützung an und ist dem Beta-Status entwachsen. Meine minimale Website geschke.net läuft inzwischen mit dem Symfony2-Framework, und so dachte ich, dass es eigentlich ganz nett wäre, diese auf der Google AppEngine zu betreiben. Die Anzahl der Besucher hält sich in Grenzen, daher dürfte das kostenlose Kontingent ausreichen, und für die paar Datenbank-Abfragen lassen sich auch ein paar Cent im Monat zahlen.

Die Idee der Google AppEngine als Bestandteil der Google Cloud Platform ist durchaus bestechend. Anstatt sich mit der Administration von (virtuellen) Maschinen, Installation von Betriebssystemen und deren Updates sowie nicht zuletzt den zugrunde liegenden Server-Systeme zu beschäftigen, wird die Anwendung einfach auf die AppEngine deployed, dazu wird eine Datenbank-Instanz gestartet und alles läuft in einer abgesicherten, hochverfügbaren und skalierbaren Umgebung. Natürlich darf diese dann auch ein paar Euro kosten. Zwar summieren sich die Kleinstbeträge auf den Monat betrachtet durchaus zu einem höheren Betrag als bei der Buchung einer „normalen“ VM bei einem beliebigen Provider fällig wäre, aber die Vorteile sollten dies eigentlich aufwiegen können.

Genug der Vorrede – im Folgenden schildere ich mehr oder minder chronologisch meine Erfahrungen der letzten Tage mit Google AppEngine und Symfony. Zunächst dachte ich, dass die Installation binnen eines Abends abgeschlossen sein könnte – ich sollte mich täuschen…

Die Vorbereitungen

Als Basis für alle weiteren Arbeiten diente eine VM unter Ubuntu Linux, enthalten sind alle für die lokale Entwicklung benötigten Programme wie PHP, Webserver, Memcached, MySQL bzw. MariaDB etc..

Zunächst wird das Google Cloud SDK installiert. Dazu dient der Aufruf von


Nach dem Download und der eigentlichen Installation folgen noch Hinweise auf weitere Informationen. Fortan steht das Kommando „gcloud“ zur Verfügung, was als CLI für Administrationstätigkeiten der Google Cloud Platform dient. Weitere Hinweise finden sich in der umfangreichen Cloud Platform Dokumentation, wobei sich hier die Angabe von URLs kaum lohnt, da sich diese sehr stark ändert. Beispielsweise stehen inzwischen diverse Distributionspakete für das Cloud SDK bereit.

Weiterhin muss das AppEngine SDK installiert werden, zunächst erfolgt der Download:


Danach kann die Datei entpackt werden, der Einfachheit halber habe ich es im Verzeichnis namens gcloud entpackt, was mir als zentrale Stelle für alle Google Cloud Tests dient:

Damit die Programme im Pfad zu finden sind, habe ich noch in die .bashrc folgendes mit aufgenommen:


Netterweise – und nur am Rande bemerkt – konnte ich meine alte Silex-Anwendung, die bereits in der AppEngine installiert war, dann einfach herunterladen:


Zwar hatte ich diese auch noch in irgend einem Git-Repository, aber so erhielt ich immerhin eine vollständige Kopie inkl. aller App-Settings, denn diese hatte ich möglicherweise dann doch nicht gesichert…

Erste Schritte mit Symfony2

Weiter geht’s mit Symfony. Google bietet für den einfachen Einstieg eine (leicht) modifizierte Version 2.6.3 des Frameworks zum Download an, inkl. einer recht ausführlichen Beschreibung, wie es innerhalb der AppEngine zum Laufen gebracht werden kann.

Daran habe ich mich zunächst gehalten, bevor ich den Versuch gestartet habe, meine minimale Symfony-Anwendung online zu stellen. Da ich noch eine AppEngine-Instanz hatte, konnte ich die bisherigen Daten übernehmen, d.h Datenbank-Name und Bezeichnung der Applikation. In der app.yaml habe ich zunächst unter ‚application‘ den Namen ‚geschkenet‘ eingetragen. Danach musste in der config_prod.yml im Verzeichnis app/config die Datenbank-Verbindung eingetragen werden. Zwar nutzt das Beispiel keine Datenbank, jedoch schadet der Eintrag auch nicht, und die Instanz war bereits vorhanden. Insgesamt sieht der Eintrag wie folgt aus:


Dazu ein Hinweis – wie hier zu sehen ist, hatte ich die Datenbank zu dem Zeitpunkt noch in einer anderen Anwendung eingerichtet (‚geschkenetsql‘). Das hatte eher traditionelle Gründe – die CloudSQL-Instanz war noch vorhanden, während ich in der aktuellen Anwendung noch keine Datenbank definiert hatte.

Anschließend hieß es, Composer zu holen und wie üblich die Abhängigkeiten zu installieren:


Bei der Abfrage der Datenbank-, Mailserver- und sonstigen Parametern konnten die Default-Werte übernommen werden – für den Betrieb in der AppEngine hatten sie keine Bedeutung.

Leider scheiterte der Versuch, die Beispiel-Anwendung lokal zum Laufen zu bringen. Der Befehl dazu wäre folgender:


Jedoch wurde jeglicher Zugriffsversuch mit einem „internal server error“ quittiert, der auch keine weiteren Informationen mit sich trug. Das Symfony-Logfile war ebenfalls leer. Nichtsdestotrotz – was hatte man schon zu verlieren – habe ich den Versuch gestartet, das Beispiel in die AppEngine hochzuladen, oder auch zu „deployen“:


Der Deployment-Prozess schien funktioniert zu haben – immerhin ein erster Schritt. Was ich allerdings bemerkenswert fand, war die Anzahl der Dateien. Ein minimales Symfony-Projekt, welches mehr oder minder nur ein „Hello, World!“ schreibt, beinhaltet über 8000 einzelne Dateien! Dabei ist absehbar, dass das Limit der AppEngine mit 10000 Dateien selbst bei kleineren Projekten schnell erreicht werden würde, schließlich bringt jedes verwendete Bundle noch etliche Dateien mit, und die eigene Anwendung möchte ebenfalls irgendwo platziert werden.

Der erste Versuch, die Anwendung zu erreichen, brachte zunächst nur eine Fehlermeldung:


Ok, da fehlte wohl etwas… Des Rätsels Lösung: Zur Speicherung der Cache- und Log-Dateien nutzt Symfony üblicherweise Standard-Verzeichnisse innerhalb der eigenen Verzeichnishierarchie. Innerhalb der AppEngine ist jedoch der schreibende Zugriff genau darauf verwehrt. Daher muss zur Speicherung ein so genanntes Bucket im „Cloud Storage“ angelegt werden. Dabei handelt es sich im Prinzip um ein Dateisystem, was per API ansprechbar und unter anderem von der AppEngine-Anwendung erreichbar ist. Sofern nicht explizit ein Bucket-Name angegeben wird, will die AppEngine ein Default Bucket nutzen. Die Einrichtung dieses Buckets gestaltete sich jedoch gelinde gesagt etwas kompliziert.

Tatsächlich habe ich in der „neuen“ AppEngine-Admin-Oberfläche keinen Menüpunkt, geschweige denn Hinweis gefunden, wie sich das Default Bucket anlegen lässt. Die Anleitung dazu lautete „Determine whether your app already has the default bucket created: Visiting the App Engine Admin console and select your app. Select Application Settings in the left navigation pane. […] Visit the App Engine Admin console and select your app. Select Application Settings in the left navigation pane and scroll to the bottom of the page to Cloud Integration“ Jedoch gab es diese Menüpunkte einfach nicht. Erst nach einigem Ausprobieren fand ich heraus, dass in der Beschreibung die Struktur der alten AppEngine-Admin-Oberfläche gemeint war (URL in der Form https://appengine.google.com/dashboard?&app_id=…).

Dort ließ sich ganz unten in den „Application Settings“ unter „Cloud Integration“ das Default Bucket anlegen („Create a Google Cloud project as well as a Google Cloud Storage bucket and a new style service account for this application„). Nachdem dies erreicht war und Google den erfolgreichen Vollzug meldete, war dies im Cloud Storage Browser zu sehen – dort gab es nun ein Bucket, was denselben Namen wie die Anwendung der AppEngine trug. Mit dem Pfad /app/example aufgerufen, wurde anstatt nun endlich „Homepage“ angezeigt – das Pendant des „Hello, World!“-Grußes!

Da ich inzwischen versucht hatte, durch Änderung innerhalb der app.php die Symfony-Anwendung lokal zum Laufen zu bringen, was leider immer noch misslang, habe ich auch einen weiteren Deploy durchgeführt. Dabei ist zu beachten, dass nach jedem Deploy der Cache, d.h. die Cache-Dateien im Bucket gelöscht werden müssen. Dazu hat Google netterweise ein kleines Skript bereit gestellt, was diese Aufgabe erfüllt.

Immerhin – das kleine Beispiel lief. Das alleine war zwar ein Proof-of-Concept, aber auch nicht mehr. Daher wollte ich in einem nächsten Schritt versuchen, meine minimalen Web-Seiten innerhalb der AppEngine zum Laufen zu bringen.

Mini-Website mit Symfony2

Zunächst habe ich dazu alles in ein Arbeitsverzeichnis ausgecheckt. Dort wollte ich einerseits die Symfony-Anwendung lokal mit Hilfe der Symfony-eigenen Tools, d.h. des Entwicklungs-Webservers zum Laufen bringen, zum anderen die Änderungen einpflegen, die von Google bereit gestellt werden. Denn vor kurzem hatte ich die Site auf die neueste Symfony-Version umgestellt, eine Migration auf eine vorherige Version erschien mir kaum sinnvoll. Zudem halte sich die Änderungen sehr in Grenzen, wie sich anhand eines Diffs erkennen lässt. Die meisten Änderungen sind in den Beschreibungen zu finden. Daneben gibt es die AppEngine-Definitionsdatei namens app.yaml im Hauptverzeichnis. Dort werden auch die Routen und einige Umgebungsvariablen definiert. Die Änderungen in der app/AppKernel.php beschränken sich auf das Umleiten der Cache- und Log-Verzeichnisse in das Bucket im Cloud Storage. Hinzugefügt wurde das Skript zum Löschen der Dateien im Cloud Storage. Der Rest besteht aus Änderungen in der Standard-Konfiguration, etwa zur Datenbank-Verbindung oder auch Ausschalten des Standard-Logverhaltens, so dass mittels Syslog geloggt wird. Diese Änderungen waren relativ schnell eingepflegt, auch gab es von der bei Google verwendeten Symfony-Version 2.6.3 und der bei mir eingesetzten 2.7.3 keine relevanten Unterschiede.

Um die Anwendung lokal in der Symfony-Entwicklungsumgebung zum Laufen zu bekommen, habe ich die nach „production“ und „development“ getrennten config-Dateien genutzt. D.h. in der config_dev.yml die Datenbank-Parameter der lokalen Datenbank eingetragen, während in der config_prod.yml die Definition für die AppEngine bzgl. CloudSQL enthalten war. Damit ließ sich die Anwendung nun immerhin lokal testen, wenngleich ohne die AppEngine-spezifischen Einstellungen, da auch hier der Betrieb in der AppEngine-SDK-Umgebung nicht funktionierte.

Eine neue Datenbank

Für das Speichern der User, Rollen und Feedbacks besitzt meine Anwendung eine kleine Admin-Oberfläche. Insofern wurde eine Datenbank benötigt, die zudem mit Default-Werten befüllt werden musste. Dafür hatte ich mir nun doch eine neue CloudSQL-Instanz inklusive Datenbank in der entsprechenden Anwendung angelegt. Die CloudSQL-Instanzen sind unter eriner IPv6-Adresse erreichbar, während die Benutzung einer IPv4-Adresse zwar konfiguriert werden kann, aber Google verlangt dafür einen kleinen Geldbetrag pro Stunde. Da ich mir dies sparen wollte, habe ich auf eine VM zurück gegriffen, bei der der Provider bereits IPv6 anbietet. Da die Konfiguration sowohl Provider-, als auch distributionsspezifisch ist, verzichte ich hier auf eine nähere Darstellung. Grundsätzlich ist festzuhalten, dass der Zugriff von einer öffentlichen IPv6-Adresse problemlos funktioniert. Zunächst musste der Zugriff per Admin-Interface freigeschaltet werden, daneben sollte ein Nutzerkonto erstellt werden. Der Erfolg sieht dann in etwa so aus:


Perfekt! Damit konnte ich die initiale Datenbank in der CloudSQL einspielen. Erst wenig später habe ich entdeckt, dass innerhalb des CLI Google auch Tools zum Datenbank-Zugriff bereit stellt. Andererseits wurde beim Versuch, die SQL-Tools zu installieren, die folgende Meldung ausgegeben: „WARNING: Component [sql] no longer exists.„. Dennoch funktionierte der Zugriff:


Die Datenbank lässt sich z.B. importieren oder exportieren, wobei als Dateiablage Cloud Storage fungiert. Insofern hätte man bei einem Import die zu importierende Datei zunächst dort ablegen müssen. Da ich die Datenbank bereits über den direkten Weg importiert hatte, habe ich den Weg somit nicht weiter verfolgt.

Ab in die Cloud!

Nun folgte der unvermeidliche Deploy. Lokal lief die Anwendung, insofern sprach nichts mehr gegen den Test in der AppEngine. Davor empfiehlt es sich jedoch, die lokal angelegten Cache-Dateien allesamt zu löschen, da diese sonst mit hochgeladen würden. Ergo einmal alles unter app/cache/dev/ geleert.

Nun konnte der Deploy-Schritt durchgeführt werden:


Wie erwartet waren es nicht weniger Dateien als im Beispiel-Projekt, sondern noch einige mehr. Aber immerhin versprach der Deploy, erfolgreich gewesen zu sein. Daher musste nun zunächst der clear_cache-Handler aufgerufen werden, um den vorherigen Cache-Inhalt im Bucket zu löschen. Und siehe da – die ersten Seiten wurden angezeigt. Perfekt? Naja, fast. Eine Meldung störte den ersten, guten Eindruck, und zwar:


Natürlich benutzte die Anwendung Sessions, jedoch hatte ich nichts dazu explizit konfiguriert, was bedeutete, dass das Standard-Verfahren benutzt wurde. Nun heißt das bei Symfony jedoch, dass auf das Filesystem zurück gegriffen wird. Mit der von der AppEngine bereit gestellten Standard-Speicherung im Memcache konnte Symfony hier zunächst nichts anfangen. Ein ähnliches Problem hatte ich bereits bei meinen ersten Tests mit Silex innerhalb der AppEngine. Die Lösung bestand nun darin, Symfony dazu zu bringen, explizit den Memcache-Session-Handler zu benutzen. Die AppEngine leitet Memcache-Anfragen automatisch an den richtigen Ort weiter, d.h. Funktionen wie addServer und ähnliche werden ignoriert. Ein wenig Googlen hat geholfen, am Ende war die Lösung wie folgt:

app/config/services.yml:


app/config/config.yml:


app/config/parameters.yml:

 


Achtung – bei einem weiteren Composer-Lauf würden die Einstellungen in parameters.yml überschrieben, daher sollten die Parameter auch in die parameters.yml.dist mit aufgenommen werden.

Damit funktionierten nun die Sessions, die innerhalb der Google Cloud Memcache Instanz abgelegt wurden. Insofern konnte ich mich in meiner Admin-Oberfläche der Anwendung einloggen, Daten ändern usw..

Die nächste Hürde stellte die Einbindung der ReCaptcha v2 dar. Zwar wird dafür inzwischen ein PHP-Package „google/recaptcha“ angeboten, was sich leicht integrieren lässt, jedoch klappte keine der Verbindungsoptionen auf Anhieb. Die Library kann mittels Sockets, Curl oder auch Stream-Funktionen bzw. file_get_contents() Requests zur Verifizierung der Eingaben durchführen. Da inzwischen Requests an externe Server auch innerhalb der AppEngine unterstützt werden, hätte dies eigentlich kein Problem sein sollen… Aber genau das war es. Letztlich musste ich die SSL-Verifizierung beim Zugriff abschalten:


Auch dabei hatte ich ein gewisses Déjà-Vu, zwar in der Vorgänger-Library, aber die Änderung war dieselbe, die ich schon einmal eingerichtet hatte…

Und endlich funktionierten alle Komponenten der eigentlich doch so minimalen Symfony-Anwendung innerhalb der Google AppEngine für PHP.

Fazit

Nun war doch alles gut, die Anwendung lief, und ich konnte meine alte Site abschalten bzw. umleiten. Oder?

Wie so oft, hat alles seine zwei Seiten. Die Anwendung lief – das war unbestritten. Symfony funktionierte innerhalb der AppEngine für PHP, die Modifikationen hielten sich insgesamt gesehen auch in Grenzen. Denn dass einige Stellen einer Anpassung bedurften, war mir durchaus klar gewesen. Immerhin sollte der Lohn der Mühen nun eine Laufzeitumgebung sein, die managed, skalierbar, ausfallsicher usw. war.

Doch die Performance überzeugte mich leider nicht. Zeitweise vergingen nicht nur am Anfang, als der Cache noch befüllt werden musste, mehrere Sekunden, bis der Server antwortete. Das Abschicken des Feedback-Formulars war zeitweise gar nicht möglich, dabei handelte es sich um ein simples Formular, was nichts weiter als die üblichen Daten abfragen und in einer Datenbank speichern wollte. Dazu kam die ReCaptcha-Verifizierung, aber daran lag es interessanterweise nicht, denn testweise hatte ich diese auch deaktiviert.

Ebenfalls war die Performance der Datenbank ok, zwar nicht vergleichbar mit einer lokalen MySQL-Instanz, aber zumindest in Ordnung. Testweise hatte ich kurzzeitig eine Instanz mit mehr Speicher betrieben, die Unterschiede waren jedoch nicht spürbar. Ebenfalls hatte ich mit den Skalierungs-Einstellungen einiges ausprobiert, eine Instanz mit mehr Speicher verwendet usw., denn natürlich ist der Speicher bei der kleinsten Instanz sehr knapp bemessen, aber auch das hat die Performance nicht steigern können.

Ein wenig Licht ins Dunkel brachten die „Traces“ unter „Monitoring“ in der Google Developers Console. Dort können einzelne Seitenaufrufe analysiert werden. Es zeigte sich, dass extrem viel Zeit – und in der Summe durchaus mehrere Sekunden – mit wiederholten Request verloren gehen, und zwar „/urlfetch.Fetch“. Dabei handelte es sich um Zugriffe auf den Cloud Storage – und somit die Buckets, d.h die Cache-Dateien von Symfony. Zwar sind in der Summe ähnlich viele Memcache-Aufrufe zu verzeichnen, doch diese sind vergleichsweise schnell.

Nun wäre es sicherlich möglich, den Symfony App-Cache irgendwie in den Memcache umzuleiten, so dass keine klassischen Dateien mehr genutzt werden. Oder dasselbe per Datenbank. An dieser Stelle waren mir die Änderungen jedoch zuviel, ganz nebenbei war bereits recht viel Zeit hinein geflossen, denn ursprünglich war mein Ziel, die Seiten „mal eben“ an einem Abend zu deployen, inkl. Umbau etc…

Insgesamt habe ich auch viel Zeit mit Recherchen verbracht, dabei jedoch auch nicht viel heraus gefunden, es gab tatsächlich sehr wenige Tipps zu Symfony in der AppEngine. Oft finden sich nur Verweise auf den Proof-of-Concept von Google, aber darüber hinaus war nicht viel Material zu erhalten.

Das alles führte letztlich zu meinem persönlichen Fazit, dass Symfony2 momentan zwar in der AppEngine läuft, aber dass man, respektive ich es so nicht laufen lassen will. Ergo – kann man machen, muss man aber nicht. Dennoch war es ein interessanter Einstieg in die Welt der Google Cloud, und wer weiß, vielleicht kehre ich später ja irgendwann einmal zurück.

 

 

Ähnliche Beiträge

Tags:

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.