Über unsMediaKontaktImpressum
Rico Fritzsche 12. September 2017

Microservices mit node.js

Microservices sind in erster Linie die Idee, Software zu modularisieren. © gudrin / Fotolia.com
© gudrin / Fotolia.com

Aktuell gibt es kaum ein Thema, über welches mehr gesprochen und geschrieben wird als über Microservices. Aber was steckt eigentlich hinter der Idee von Microservices? Haben wir mit diesen endlich den richtigen Architekturansatz gefunden, um komplexe Softwaresysteme zu beherrschen? Dieser Artikel soll neben einer kurzen Einführung anhand eines praktischen Beispiels mit node.js und Docker zeigen, warum uns dieser Ansatz helfen kann, die Komplexität von Software zu beherrschen.

Was sind Microservices eigentlich?

Gehen wir zuerst der Frage nach, was Microservices eigentlich sind. In erster Linie sind Microservices die Idee, Software zu modularisieren. Das ist an sich nichts wirklich Neues. Dafür gibt es bereits unterschiedliche Ansätze wie Klassen, Bibliotheken, Pakete usw.

Die Idee der Modularisierung von Softwaresystemen wird unter anderem auch ausführlich von Eric J. Evans [1] beschrieben, der sich zum Ziel setzt, Komplexität von Software zu reduzieren bzw. in den Griff zu bekommen. Die Ideen von "Bounded Context" und "Context Map" sind auch für die Erarbeitung von Microservices interessant.

In vielen Softwareprojekten, an denen ich in der Vergangenheit mitgearbeitet habe, sind wir diesen Prinzipien und der Idee der Modularisierung gefolgt und haben die Software auf unterschiedlichen Ebenen modular aufgebaut, in dem zum Beispiel eine Klasse auch nur eine Zuständigkeit hatte und unterschiedliche fachliche Belange in unterschiedliche Bibliotheken gegliedert wurden. Es steht außer Frage, dass es auf diese Art möglich ist, Software gut zu strukturieren, den Überblick zu behalten und wartbare Systeme zu entwickeln.

Was ist aber nun bei Microservices anders? Eine entscheidende Idee von Microservices ist das unabhängige Deployment von Modulen. Ein Microservice – und damit eine fachliche Einheit – muss deploybar sein, ohne dass andere Microservices betroffen sind und ebenfalls deployed werden müssen. Es ist demnach anders als bei klassischen Projekten nicht erforderlich, bei jeder kleinen Änderung das gesamte Projekt zu deployen.

Ein weiterer Unterschied zu klassischen Systemen ist, dass jedes Modul einzeln nach Bedarf skaliert werden kann. Bei klassischen Deployment-Monolithen ist dies nicht möglich. Da kann logischerweise nur das gesamte System skaliert werden. Da Microservices komplett unabhängig voneinander sind und isoliert voneinander laufen, ist es nicht notwendig, sich für alle Zeit auf eine bestimmte Technologie festzulegen. Es kann demnach von Fall zu Fall zu entschieden werden, welche Technologie für einen Microservice verwendet wird. Das kann von unterschiedlichen Bedingungen abhängig sein, wie beispielsweise verfügbare Ressourcen. In größeren Projekten entwickeln unterschiedliche Teams unabhängig voneinander. Jedes Team kann grundsätzlich Technologien selbst wählen, die für die Erfüllung der fachlichen Anforderungen optimal sind.

Fassen wir kurz zusammen, was Microservices sind:

  • Einheiten bzw. Module, welche gemeinsam mit anderen eine Gesamtapplikation ergeben,
  • jeder Teil der Applikation ist unabhängig deploybar und skalierbar und
  • Microservices können durch unterschiedliche Teams in unterschiedlichen Sprachen/ Technologien entwickelt bzw. getestet werden.

Vorteile von Microservices:

  • sie beschleunigen die Entwicklung, das Deployment und erhöhen die Produktivität,
  • jeder Service kann unabhängig von anderen ausgerollt werden,
  • bei der Entwicklung neuer Microservices können aktuelle Technologien verwendet werden, weil man sich nicht einmalig auf einen Technologie-Stack festlegen muss und
  • es existiert eine klare Trennung von Zuständigkeiten und Verantwortlichkeiten, dadurch bessere Strukturierung des Codes.

Aber gibt es auch Nachteile? Aus meiner Sicht gibt es die insofern, dass Entwickler gezwungen sind, sich mit der Thematik von verteilten Anwendungen auseinander zu setzen. Die Komplexität von Anwendungen wird gewissermaßen in Richtung Deployment verschoben. 

