Microservices im Zusammenspiel mit Continuous Delivery, Teil 1 – die Theorie

Architektur/Methoden  –  2 Kommentare

Durch Continuous Delivery wird Software viel öfter und zuverlässiger in Produktion gebracht. Wesentliches Werkzeug dafür ist eine Continuous Delivery Pipeline. Auf den ersten Blick scheint dazu nur eine Optimierung der Prozesse nötig, aber moderne Architekturansätze wie Microservices spielen ideal mit Continuous Delivery zusammen.

Zentrum von Continuous Delivery [1] [2] ist ein Deployment-Prozess, wie ihn unten stehende Abbildung zeigt: Die Releases werden in einer Pipeline immer weiter propagiert. Letztere besteht aus einer Reihe von Stadien. Während der anfänglichen Commit-Phase kompiliert das System die Software, führt Unit-Tests aus und unterzieht den Code einer statischen Analyse.

Im Anschluss prüfen die Akzeptanztests die Software auf fachliche Korrektheit. Die Tests sind automatisiert, sodass sie reproduzierbar sind und jeder einzelne Lauf keinen besonderen Aufwand darstellt. Bei den Kapazitätstests lässt sich die Software danach darauf untersuchen, ob sie bei der
in Produktion zu erwartenden Last die notwendige Performance liefert – auch diese Tests sind automatisiert. Fachliche Experten begutachten schließlich bei den explorativen Tests neue Features genauer. Am Ende lässt sich die Software der Produktion übergeben.

Eine klassische Continuous Delivery Pipeline

Die Pipeline wird sehr oft durchlaufen, durchaus auch mehrere Male pro Tag. Da solch ein Vorgehen mit manuellen Tests zeitlich und kostengünstig wohl kaum umsetzbar wäre, ist eine weitgehende Automatisierung die Grundlage der Pipeline.

Continuous Delivery ist eine Erweiterung von Continuous Integration: Letztere stellt sicher, dass Software sich jederzeit kompilieren lässt und alle Änderungen der Entwickler regelmäßig integriert werden. Sie ist weitgehend mit der Commit-Phase in Continuous Delivery deckungsgleich und erweitert den kontinuierlichen Prozess so bis zur Auslieferung in Produktivbetrieb.

Die Automatisierung macht dabei auch vor der Installation der Software nicht halt: Da die Software getestet werden muss und schließlich in Produktion gebracht wird, sind dazu der Installationsprozess und die Konfiguration der Server zu automatisieren.

Das wesentliche Ziel von Continuous Delivery ist schnelles Feedback. Damit steht das Verfahren in der Tradition agiler Prozesse: Sie setzen auf das Deployment in Produktion und das Feedback vom Kunden, um so neue Anforderungen zu ermitteln. Continuous Delivery kann die Softwareverteilung im Produktivbetrieb beschleunigen. Dank der erwähnten Pipeline wird nach jedem Commit eines Entwicklers untersucht, ob der Commit neue fachliche Fehler oder ein unzureichendes Verhalten ergeben hat. Ohne diese Pipeline müssten Programmierer dazu die entsprechenden Testphasen abwarten, was durchaus Wochen oder Monate dauern kann. In der Zwischenzeit hat sich die Software oft so umfassend geändert, dass es schwierig ist, die für einen Fehler verantwortliche Änderung zu identifizieren.

Das Feedback lässt sich sogar noch weiter beschleunigen, indem man zuerst alle fachlichen Features für einfache Fälle testet, bevor man sich an komplexere Fälle begibt. Beispielsweise lässt sich zunächst überprüfen, ob eine Bestellung und die Registrierung eines neuen Kunden grundsätzlich funktionieren, bevor man die Features mit all ihren Regeln und Sonderfällen im Detail testet. Wenn Unternehmen die Reihenfolge der Tests so ändern, lassen sich fundamentale Probleme mit einem Feature schneller identifizieren, als wenn jedes sofort in voller Tiefe getestet würde.

Continuous Delivery ist also ein Vorgehen der Softwareentwicklung, das Techniken in den Mittelpunkt stellt, bei denen es vor allem um das Bauen und das Testen von Software geht. Eigentlich sollte das keinen Einfluss auf die Architektur haben – denn der Aufbau der Software ist von solchen Aspekten unabhängig.

