9
.
5
.
2017

Selbstskalierende Microservice Infrastruktur - PART II

Teil zwei mit Backend und stateless Authentication

EINLEITUNG

Im ersten Teil der BlogPost-Serie haben wir uns mit der Analyse, einem Migrationsplan und einem Proof of Concept beschäftigt. Dabei haben sich bereits fast alle unserer Anforderungen automatisch umgesetzt. Geblieben ist die Anforderung, dass das SeeVee-Backend mit Auto-Scaling und non-sticky Loadbalancing umgehen muss. Das ist der eine große Themenschwerpunkt von Teil II der BlogPost-Serie. Der andere Schwerpunkt ist die Aufteilung des SeeVee-Backends in mehrere kleine Microservices

Hier nochmal der Stand der Backendanforderungen zur Erinnerung.

bereits umgesetzte Backend Anforderungen
‍bereits umgesetzte Backend Anforderungen

Nachdem ich Teil I umgesetzt hatte, habe ich mir das Buch ‘Building Microservices - Designing Fine-Grained Systems’ geholt und mich hat bereits nach den ersten paar Seiten das Microservice Fieber gepackt. Das Backend erledigt ja so einige fachliche Aufgaben, die eigentlich in verschiedene Microservices aufgetrennt werden sollten (Separation of Concerns). Der Leitspruch aus dem Buch, der mich dabei geleitet hat war ‘Ein Microservice soll so klein sein, dass man ihn in zwei Wochen komplett neu schreiben könnte.’.

Nach intensivem Nachdenken darüber, wie man die Anwendung weiter in kleinere Teile aufteilen könnte, immer mit dem stateless Authentication Problem im Hinterkopf, kam ich zu folgendem Ergebnis:

  • SeeVee-Auth-Server - Microservice der stateless GitHub oAuth ermöglicht und ein eigenes JWT Token ausstellt, das zur stateless Authentication der Services mit dem Auth-Server dient.
  • SeeVee-Pdf-Converter - Microservice zur PDF Generierung mit definierter API und definiertem CV-Datenformat.
  • SeeVee-Backend - Microservice zur Bereitstellung der Datenbank via REST-API.
  • SeeVee-Frontend - Microservice zur Bereitstellung des User-Interface.

Was aus dem Backend nun alles rausfällt ist enorm. Bisher wurde einfach munter alles ins Backend gepackt was “serverseitig” war. Die PDF Generierung mit PhantomJS wird komplett ersetzt und ausgelagert, dazu aber später mehr. Auch das Image-Cropping und Image-Scaling mit Imagemagick wurde ins Frontend ausgelagert, dazu aber mehr in Teil III. Für jetzt reicht uns das Wissen, dass das SeeVee-Backend nichts mehr tut, außer die Datenbank über eine REST API bereitzustellen. Dadurch sind Anforderungen wie BAF-6 hinfällig.

Die neue SOLL-Infrastruktur sieht daher wie folgt aus. Und da mit Boxfuse die Umsetzung und Deploy-Chain so simpel sind, haben wir auch keinen besonderen Aufwand das umzusetzen. Einfach neues GitHub Repository, Boxfuse App und Jenkins Job anlegen und fertig.

SOLL-Infrastruktur mit Microservices
‍SOLL-Infrastruktur mit Microservices

 

ITERATION 3: STATELESS AUTHENTICATION MIT AUTH-SERVER MICROSERVICE

Authorization und Authentication innerhalb einer Microservice Architektur ist nicht gerade ein einfaches Thema. Es gibt viele Fallstricke und die technische Implementierung ist auch oft nicht trivial. Jeder kennt das klassische Basic Auth, oder cookie-session-based Authentication Strategien. Wenige kennen stateless Authentication mit JSON Web Token (JWT). Wie man das ganze mit Spring Security, dem GitHub oAuth und als Self Contained Microservice umsetzt ist unsere Aufgabe.

Hier mal der gesamte stateless JWT-based Authentication-Workflow als interaktive SlideShow Infografik. Sie können unten links an der Grafik die PausePlay und Next/Prev Buttons nutzen, um sich gewisse Folien genauer anzusehen.

svg:https://media.comsysto.com/images/2017-01-23-boxfuse-part-2/seevee-stateless-jwt-auth--webfont.svg

