Zwölf Regeln für die perfekte (Micro-)Services-Architektur

Services versprechen eine einfache, zielgerichtetete und fachlich adäquate Softwareentwicklung. Doch worauf sollte man bei ihrem Bau achten?

In Pocket speichern vorlesen Druckansicht 13 Kommentare lesen
Puzzleteile, die teilweise zusammen und teilweise lose sind

(Bild: Shutterstock.com/Kenishirotie)

Lesezeit: 15 Min.
Von
  • Golo Roden
Inhaltsverzeichnis

Häufig wird Entwicklerinnen und Entwicklern empfohlen, auf Services als Architektur zu setzen. Doch ist es gar nicht so einfach, Services derart zu bauen, dass sie nachher gleichermaßen schlank und skalierbar sind. Kommen dann Zweifel am eigenen Vorgehen hinzu, wird die Beschäftigung mit Services schnell zum Bremsklotz. Vor vielen Jahren gab es von Heroku für Cloud-Native-Anwendungen die Empfehlung der 12-Factor-Apps: zwölf Regeln, die sich – laut Heroku – als tragfähige Basis erwiesen haben. Doch teilweise sind diese Regeln in die Jahre gekommen, außerdem wurden sie nie explizit für Services gemacht.

the next big thing – Golo Roden

Golo Roden ist Gründer und CTO von the native web GmbH. Er beschäftigt sich mit der Konzeption und Entwicklung von Web- und Cloud-Anwendungen sowie -APIs, mit einem Schwerpunkt auf Event-getriebenen und Service-basierten verteilten Architekturen. Sein Leitsatz lautet, dass Softwareentwicklung kein Selbstzweck ist, sondern immer einer zugrundeliegenden Fachlichkeit folgen muss.

Was liegt also näher, als das Ganze aufzugreifen, mit der eigenen Erfahrung der vergangenen Jahre im Bereich der Konzeption und Entwicklung von Web- und Cloud-Services zu kombinieren, und zwölf aktualisierte Regeln aufzustellen? Genau das versuche ich in diesem Blogpost.

Eines sage ich jedoch direkt vorneweg: Die Auswahl der Regeln ist natürlich (wie auch damals bei Heroku) subjektiv und gefärbt. Das, was für uns bei der the native web GmbH gut funktioniert, muss nicht für alle gut funktionieren. Wie es im Englischen so schön heißt: Your mileage may vary (YMMV).

Die erste Regel wird vermutlich häufig belächelt, dennoch halte ich sie für die wichtigste schlechthin. Und auch, wenn viele sagen werden "das ist doch logisch, das braucht man doch nicht extra zu erwähnen", zeigt mir die Praxis, dass sie in 99 von 100 Fällen ignoriert wird. Die erste Regel lautet nämlich: Vor dem Entwickeln muss klar sein, was das fachliche Problem ist, das der Anwendung zugrunde liegt. Wer das nicht beantworten kann, wird nicht in der Lage sein, eine zielgerichtete und fachlich adäquate Lösung zu entwickeln, und zwar völlig unabhängig von der eingesetzten Technologie.

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.

Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.

Daher: Man sollte als allererstes ein robustes, fundiertes und detailliertes fachliches Verständnis der entsprechenden Domäne aufbauen. Es ist dabei völlig zweitrangig, ob man dafür Domain-Driven Design (DDD) oder sonst irgendeine Methodik verwendet, aber man muss gedanklich von CRUD (Create, Read, Update, Delete) wegkommen, ansonsten baut man bestenfalls "Forms over Data", trifft aber nicht den eigentlichen Kern der Sache. CRUD ist ein Anti-Pattern, das es zu vermeiden gilt.

Die zweite Regel besagt, dass ein Service stets genau einem Betriebssystemprozess entspricht. Mit anderen Worten: Ein einzelner Service besteht nicht aus zig unterschiedlichen Programmen, sondern ein Service besteht aus einer Codebase für einen Prozess in einem Git-Repository. Wenn von diesem Service mehrere Instanzen gestartet werden sollen, dann laufen natürlich mehrere Prozesse, aber pro Instanz gibt es jeweils nur genau einen Betriebssystemprozess.

Das heißt auch, dass man nicht mehrere Services in einem gemeinsamen Git-Repository unterbringt, sondern dass jeder Service über ein eigenes Repository verfügt. Der Grund dafür ist einfach: Nur auf dem Weg ist sichergestellt, dass jeder Service unabhängig gepflegt, versioniert, dokumentiert, deployed, … werden kann. Außerdem ermöglicht das Zuordnen von Services zu Repositories ein feingranulares Verwalten der Berechtigungen auf den Code der jeweiligen Services.