In der Wirklichkeit hat Continuous Delivery jedoch durchaus Einfluss auf die Architektur. Wenn es gewünscht ist, Software jederzeit auszuliefern, ist das beim Umsetzen neuer Features zu beachten. Oft werden in der Versionskontrolle die Änderungen für ein neues Feature in einem eigenen Branch angelegt. Erst wenn es in Produktion gehen soll, werden die Änderungen in den Haupt-Branch übernommen. Anschließend kann die neue Funktion live gehen.

Das widerspricht der grundlegenden Idee von Continuous Integration: Statt ständig alle Änderungen zu integrieren, werden die Änderungen für ein neues Feature getrennt und erst später mit den anderen – beispielsweise für andere Features – integriert. Erst bei der Integration des Feature-Branches treten die Probleme auf, die sich durch Inkompatibilitäten und Fehler der verschiedenen Entwicklungszweige ergeben.

Wenn aber alle Änderungen für alle Features in der Versionskontrolle landen und das Ziel ist, den Code möglichst oft auszuliefern – wie lässt sich dann verhindern, dass halbfertige Features plötzlich im Produktivbetrieb sind? Die Lösung für dieses Problem sind Feature Toggles. Sie ermöglichen das Aktivieren und Deaktivieren der Funktionen. So lässt sich ein Feature entwickeln, ohne dass es in Produktion gleich aktiv ist. Das fügt zur Architektur eine weitere Dimension hinzu: Für jede Funktion gibt es einen eigenen Feature Toggle. Ebenso ist es aber auch möglich, nur ein Toggle zu implementieren, der zwischen Entwicklungs- und Produktivumgebungen unterscheidet. Der Code jedes Features muss dann entscheiden, ob es in Produktion aktiv sein sollen oder nicht. So kann das Feature zunächst nur in Test-Umgebungen aktiv sein, bevor es auch für den Produktivbetrieb aktiviert wird.

Und es kann auch sinnvoll sein, sie aus anderen Gründen zu deaktivieren: Wenn beispielsweise das System nicht bereitsteht, das die Verfügbarkeit von Produkten überprüft, lassen sich dieses Feature deaktivieren und stattdessen bestimmte Standardeinstellungen nutzen. Dann sind nicht mehr genutzte Toggles aus den Konfigurationen und dem Code zu entfernen, damit der Code auch langfristig wartbar bleibt.

Einen weiteren Einfluss auf die Architektur hat Continuous Delivery hinsichtlich der Deployment-Artefakte. Vor allem in der Java-Welt entstehen meistens große monolithische Anwendungen. Monolithisch meint hier nicht den internen Aufbau der Anwendung, sondern die Softwareverteilung: Auch perfekt modularisierte Java-Anwendungen werden oft in einem einzigen EAR-File ausgeliefert, in dem dann alle Module enthalten sind – oder auch in einem WAR, bei dem die weiteren Module als Bibliotheken enthalten sind. Dieses Vorgehen führt zu einem Problem: Wenn sich nur ein kleiner Teil der Anwendung ändert, muss die gesamte Anwendung neu gebaut und getestet werden. Selbst Funktionen, die gleich geblieben sind, müssen noch einmal alle Schritte der Continuous-Delivery-Pipeline durchlaufen. Das ist aber überflüssig – schließlich hat sich nichts geändert und es kann keine neuen Fehler geben.

Zusätzliche Arbeit verzögert das Feedback, was wiederum dem Ziel von Continuous Delivery widerspricht: Erst nach vielen überflüssigen Tests werden die ausgeführt, die für das neue Stück Code tatsächlich relevant sind. Deshalb ist es gar nicht so einfach, für eine monolithische Anwendung eine Continuous Delivery Pipeline aufzubauen, die wirklich effektiv ist. Wenn das Bauen und Testen der Anwendung eine oder gar mehrere Stunden läuft, kann die Pipeline nicht so häufig durchlaufen werden, und es dauert, bis Fehler offensichtlich werden. Sicherlich lassen sich Tests und Builds beschleunigen – aber das ist aufwendig und dem Optimierungspotenzial sind enge Grenzen gesetzt.

Die Alternative ist die Optimierung der Architektur für Continuous Delivery. Das Ziel sind möglichst kleine deploybare Artefakte, die sich getrennt voneinander verteilen lassen. Das Softwareverteilung sollte dabei so unabhängig wie möglich sein – idealerweise also, ohne dass andere Artefakte ebenfalls neu zu deployen oder neu zu starten wären. Dann ist bei einer Änderung nur die Deployment-Pipeline für das geänderte Artefakt zu durchlaufen, alle anderen Softwarebausteine sind nicht betroffen. Da die Artefakte kleiner sind und weniger Funktionen und Code haben, geschieht das Durchlaufen der Deployment Pipelines wesentlich schneller. So erreicht man ein schnelles Feedback.