Ich will jetzt hier nicht groß ins Detail zu JWT gehen, da es dazu bereits unendlich viele BlogPosts gibt. Wir halten einfach nur fest, dass wir mit JWT folgendes erreichen:

  • Auth-Server führt GitHub oAuth durch und stellt nach Erfolg ein eigenes JWT Token aus.
  • Stateless Authentication aller Microservices am Auth-Server via Token.
  • Token expired nach 8 Stunden.
  • Token refresh nicht möglich.
  • Token ist nicht verschlüsselt. Jeder kann Payload lesen (Base64 decode).
  • Token ist signiert. Nur Auth-Server kennt secret und kann es validieren.
  • Token wird gegen ausstellenden Auth-Server validiert.
  • Token enthält alle GitHub Informationen, die wichtig sind (Username, User-ID, Orga-IDs).
  • Jeder Request an einen API bereitstellenden Microservice wird über eine Token-Validierung abgesichert.

So habe ich es umgesetzt. Und mir ist bewusst, man kann das in vielfältiger Weise auch anders umsetzen, aber so ist es für uns optimal. Auch wenn ich nicht groß ausschweifen wollte, möchte ich an dieser Stelle aber nochmal den Grund für die Stateless-Authentication anhand eines Beispiels nennen.

© icecreamsandwichcomics.com
‍© icecreamsandwichcomics.com

Wie schon der Comic illustriert, braucht jede Webseite einen Mechanismus zur eindeutigen Identifikation eines wiederkehrenden Benutzers. Gerade auf Seiten, in die man sich einloggt ist das zwingend nötig. Bisher haben wir auch bei SeeVee eine Cookie-Based Authentication Strategy genutzt. Das bedeutet, dass nach dem Login des Benutzers im Server eine Session eröffnet wird und die Session-ID in einem Cookie im Browser des Besuchers gespeichert wird. Ruft der Besucher nun einen weiteren Link innerhalb der Seite auf, kann der Server anhand des Cookies und der darin enthaltenen Session-ID den Besucher einer Session zuordnen.

Doch was passiert, wenn wir plötzlich 100 Server mit non-sticky Loadbalacing haben? Die Antwort ist simpel. User logt sich ein und wird dabei zufällig auf Server 23 geroutet. Dieser erzeugt eine Session und setzt das Session Cookie. Der Besucher klickt nun einen weiteren Link. Der Loadbalancer routet diesmal auf Server 42. Dieser Server holt sich die Session ID aus dem Cookie und muss feststellen. Keine Session für diese Session ID verfügbar oder noch schlimmer die Session ID ist verfügbar, gehört aber zu einem anderen Nutzer. Gut. Ich denke es ist klar, warum das nicht geht. Warum dann nicht sticky Loadbalancing?. Gute Frage. Es ist ein Konzept der Architektur, erst gar keine Session zu haben. Sprich, ein Server führt keine “Liste” darüber wer gerade alles eingeloggt ist. Sondern er soll einfach “stateless” alles abarbeiten was gerade anfällt. Serverseitige Sessions haben auch wiederum ihre Tücken und beispielsweise beim Atlassian JIRA Data Center wird sticky-session Loadbalancing eingesetzt, was meiner Meinung nach mehr Probleme macht, als es löst. In der Regel macht man das nur, wenn die Architektur der Anwendung so komplex ist, dass man sich nicht traut auf statelessness umzustellen.

Kurz zusammengefasst: Haben wir stateless Authentication, kann der Loadbalancer uns zu einem beliebigen Server der 100 Server leiten und wir werden dort korrekt zugeordnet. Das geschieht indem wir bei jedem Request unser JWT Token als HTTP Header mitsenden.

Während des Schreibens wurde ich darauf hingewiesen, dass JWT auch seine ‘Nachteile’ hat. Das möchte ich an dieser Stelle nicht verschweigen, halte diese sogenannten Nachteile aber als hinnehm- und vernachlässigbar.

  • Ein JWT Token lässt sich nicht invalidieren.
  • Ein JWT Token kann nur expiren.
  • Jeder Request an einen Microservice enthält das JWT Token im HTTP Header und löst im Hintergrund einen Request zur Tokenvalidierung an den Auth-Server aus.
  • Viele Validierungsrequests. Erhöhte Request Größe, wegen JWT Token im HTTP Header.

Mir wurde gesagt, dass das dann zu viele Requests wären. Doch ich bin da anderer Meinung. Die gewonnene Freiheit der losen Kopplung zwischen den Microservices überwiegt hier die paar Requests mehr. Gerade auch deswegen, weil die Microservices alle zusammen in einem Netzwerk deployed sind sollte man diese Validierungs-Requests nicht überbewerten. Es ist ja nicht so, dass wir dabei von einer enfternte API Megabytes an Daten ziehen würden. Es wird lediglich der Token an den Auth-Server geschickt und der Antwortet entweder mit HTTP 200 oder 403 Forbidden. Gerade durch den Einsatz des Auth-Servers, der zum Zeitpunkt des GitHub oAuths gleich auch noch die GitHub API nach GitHub Organizations des User abfragt und diese im JWT Token als Payload speichert, sparen wir uns erhebliche Requests an die GitHub API. Wir fragen die GitHub API nämlich nur einmal zum Zeitpunkt der Token Ausstellung. Alle weiteren Token Validierungs-Requests geschehen komplett innerhalb unserer Microservice Infrastruktur und sind daher extrem schnell abgearbeitet.