Was ist mit dem User Interface?

Abb.1: Beispiel Single Page Application (SPA) und Microservices. © Rico Fritzsche
Abb.1: Beispiel Single Page Application (SPA) und Microservices. © Rico Fritzsche

In der Praxis bin ich schon häufig auf die Frage gestoßen, ob das User Interface oder Frontend Teil des Dienstes ist. Oder bietet der Microservice nur eine API an? Meiner Auffassung nach, sollte ein Microservice einen fachlichen Aspekt komplett abbilden und demnach auch das User Interface enthalten. Würde er dies nicht tun, dann wäre es beispielsweise nicht möglich, dass ein fachlicher Kontext in der Verantwortung eines Teams liegen könnte. Ein übergeordnetes UI-Team wäre notwendig, welches am Ende wieder einen Monolithen erzeugen würde, siehe Abb.1. Änderungen an einem Microservice ziehen zwangsläufig auch Änderungen in der UI-Komponente nach sich, so dass autonome Änderungen und Deployment eines Microservices nicht mehr möglich sind. Ich halte dieses Vorgehen für weniger optimal, wenn es sich um Webanwendungen handelt. Baut man eine App, ist es zwangsläufig erforderlich, die UI-Komponente als SPA zu entwerfen.

Die Lösung – Self-Contained Systems (SCS)

Abb.2: Self-Contained System. © Rico Fritzsche
Abb.2: Self-Contained System. © Rico Fritzsche

Self-Contained Systems sind immer Web-Applikationen [2]. Sie besitzen keine geteilte UI-Komponente, da jeder Service sein Web-Interface ausliefert und alle erforderlichen Komponenten enthält – also neben UI auch Logik und Datenhaltung. Eine API wird optional angeboten. Somit ist jedes Team in der Lage, einen Microservice autonom zu entwickeln.

Damit die einzelnen Microservices für den Nutzer als eine Anwendung erscheinen, ist ein API-Gateway erforderlich. Je nachdem, welche Repräsentation der Ressource durch den Aufrufer angefordert wird, liefert das API-Gateway beispielsweise HTML oder JSON aus. HTTP bietet uns bereits einen Mechanismus dafür an – Content Negotiation.

IT-Tage 2017 - Microservices

Beispiel in node.js

Abb.3: Fachliche Kontexte und erforderliche Module. © Rico Fritzsche
Abb.3: Fachliche Kontexte und erforderliche Module. © Rico Fritzsche

Im folgenden Beispiel wird gezeigt, wie einzelne Microservices über ein API-Gateway nach außen als eine Anwendung agieren. Für das Deployment der Microservices werden Docker-Container genutzt. Fachlich geht es darum, eine Anwendung bereitzustellen, die es ermöglicht, CrossFit Workouts – sogenannte WODs – zu erfassen und Übungen zu verwalten. In der fachlichen Analyse wurde ermittelt, dass folgende fachliche Kontexte existieren und folgende Module erforderlich sind, siehe auch Abb.3:

  • Exercises Service (Erfassung und Bereitstellung von Übungen)
  • Workouts Service (Erfassung und Bereitstellung von Standard Workouts)
  • User Workouts Service (ermöglicht die Erfassung von individuellen WODs)
  • Profile Service (Verwaltung des eigenen Profils).

Jeder Microservice stellt eine REST-API zur Verfügung. Über Content Negotiation kann der Aufrufer entscheiden, welche Repräsentation der Ressource abgerufen wird.

Exemplarischer Aufbau eines Microservices und Node-Module

In Abb.4 sind Aufbau und Struktur eines Microservices zu sehen. 

Jeder Service enthält ein Docker-File, welches das automatisierte Deployment als Docker-Container ermöglicht (s.Abb.5).

Abb.4: Aufbau und Struktur eines Microservices. © Rico Fritzsche
Abb.4: Aufbau und Struktur eines Microservices. © Rico Fritzsche
Abb.5: Docker-File, welches das automatisierte Deployment als Docker-Container ermöglicht. © Rico Fritzsche
Abb.5: Docker-File, welches das automatisierte Deployment als Docker-Container ermöglicht. © Rico Fritzsche

Schauen wir uns nun die API-Implementierung des Services an (s.Abb.6).

Exercises.js im Verzeichnis api registriert die Routen des Exercises Microservices. Dieser stellt eine GET- und eine POST-Methode zur Verfügung. Die GET-Methode nutzt zusätzlich das node-Modul express-negotiate, welches dafür sorgt, dass je nach Content-Type HTML oder JSON ausgeliefert wird.