Die dritte Regel fordert, dass es für das Bauen eines Services ein Skript gibt, das überall lauffähig ist. Die Technologie ist dabei völlig zweitrangig, theoretisch genügt ein einfaches Makefile. Der springende Punkt ist: Um vom Quellcode zum ausführbaren Binary zu kommen, darf nicht mehr Aufwand erforderlich sein, als einen einzigen Befehl auszuführen. Dieser Befehl kompiliert und linkt dabei nicht nur, sondern führt auch die Tests aus, die Codeanalyse und alles Weitere, was um den Build herum noch existiert.

Wichtig ist, dass dieses Skript in identischer Form auf jedem Rechner im Team lauffähig ist, das heißt, jede Entwicklerin und jeder Entwickler im Team kann eigenständig lokal bauen, und das identische Skript wird auch im Build-Schritt der CI/CD-Pipeline verwendet. Das Skript muss (abgesehen vom Einrichten gegebenenfalls erforderlicher Berechtigungen) erfolgreich ausführbar sein, nachdem man das Repository geklont und die Abhängigkeiten installiert hat. Alles, was darüber hinausgeht, ist unnötiger manueller Aufwand.

Was in den Build mit hineinspielt, ist die vierte Regel, die sich mit einer passenden Branching-Strategie beschäftigt: Konkret sieht diese Strategie so aus, dass es einen main-Branch gibt, der per definitionem als stabil angesehen wird. Jegliche Entwicklung, sei es das Hinzufügen neuer Features, das Beheben von Bugs oder sonst etwas, passiert in gesonderten Branches, die von main abzweigen und nachher wieder nach main gemerged werden (das entspricht dem klassischen Ansatz von Feature-Branches).

Der einzige Weg, einen solchen Merge nach main durchzuführen, ist per Pull Request, bei dem auch wieder sämtliche Tests, Codeanalyse & Co. laufen. Außerdem muss jeder Pull Request, bevor er nach main gemerged werden kann, gründlich gereviewed werden. Außer dem stabilen Produktions-Branch main gibt es ansonsten also ausschließlich kurzlebige Branches, die sich um main herum ranken. Und jedes Mal, wenn ein Branch nach main gemerged wird, wird dieser gesquashed, und auf der neuen Basis von main ein neues Release des Services mithilfe von Semantic Versioning gebaut.

Die fünfte Regel besagt, dass jeder durch einen Pull Request erzeugte Commit auf main nicht nur eine neue Version erzeugt, sondern auch ein neues Docker-Image, das ebenfalls versioniert ist. Auf dem Weg lässt sich jeder x-beliebige Stand des Services jederzeit im Nachhinein anhand seiner Versionsnummer entweder als Binary oder als komplettes Docker-Image herunterladen und ausführen.

Wichtig dabei ist, dass der Service zügig startet, weshalb auch im Docker-Image nicht mehr gebaut werden darf. Stattdessen muss das alles bereits vorher erfolgt sein. Hier bieten sich Multi-Stage-Builds an, um das finale Produktiv-Image möglichst sauber und klein zu halten. Da Container damit rechnen müssen, jederzeit abgeschossen zu werden, darf es keine speziellen Anforderungen an einen Graceful-Shutdown des Dienstes geben.

Nachdem sich die vorangegangenen Punkte im Wesentlichen auf die Infrastruktur für Services bezogen haben, kommen wir nun zum Inhaltlichen. Jeder Service verfügt über eine API, über die er von außen angesprochen werden kann. Standardmäßig handelt es sich dabei um eine ganz klassische HTTP-API, also kein GRPC, kein GraphQL, ja noch nicht einmal HTTPS. Der Grund dafür ist einfach: Solange man auf einfaches, unverschlüsseltes HTTP setzt, lässt sich die API einfach testen und debuggen. Außerdem ist HTTP kompatibel zu praktisch jeder Technologie und Plattform, man braucht keinen speziellen Client (theoretisch genügt sogar curl), und man spart sich jeglichen Aufwand mit Zertifikaten und Co.

Diese HTTP-API besteht dabei aus drei Teilen:

  • Zum einen gibt es die fachlichen Commands. Das sind POST-Routen, die irgendeine fachliche Veränderung im Service bewirken.
  • Zweitens gibt es die fachlichen Queries. Das sind GET-Routen, die Daten aus dem Service abfragen und zurückgeben.
  • Drittens gibt es noch ein paar technische Routen, allen voran eine Ping- und eine Health-Route, um zu ermitteln, ob der Service überhaupt läuft und, wenn ja, wie sein Zustand ist.

Wem Commands und Queries als Begriffe nicht viel sagen, schaut sich das Design-Pattern CQRS an. Dort schließt sich auch der Kreis zu der ersten Regel, die fordert, die Fachlichkeit und nicht CRUD in den Vordergrund zu rücken.