Oder um einige Buzzwords zu nutzen – Einrichtung einer (Sub-)Domain zur Nutzung von HTTPS für den Serverless Computing Dienst Function Compute in der Alibaba Cloud mit Docker-Nginx-Proxy und Docker-Let’s-Encrypt-Nginx-Proxy-Companion. Zugegeben, da heißt es erstmal tief Luft holen und mit der Geschichte ganz am Anfang zu beginnen.
Eigentlich hätte ich den Beitrag auch „Cloud-Geschichten – Teil 8“ nennen können, aber da ich diese kleine Serie letztens für abgeschlossen erklärt hatte, wollte ich nicht in die Versuchung kommen, endlose Fortsetzungen zu schreiben. Zunächst einmal ein kurzer Überblick über die einzelnen Bausteine, die anschließend im besten Fall ein Gesamtbild ergeben.
Domains für Function Compute
Der Serverless Computing Dienst, den Alibaba Cloud Function Compute nennt, funktioniert soweit sehr gut. Ich verwende ihn wie in Teil 6 und Teil 7 beschrieben als Backend für die Formularverarbeitung. Dazu war es nötig, die Funktion mit einer – sagen wir mal – recht sperrigen URL aufzurufen, bestehend aus einer ID, der Location, dem Dienst und schließlich der Domain aliyuncs.com – letztlich wurde daraus „https://<eine längere id>.eu-central-1.fc.aliyuncs.com/2016-08-15/proxy/gnsitetest/feedbackfunc/
„. Das ist erstens lang, zweitens unschön und drittens könnte man damit den Eindruck erwecken, auf Cloud-Dienste zurück zu greifen, was man vielleicht eher verbergen möchte.
Genau dazu bietet Alibaba Cloud die Möglichkeit des „Custom Domain Management“, die auch prominent in der Übersicht des Function Compute Services beworben wird.
Die Einrichtung würde vermutlich leicht vonstatten gehen – ich habe mich mal ein wenig weiter gewagt und bin auf eine Seite gelangt, die noch etwas rudimentär aussieht und auf der sich eine „Custom Domain“ einrichten lässt.
Klickt man auf „create domain“, erscheint ein Formular, um genau dies zu tun.
Das Protokoll lässt sich übrigens nicht ändern, HTTP ist fest definiert. Im unteren Bereich lassen sich noch individuell Pfade einrichten, so dass sich auch die Pfad-Angabe vereinfachen bzw. verkürzen lässt, somit wird ein Pfad einem Service und einer Funktion von Function Compute zugeordnet.
Custom Domains mit verschlüsselter Übertragung
Das erscheint alles sinnvoll und logisch, doch spätestens an dieser Stelle habe ich mich gefragt, wie es mit der Nutzung von SSL-/TLS-Verschlüsselung aussieht, also https. Denn wenn man für eine seiner Domains eine Subdomain einrichtet und dieser per CNAME-Eintrag auf einen anderen Namen verweisen lässt, hinter dem letztlich ein Server steht, auf den man so gut wie keinen Einfluss hat, hätte ich schon gerne gewusst, wie sich https einrichten lässt.
Wir haben 2019 – niemand möchte mehr unverschlüsselte Web-Seiten sehen und erst recht keine Formulardaten per http durch die halbe Welt senden. Das gilt umso mehr, seitdem sich dank der Let’s-Encrypt-Initiative SSL-Zertifikate endlich kostenlos nutzen lassen. Auch wenn die Hürde zuvor nur in wenigen Euro oder Dollar bestand, bei einem für alle Subdomains verwendbaren Wildcard-Zertifikat war die Hürde schon höher, ansonsten wäre hingegen für jede Subdomain ein neues Zertifikat fällig gewesen. Rein wirtschaftlich betrachtet rechnen sich Wildcard-Zertifikate je nach Anbieter übrigens bei ca. 8 – 10 Subdomains. Letztlich bestanden dieselben Überlegungen wie bereits beim Hosting mit OSS.
Also zusammen gefasst – eine eigene Domain bzw. Subdomain für die Function Compute Dienste wären schön, aber wenn, dann muss https zwingend verwendbar sein.
Leider habe ich in der gesamten Dokumentation von Function Compute und erst recht nicht in den inhaltlich eher dünnen Abschnitten über die Einrichtung von Custom Domains nichts über https gefunden. Also wirklich rein gar nichts, an keiner einzigen Stelle. Während bei OSS die Möglichkeit bestand, ein SSL-Zertifikat hochzuladen, ist zumindest auf den ersten Blick davon bei den Custom Domains von Function Compute nichts zu finden. Und während die Dokumentation bei OSS ebenso auf die Zertifikatsverwaltung hinweist, herrscht diesbezüglich Fehlanzeige bei Function Compute.
Ergo lautet das Zwischenfazit, dass Function Compute bei Custom Domains nicht die Möglichkeit bietet, eigene Zertifikate zu verwenden. Bei Nutzung der Alibaba-Cloud-URL ist https übrigens kein Problem, denn die Domains, die Alibaba für den Function Compute Service nutzt, besitzen ein gültiges Wildcard-Zertifikat – hier wurde also alles richtig gemacht.
Nun hätte man des Weiteren auf die Idee kommen könnten, irgend einen anderen Dienst von Alibaba Cloud zu nutzen, der einen Proxy bietet, um von dort aus auf Function Compute zuzugreifen. Der „Server Load Balancer“-Service sieht für mich am geeignetsten aus, vorausgesetzt, er unterstützt auch weitere Dienste als Backend Server, und nicht nur Elastic Compute Service Instanzen, was die Dokumentation auf den ersten Blick nahe legt. Dann würde nur noch das Zertifikat fehlen, was sich ja sicherlich bei Alibaba Cloud erstehen lässt… Ja, dort kann man Zertifikate kaufen. Aber nein, so richtig sinnvoll erscheint es mir nicht, denn die Zertifikate sind einfach extrem teuer im Vergleich mit anderen Anbietern. Die Preise sind so hoch, dass ich sie „sauteuer“ nennen möchte. Diese Möglichkeit hatte ich somit schnell ausgeschlossen.
Ein Proxy für die Cloud
Aber da war ja noch was… Schließlich laufen auf meinen virtuellen Servern seit einiger Zeit der Nginx-Proxy-Container mitsamt Nginx-Proxy-Companion. Dazu hatte ich seinerzeit auch einen Artikel verfasst, für Details verweise ich daher auf „Howto: Nginx-Proxy und Nginx-Proxy-Companion im Docker Swarm Mode auf einem Host„. Der Nginx-Proxy-Container stellt dabei die erste Anlaufstelle für Requests zur Verfügung, ist somit der Reverse-Proxy, der auf den eigentlichen HTTP-Server umleitet. Und der Nginx-Proxy-Companion kümmert sich um die Zertifikatsvergabe der Let’s-Encrypt-Zertifikate. Der Nginx-Proxy und dessen Companion laufen jeweils in Docker-Containern und werden konfiguriert durch Environment-Variablen weiterer Container.
Also könnte man einen Nginx-Container erstellen, der als Reverse-Proxy für Function Compute dient und Requests einfach dorthin weiter leitet. Und die entsprechend konfigurierte Subdomain verweist einfach auf den eigenen virtuellen Server. Damit wären einerseits die Nutzung der eigenen Domain und andererseits die verschlüsselte Übertragung per https möglich, denn um Zertifikate & Co. kümmert sich das Gespann der beiden Nginx-Proxy- und Nginx-Proxy-Companion-Container.
Sinn oder Nicht-Sinn…
Auch mir erschien diese Idee anfangs etwas verwegen, schließlich soll die Cloud ja gerade in den Aspekten Skalierbarkeit, Ausfallsicherheit, Unabhängigkeit von eigenen Servern und deren Unzulänglichkeiten, Verringerung des Management-Aufwands usw. punkten. Und nun wieder der Schritt zurück, der Docker-Container auf dem eigenen virtuellen Server, damit Abhängigkeit von der Leistung desselben, erkauft mit höherem Management-Aufwand? Ja und nein. Soll heißen – es kommt immer darauf an. Die eingangs gesteckten Ziele werden erreicht, was schon einmal für diese Lösung spricht. Auf der eigenen Maschine läuft nur eine Nginx-Instanz, die Daten weiterleitet. Die eigentliche Arbeit wird weiterhin beim Function Compute Dienst verrichtet, was den Punkt Skalierbarkeit bereits relativiert. Schließlich kann ein Nginx schon ziemlich viele Requests verarbeiten und Daten hin- und her schieben. Eine wirkliche Ausfallsicherheit ist hingegen nicht gegeben – fällt die VM aus, ist der Function Compute Dienst auch nicht erreichbar. Das ist schade, aber für meine Anwendung verschmerzbar, immerhin lagern die Web-Seiten für die Domain ebenfalls nur auf einem normalen Webserver und nicht in der Cloud. Hier ließen sich natürlich Lösungen realisieren, die sich auch wieder der Cloud bedienen dürften. Der Verwaltungsaufwand hingegen ist gering, da nur ein weiterer Container bzw. Docker Stack zum Laufen gebracht werden muss. Wobei hier auch ein Docker Service ausreichen würde, aber da ich alle weiteren Dienste ebenfalls als Docker Stack definiert hatte, wollte ich eine einheitliche Lösung.
Einrichtung von DNS bis Docker Stack
Die Konfiguration ist simpel – im ersten Schritt wird der DNS entsprechend eingerichtet. Im konkreten Fall verweist „func.geschke.net“ auf die IP des virtuellen Server:
geschke@anklam:~$ dig func.geschke.net ; <<>> DiG 9.11.4-3ubuntu5-Ubuntu <<>> func.geschke.net [...] ;; ANSWER SECTION: func.geschke.net. 86400 IN A 194.55.15.79 [...]
Danach habe ich den Docker Stack bzw. Service anhand eines Docker-Compose-Files (website.yml
) definiert:
version: '3.3' services: nginx: image: geschke/nginx-swrm volumes: - type: bind source: ./html target: /var/www/html deploy: replicas: 1 placement: constraints: - node.hostname == anklam configs: - source: nginx_config_default target: /etc/nginx/sites-enabled/default mode: 0440 networks: - website_net environment: VIRTUAL_HOST: func.geschke.net LETSENCRYPT_HOST: func.geschke.net LETSENCRYPT_EMAIL: info@geschke.net configs: nginx_config_default: file: ./nginx/sites-enabled/default networks: website_net: driver: overlay external: true
Da der Nginx-Proxy-Container und der Nginx-Proxy-Companion bereits installiert waren, das Overlay-Netzwerk namens „website_net“ ebenfalls bereits existierte, sind keine weiteren Vorbereitungen notwendig. Bei der Docker-Compose-Datei handelt es sich um eine abgespeckte Version der Datei, die für den Betrieb von WordPress eingerichtet wurde, denn der komplette Stack besteht nur aus dem einzigen Dienst, und zwar Nginx.
Dieser Nginx will natürlich konfiguriert werden, dazu wird die Datei ./nginx/sites-enabled/default
genutzt und an geeigneter Stelle in den Docker-Container gemountet.
Die Nginx-Konfigurationsdatei ist sehr übersichtlich:
upstream 5537741186916234.eu-central-1.fc.aliyuncs.com { server 5537741186916234.eu-central-1.fc.aliyuncs.com:443; } # Default server configuration server { listen 80 default_server; listen [::]:80 default_server; root /var/www/html; # Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html; server_name _; location / { proxy_pass https://5537741186916234.eu-central-1.fc.aliyuncs.com; proxy_set_header X-Forwarded-Proto https; proxy_buffering off; fastcgi_param REMOTE_ADDR $http_x_real_ip; } }
Hier findet sich nur eine Konfiguration des Reverse-Proxy, d.h. jeder Request wird an die URL des Function Compute Servers geleitet. Die Pfadangabe wird dabei nicht verändert, sondern einfach direkt durchgereicht. Eine mögliche Erweiterung wäre ein Mapping eines kürzeren Pfades auf die entsprechende Funktions-URL, wie bei der Einrichtung der Custom Domains in der Alibaba Cloud-UI. Darauf habe ich hier jedoch zunächst verzichtet.
Das war es auch schon. Gestartet wird der Docker Stack mit folgendem Kommando:
geschke@anklam:~/services/func.geschke.net$ docker stack deploy -c website.yml funcgeschkenet
Der Erfolg sollte sich in der Liste der Stacks zeigen, einer davon der neue Stack namens funcgeschkenet:
geschke@anklam:~/services/func.geschke.net$ docker stack ls NAME SERVICES ORCHESTRATOR [...] funcgeschkenet 1 Swarm [...]
Hier wird nur ein Service benötigt, d.h. der Nginx-Container selbst. Daher ist anzunehmen, dass der Start erfolgreich war.
Danach sollte sich Function Compute melden, wenn man versucht, auf eine URL der neuen Subdomain zuzugreifen. Ebenfalls wird der Zugriff per https möglich und das von Lets’s Encrypt ausgestellte Zertifikat ist valide.
Die ersten Tests waren insofern auch erfolgreich, die Feedback-, aber auch die Test-Funktion konnten erreicht werden.
Nachhilfe für ReactPHP / Function Compute
Jedoch führte das Versenden der Formularinhalte zu einem Fehler – und zwar glaubte Google ReCaptcha nicht, dass man kein Roboter sei. Da ich auf diesen Seiten ReCaptcha v3 nutze, waren die Eingriffsmöglichkeiten auch beschränkt – Google glaubt einem -oder aber nicht. Bei genauerer Analyse stellte sich heraus, dass ReCaptcha ein Problem mit der IP-Adresse des Clients hatte. Das ist auch verständlich, denn nun wurden die Daten vom Proxy weiter geleitet, der als Client gegenüber dem Server auftritt und insofern eine andere IP-Adresse besitzt als der eigentliche Client, d.h. der Browser. Der ReCaptcha-Dienst nutzt natürlich die IP als eine der Merkmale, die korrekt sein müssen, um einen etwaigen Bot gegenüber einer menschlichen Person zu identifizieren. Wenn nun bereits die IP-Adresse des Clients nicht übereinstimmt, ist das erstmal schlecht.
Hier musste insofern PHP bzw. der Function Compute Code dazu gebracht werden, die IP des Clients anstatt des Proxy zu nutzen. Der Proxy gibt die IP-Adressen in den bekannten Variablen weiter, etwa in „X-Forwarded-For
“ – üblicherweise von PHP aus erreichbar im $_SERVER
-Array.
Nicht jedoch mit Function Compute oder vielmehr dem darunter liegenden ReactPHP. Während die IP-Adresse des Clients, die normalerweise mit „$_SERVER['REMOTE_ADDR']
“ erreichbar ist, bei Function Compute mit „$remoteIp = $request->getAttribute('clientIP');
“ ausgelesen werden kann, finden sich die nun benötigten Angaben in den Header-Daten. Mit einer kleinen Änderung ist somit die IP-Adresse des Clients wieder nutzbar:
$remoteIp = $request->getAttribute('clientIP'); $headers = $request->getHeaders(); if (isset($headers['X-Forwarded-For']) && $headers['X-Forwarded-For']) { $remoteIp = $headers['X-Forwarded-For']; }
Nachdem zunächst auf die zuvor verwendete Art und Weise die IP-Adresse des Clients bestimmt wurde, wird anschließend geprüft, ob die Header-Variable X-Forwarded-For
gesetzt ist. In diesem Fall wird die Client-IP-Adresse durch deren Inhalt ersetzt, denn dann ist davon auszugehen, dass ein Proxy verwendet wird und die „echte“ IP-Adresse sich in genau dieser Variable befindet. Danach kann die ReCaptcha-Prüfung mit der korrekten IP-Adresse des Clients durchgeführt werden.
Fazit
Eigentlich ist es schade, dass man auf derartige Lösungen zurück greifen muss, um das Ziel verschlüsselte Übertragung mit Custom Domains im Function Compute Service zu erreichen. Ich wäre auch gerne vollständig in der Cloud-Infrastruktur geblieben, doch war dies aus den genannten Gründen nicht möglich. Es bleibt zu hoffen, dass Alibaba Cloud vielleicht irgendwann Let’s-Encrypt-Zertifikate nativ unterstützt, so dass man sich derartige Umwege sparen kann. Andererseits verweisen sie selbst an verschiedenen Stellen in der Doku auf die Möglichkeit, Proxy-Dienste zu verwenden (z.B. für Floating IPs oder Custom Domains für OSS), sogar mit Hinweisen auf Nginx. Insofern scheint die Idee ja gar nicht so abwegig gewesen zu sein, wie ich im ersten Moment gedacht hatte. Wobei mir ein in der Cloud gehosteter Proxy-Dienst auch gefallen würde, natürlich mit Let’s-Encrypt-Unterstützung…