Wie erwähnt, müssen die Artefakte vollständig getrennt voneinander sein – die Änderung eines Artefakts darf idealerweise noch nicht einmal dazu führen, dass ein anderes neu zu starten ist. Letztlich muss jeder Baustein ein eigener Prozess sein. Für dieses Konzept hat sich der Name "Microservice" eingebürgert.

Ihn kann man durchaus kritisch sehen: "Micro" stellt die Größe der Services in den Mittelpunkt. Sicherlich sind sie verhältnismäßig klein und kleiner als "normale" Anwendungen. Wichtiger als die Größe aber ist der fachliche Schnitt, aus dem sich die Aufteilung in Services ergibt. Auch sind Microservices mehr als "nur" Services – ein wesentlicher Bestandteil des Konzepts ist, dass sie auch GUI-Elemente haben. Wenn sich die Behandlung einer Benutzer-Interaktion von der GUI über die Logik bis hin zur Persistenz vollständig in einem Microservice abarbeiten lässt, fällt kein Kommunikations-Overhead an. Außerdem sind alle technischen Elemente für eine fachliche Interaktion in einem Microservice versammelt. Das kommt der fachlichen Architektur zugute. Schließlich lassen sich nur so für den Nutzer nachvollziehbare Akzeptanztests definieren. Wenn der Nutzer die Aktivitäten durch eine GUI selbst auslösen kann, versteht er besser, was bei den Tests überhaupt genau abläuft.

In einer E-Commerce-Anwendung können Bereiche wie Bestellung, Suche oder der Katalog jeweils eigene Microservices sein. Klassische Portale, die unterschiedliche Funktionen zusammenführen und gemeinsam anbieten, legen schon von ihrer Aufgabe her eine Unterteilung in Microservices nahe: Schließlich lässt sich jede einzelne Funktion als einen solchen Service implementieren. Ebenso kann man die Ergänzung einer monolithischen Anwendung durch einen Microservice implementieren. Die Integration kann beispielsweise durch Links oder Aufrufe der alten Anwendung geschehen. Der Vorteil: Der Microservice kann einen eigenen Technologie-Stack nutzen, und es sind fast keine Änderungen am oft schwer wartbaren Legacy-Code notwendig.

Microservices grenzen sich auch klar von serviceorientierten Architekturen (SOA) ab: Letztere fokussieren auf die Integration verschiedener Anwendungen im gesamten Unternehmen. SOA soll durch die Aufteilung der Anwendungen in Dienste neue Integrationsmöglichkeiten schaffen und neue Applikationen durch die Komposition der Dienste einfacher realisierbar machen. SOAs beeinflussen
also mehrere Anwendungen und daher auch mehrere Teams, die über verschiedene Organisationseinheiten verstreut sein können. Microservices hingegen sind ein Modell, um eine einzelne Applikation umzusetzen. Man nutzt sie, statt Komponenten der Anwendungen als Bibliotheken oder beispielsweise JAR-Dateien zu realisieren.

Also entstehen Microservices in einem Projekt-Team. Das vereinfacht die Kommunikation und Koordination. So lassen sich in ihm alle Services so koordinieren, dass bei einer Änderung an einer Schnittstelle in relativ kurzer Zeit alle Dienste auf eine neue Schnittstelle umgestellt werden. Bei einer SOA-Architektur ist das viel schwieriger, weil die Teams und Services nicht so eng koordiniert sind – vielleicht werden einige Services sogar gar nicht mehr weiterentwickelt.

Die Microservices können zum Beispiel mit REST miteinander kommunizieren. Da sie Oberflächen anbieten, können Services Links auf andere Services anbieten und so miteinander integriert werden. Um Microservices vollständig umzusetzen, sind noch einige Punkte zu beachten: Idealerweise sollten sie getrennte Datenbestände und Datenbanken haben. Nur dann sind die Microservices wirklich unabhängig voneinander. Wenn zwei Services dieselbe Datenbank nutzen, kann man die Datenstrukturen und das Schema nicht mehr ändern, was die weitere Entwicklung einschränkt. Eine Replikation von Daten aus anderen Microservices ist denkbar.