Rufen wir die Route im Browser auf, erhalten wir HTML, s.Abb.7.

Abb.6: API-Implementierung des Services. © Rico Fritzsche
Abb.6: API-Implementierung des Services. © Rico Fritzsche
Abb.7: Aufruf der Route im Browser – HTML. © Rico Fritzsche
Abb.7: Aufruf der Route im Browser – HTML. © Rico Fritzsche

Rufen wir mit Curl über den Terminal die Route mit dem Accept-Header application/json auf, so erhalten wir JSON als Ergebnis.

curl -H "Accept: application/json" http:// localhost:3000/
Abb.8: Aufruf von application/json. © Nico Fritzsche
Abb.8: Aufruf von application/json. © Nico Fritzsche

Die Implementierung für Content-Negotiation ist mit Node.js/ Express sehr einfach. Im Grunde muss nur der Request auf den Headereintrag geprüft werden. Dies geschieht mit folgendem Code:

req.negotiate(req.params.format, {
          'application/json': () => {
            res.status(status.OK).json(exercises);
        },'html': () => {
          res.render('index', { title: 'CrossFit Excercises', exercises: exercises });
        }
      });

Ist der Accept-Header html, so wird eine View ausgeliefert. Im Beispiel haben wir die View-Engine Pug verwendet. Die anderen Services sind analog implementiert, könnten in der Praxis aber auch vollkommen anders implementiert sein und wie oben erwähnt auch andere Technologien nutzen.

Das API-Gateway

Das API-Gateway ist dafür verantwortlich, dass die einzelnen Microservices nach außen als eine Anwendung repräsentiert werden. Natürlich möchten wir auch hier einen möglichst hohen Grad an Automatisierung. Aus diesem Grunde hört das Gateway, welche Docker-Container gestartet sind und registriert diese am Gateway. Wird ein Container und damit ein Microservice abgeschaltet, so wird die Route automatisch entfernt, s.Abb.9.

Abb.9: Automatische Entfernung der Route. © Rico Fritzsche
Abb.9: Automatische Entfernung der Route. © Rico Fritzsche

Die Implementierung des Abhörens der Docker-Container erfolgt im Beispiel mit dem Node-Module node-docker-monitor. Dieses stellt die Listener onContainerUp und onContainerDown zur Verfügung.

Mit dem Befehl docker ps können alle laufenden Container aufgelistet werden. In Abb.10 ist zu sehen, dass alle Dienste laufen. 

Abb.10: Alle Dienste laufen. © Rico Fritzsche
Abb.10: Alle Dienste laufen. © Rico Fritzsche

Schalten wir nun einen Microservice mit dem Befehl docker stop exercises-service ab, wird die Route exercises am Gateway entfernt. Fahren wir den Dienst mit dem Befehl docker restart exercises-service wieder hoch, so wird die Route exercises wieder registriert, wie in Abb.11 zu sehen.

Abb.11: Die Route exercises wird wieder registriert. © Rico Fritzsche
Abb.11: Die Route exercises wird wieder registriert. © Rico Fritzsche

Fazit

Die Implementierung von Microservices bzw. Self-Contained Systems ist mit node.js sehr gut möglich. Um wirklich von einander unabhängige Microservices zu implementieren, empfiehlt es sich, dass jeder Service eine API anbietet, die in der Lage ist, UI auszuliefern. Nur so kann gewährleistet werden, dass ein Team die komplette Verantwortung für einen fachlichen Kontext übernehmen kann und der Dienst unabhängig von anderen ausgerollt werden kann.

Das node.js-Projekt muss so angelegt werden, dass es unabhängig von anderen ausgeführt werden kann. Es enthält alle für den Microservice erforderlichen Komponenten. Das Dockerfile sorgt dafür, dass die Verteilung in unterschiedlichen Stages mit den exakt selben Abhängigkeiten automatisiert möglich ist.

Quellen
  1. Eric J. Evans, 2003: Domain-Driven Design: Tackling Complexity in the Heart of Software, Addison Wesley
  2. Self-contained System (SCS)
nach Oben
Autor

Rico Fritzsche

Rico ist freiberuflicher Softwarearchitekt und Webentwickler. Er beschäftigt sich seit vielen Jahren intensiv mit Themen wie Domain-Driven Design, Microservices, verteilten Architekturen und Cloudlösungen.
>> Weiterlesen
botMessage_toctoc_comments_929