Die technische Umsetzung sprengt hier den Rahmen, ist aber eher trivial. Spring Security muss man nur ein wenig dazu ‘zwingen’ keine Session zu erzeugen. Dann mit einem Request-Filter das Token herausfiltern, Token gegen den Auth-Server validieren und die Token-Payload innerhalb der Controller verfügbar machen. Bei einem invaliden Token, wird bereits im Spring Security Filter die Response mit “HTTP 403 Forbidden” ausgegeben.

Hier nur mal ausschnittsweise die SecurityConfig und der TokenAuthenticationService der SeeVee-Backend Spring Boot Anwendung.

github:c263a42907461e1132147f580b7d8b09

 

github:7cf965d4c68e104c4bb992bf24c86e19

Die UserAuthentication steht nun in den Controllern zur Verfügung und man hat Zugriff auf den JWT Payload. So kann man bspw. die GitHub-User-ID aus dem Payload nutzen, um den Besucher eindeutig einem Datenbank-Benutzer zuzuordnen.

github:0d244b17d33c93c9d8d59ee6eb7c1cf5

Wir können somit auf Sessions verzichten und haben durch das JWT Token trotzdem alle Informationen im Controller zur Verfügung, die wir brauchen.

 

 

ITERATION 4: EIGENER MICROSERVICE FÜR DIE PDF-GENERIERUNG

Nachdem wir nun einen funktionierenden Auth-Server Microservice haben, können wir uns an den SeeVee-Pdf-Converter heranwagen. Bisher war die PDF Generierung Teil des SeeVee-Backends. Mittels PhantomJS wurde eine HTML-Seite mit CSS Code und Base64-Image-URIs in ein PDF konvertiert. Damit gab es schon von Beginn an folgende Probleme:

  • Layout, Flusskontrolle und Genauigkeit: HTML wurde im Frontend via React generiert. Fußzeile und Platzierung der Elemente nicht optimal für print.
  • Größe der Binary: Mit satten 70MB schlug PhantomJS zu buche.
  • Security: Auf seinem Server eine Binary mit HTML-Quellcode zu füttern birgt immer ein potentielles Sicherheitsrisiko.

 

Das neue Konzept der PDF Generierung mit PDFKit soll nun diese Probleme ausmerzen.

  • Plain-NodeJS PDF Generierung ohne externe Abhängigkeiten.
  • Generierung des PDF direkt aus dem CV-JSON heraus (Datenformat).
  • Keine HTML/CSS Konvertierung mehr.
  • Saubere Flußkontrolle, also gezielte PageBreaks vor und nach großen Textblöcken möglich.
  • Kopf- und Fußzeilen mit Seitenzahlen möglich und millimetergenau setzbar.
  • Eigenständiger (self-contained) Microservice für die PDF Generierung

Bisher konnte jede CSS oder HTML Layout Änderung am SeeVee-Frontend potentiell das Ergebnis der PDF Generierung zerstören. Diese Abhängigkeiten zwischen HTML Ansicht des Lebenslaufes und der PDF Ansicht sind nun zu 100% getrennt und das gemeinsame Bindeglied ist das CV-JSON Datenformat. Wir schaffen eine genormte REST-API und können nun die PDF Generierung auch völlig unabhängig vom SeeVee-Frontend entwickeln und testen. Dazu habe ich mir bspw. ein simples CURL Kommando gebastelt, welches eine JSON-Datei an den Konvertierungs Endpoint sendet und mir BASE64 encodiert das PDF zurückgibt. Dazu sende ich die test_cv_request.json and den Pdf-converter und speichere das Ergebnis als response.pdf.

github:e88733b990893e109560fa57bd31c171

 

github:9a6f7df270a25a617d6b880d302900e8
PDF Vorschau
‍PDF Vorschau

Dadurch konnte ich den Microservice gezielt ohne externe Abhängikeiten entwickeln und muss mir über Seiteneffekte keine sorgen mehr machen. Jetzt kann das PDF nur noch zersört werden, wenn jemand gezielt Quellcode im SeeVee-PDF-Converter “kaputt programmiert”. Doch dann gibts vom Continuous-Integration-System eins auf die Nuss!