Die Schnittstellen müssen rückwärtskompatibel sein. Sonst sind bei der Softwareverteilung einer neuen Serviceversion auch alle abhängigen Services neu zu deployen. Ebenso sollten sie zustandslos sein. Beim Neustart eines Microservice geht so kein Zustand verloren. Natürlich lässt sich in einer Datenbank der Zustand speichern. Schließlich hat jeder Microservices Schnittstellen zu anderen Microservices. Ihr Aufruf kann fehlschlagen – beispielsweise weil sie gerade nicht laufen oder ein Problem im Netzwerk aufgetreten ist. Daher sind bei solchen Aufrufen entsprechende Vorkehrungen zu treffen.

Durch Microservices entstehen weitere Herausforderungen: Die Aufteilung der Anwendung in einzelne Komponenten wird technisch als eine Aufteilung in verschiedene Microservices implementiert. Um die tatsächlich umgesetzte Architektur zu verstehen, müssen also die Kommunikationsbeziehungen zwischen den Services bekannt sein – das kann aber in der Praxis nur schwer nachvollziehbar sein. Ebenso ist es schwierig, Funktionen von einem Microservice in einen anderen zu verschieben. Im Extremfall sind die Microservice noch nicht einmal in derselben Programmiersprache geschrieben, sodass die zu verschiebende Funktion neu zu implementieren ist. In einem Monolithen, der in einer Programmiersprache implementiert ist, sind solche Umstrukturierungen viel einfacher.

Ein wichtiger Hinweis noch: Dieser Artikel fokussiert auf die Vorteile von Microservices in Bezug auf Continuous Delivery – aber es gibt viele weitere Gründe für Microservices, auf die nur kurz eingegangen sei. Da sich Microservices unabhängig voneinander verteilen lassen, ist es kein Problem, jeden für sich weiterzuentwickeln. Dadurch kann man neue Features unabhängig voneinander schreiben. Wenn für jeden Microservice ein Team zuständig ist, kann es ein neues Feature schnell und produktiv umsetzen.

Weil Microservices so strikt getrennt voneinander sind, können sich auf Code-Ebene keine Abhängigkeiten einschleichen. Dadurch ist sichergestellt, dass sie auch langfristig wartbar bleiben. Wer schon einmal die Architektur eines größeren Systems analysiert hat, kennt das Phänomen: Innerhalb einer Deployment-Einheit kann die Architektur schnell chaotische Zustände annehmen, wenn kein aktives Architektur-Management betrieben wird. Zyklische und andere unerwünschte Abhängigkeiten sind keine Seltenheit. Zwischen den Deployment-Einheiten sind die Abhängigkeiten aber immer relativ sauber. Genau deshalb kann man auch erwarten, dass die Abhängigkeiten zwischen den Microservices immer relativ klar sein werden.

Eine Continuous Delivery Pipeline steht für die Optimierung der Deployment-Prozesse – und damit dem Fokus auf der Automatisierung. Auf den ersten Blick scheint dieses Vorgehen keine Auswirkungen auf die Software und die Softwarearchitektur zu haben. Bei genauerer Betrachtung wird aber offensichtlich, dass Feature Toggles zum Ein- und Ausschalten bestimmter Features praktisch eine Notwendigkeit für Continuous Delivery sind. Sonst führt das Deployment einer neuen Version automatisch dazu, dass neue Features zur Verfügung stehen, die vielleicht noch gar nicht live sein sollen.

Um die Continuous Delivery Pipelines einfach zu halten und schnelles Feedback zu ermöglichen, sollten für Continuous Delivery die einzelnen Deployment-Einheiten nicht zu groß sein. Dazu bieten sich Microservices an. Sie bringen auf der einen Seite einige neue Herausforderungen mit, bieten aber gleichzeitig den Vorteil einer besseren Produktivität durch unabhängige Softwareverteilung und eine klare Trennung der Komponenten.

Im zweiten Teil geht es darum, wie sich Microservices konkret technisch ausrollen lassen – immerhin entsteht eine Vielzahl an einzelnen Prozessen, die konfiguriert und mit einer Ablaufumgebung versehen werden müssen. (ane)

Eberhard Wolff (@ewolff)
arbeitet als freiberuflicher Architekt und Berater. Außerdem ist er Java Champion und Leiter des Technologie-Beirats der adesso AG. Sein Schwerpunkt liegt auf modernen Softwarearchitekturen.

  1. Eberhard Wolff; Continuous Delivery; dpunkt.verlag 2014
  2. Jez Humble, David Farley; Continuous Delivery; Reliable Software Releases through Build, Test, and Deployment Automation; Addison-Wesley 2010