Natürlich kostete es einiges an Nerven sich mit dem neuen Framework PDFKit auseinanderzusetzen und man fühlte sich erinnert an TurboPascal zeiten, denn man muss mit X- und Y-Koordinaten die Elemente setzen, deren Höhe berechnen und anschließend die nachvolgenden Elemente relativ dazu positionieren. Aber nach kurzer Zeit hatte man auch hier den Dreh raus. Schön an so einer Pixelgenauen Methode ist wie bereits angesprochen die Flusskontrolle des Dokuments. So konnte ich immer anhand des offsetY und der pageHeight entscheiden, ob noch Platz für das folgende Element ist, oder ob ich lieber einen Seitenumbruch auslöse. Diese Entscheidungen kann man nun aktiv treffen und muss nicht wie bisher auf magische CSS Anweisungen wie ‘break-page: before’ vertrauen, die letztendlich bei komplexen verschachtelten Elementen nie das gewünschte Resultat erzielt haben. Auch wenn es anfangs umständlich erschien, bin ich von PDFKit sehr begeistert. Warum PDFKit nicht im Browser das PDF Generieren lassen? Das war zu Anfangs auch meine Idee, aber folgendes spricht dagegen:

  • Buildchain: PDFKit hat eine komische Buildchain, die auf Browserify und mocks für require(‘fs’) aufbaut und das war nicht mit Webpack lauffähig zu bekommen.
  • Fonts: Wir wollen mehrere Fonts in die PDFs einbetten, und diese haben eine gewisse größe, da das TTF verwendet werden muss.
  • Stilelemente: Wir verwenden gewisse SVG-Pfade und sonstige Bild-Elemente die der Browser laden müsste, was serverseitig keine Problem ist.
  • Größe: PDFKit schlägt in der minified version mit 2MB zu buche und das ist für die Ladezeit der Seite zu lang.
  • Separation of Concerns: Der wohl wichtigste Punkt. Wir wollten gezielt SeeVee-Frontend und die PDF-Generierung trennen.
  • Sinnhaftigkeit: Es macht kaum Sinn, das Frontend alle Daten aus dem Backend laden zu lassen und dann an den Pdf-Converter zu schicken. Daher gibt es einen simplen Pdf-Download-GET-Endpoint im Backend der alle Daten aus der Datenbank holt (Portraits, CV-JSON, Company-Logo usw.) und direkt an den Pdf-Converter schickt. Die Antwort wird BASE64 decodiert und in den Response Stream geschrieben, sodass der Browser direkt das PDF herunterlädt.

Ich bin von der serverseitigen PDF Generierung und Performance in unserem Fall sehr überzeugt.

 

Wir sind also nun mit allem fertig, was wir im Backend umsetzen wollten. Auch unsere Infrastruktur Anforderungen sind alle erfüllt.

Umgesetzte Backend Anforderungen
‍Umgesetzte Backend Anforderungen

 

 

FAZIT

Wir haben das SeeVee-Backend nach dem Single-Responsibility-Prinzip weiter in kleinere Microservices zerteilt. Jeder Microservice erfüllt dabei genau eine Aufgabe. Zwischen den Microservices bestehen definierte API Schnittstellen mit definierten Datenformaten. Die Seiteneffekte, die bei Code-Änderungen an großen monolithischen Anwendungen auftreten können, sind dadurch minimiert worden. Auch kann nun bspw. ein Team den SeeVee-Pdf-Converter Microservice unabhängig vom Frontend oder Backend weiterentwickeln. Die loose Kooplung ist gelungen und auch wenn mich das heute zufriedenstellt, so wird man unter guter Microservice Architektur in einem Jahr bestimmt schon wieder etwas anderes verstehen. Doch auch dann wird gelten, dass wir unsere Microservices binnen zwei Wochen neu implementieren könnten.

Damit Sie auch die weiteren Teile der BlogPost-Serie nicht verpassen, melden Sie sich am besten zu unserem Newsletter an - scrollen Sie dazu ganz nach unten und tragen Sie sich in das entsprechende Formular ein.

Wenn auch Sie Ihre Dienste mit Boxfuse betreiben wollen und alle Vorteile von unveränderbarer Infrastruktur nutzen wollen, untersützen wir Sie gerne. Wir können in Workshops oder projektbegleitendem Coaching Ihre Mitarbeiter im Umgang mit Boxfuse schulen oder auch gemeinsam mit Ihnen Ihre Infrastruktur migrieren. Mehr Details finden Sie unter Architektur & Entwicklung. Wir freuen uns über eine Kontaktaufnahme, gerne auch ganz formlos über das unten angezeigte Kontaktformular.