Update 18.11.2024: Ich habe das Diagramm zum dritten Modell „Mehrere Container mit externen Abhängigkeiten“ ergänzt um Abhängigkeiten zwischen den Containern. Danke an Ralf Westphal für den Hinweis.
Als Entwickler kann ich das Ergebnis meiner Arbeit an den Betrieb übergeben, der sich dann um alles weitere kümmert. Das macht Freude, denn so habe ich mit dem lästigen Zeugs nichts mehr am Hut. Das Bild dazu: ich werfe mein Zeug über den Zaun und der Betrieb macht den Rest. Cool? Mitnichten!
Unter dem Begriff DevOps verstehe ich eine enge Zusammenarbeit zwischen Entwicklung und Betrieb. Als Entwickler betrachte ich auch den Betrieb der Software. In einem agilen Umfeld findet der Betrieb nicht erst nach Monaten oder Jahren der Entwicklung statt, sondern ab dem ersten Durchstich. Es ist wichtig, von Anfang an möglichst nah am Betrieb der Software zu sein und diesen Aspekt mit zu entwickeln. So werden mögliche Probleme des Betriebs frühzeitig erkannt. Ferner wird dadurch der Product Owner in die Lage versetzt, Feedback zu geben. Und der Betrieb wird in die Lage versetzt, seinen Teil der Arbeit zu tun, wie bspw. das Monitoring der Software einzurichten.
Anfangs mag das Softwaresystem nur intern in Betrieb genommen werden. Jedoch sollte dabei alles so nah an der zukünftigen „echten“ Betriebsumgebung sein, wie möglich. Falls du dich fragen solltest, wie das ganz praktisch gehen soll: das Schlüsselelement heißt Docker.
Docker und der Monolith
Bevor wir zu weiteren Details kommen: Docker wird häufig in Zusammenhang mit Microservices genannt. Doch auch Monolithen können als Docker Container deployed werden. Das ist sogar unabhängig davon, ob der Monolith intern gut strukturiert ist oder es sich um ein „chaotisches“ Legacy System handelt. Nicht jedes Softwaresystem sollte auf Microservices aufgeteilt werden. Die Notwendigkeit muss sich aus den Anforderungen ergeben. Nur weil wir Entwickler irgendetwas cool finden, ist das noch kein Grund, alle Buzzwords am echten System auszuprobieren.
Erstellt man ein Docker Image für seinen Monolithen (oder auch die Microservices), kann damit das Deployment drastisch vereinfacht werden. Die Zeiten, in denen wir Verzeichnisse kopiert haben, um eine Anwendung zu deployen, sind vorbei. Deployment muss auf Knopfdruck erfolgen können und vor allem kontinuierlich stattfinden. Wer nicht an den großen Nutzen kontinuierlicher Deployments „glaubt“: das Buch Accelerate gibt fundierte Hinweise!
Ein weiterer Vorteil: man kann mit einem Docker Image der Anwendung das automatisierte Testen vereinfachen. Die Anwendung kann so auch auf den Entwicklermaschinen ausgeführt werden. Ferner ist dies ein wichtiger Schritt auf dem Weg zu Continuous Integration, weil dann auch auf dem CI Server Integrationstests ausgeführt werden können. Diese laufen gegen denselben Container, der später auch deployed wird. Dadurch sind die Integrationstests so nah wie möglich am realen Deployment.
Warum ist Docker so gut geeignet für CI/CD?
- Container bringen alle Abhängigkeiten innerhalb der Betriebssystemumgebung mit.
- Es wird eine reproduzierbare Umgebung erreicht. Der Umzug auf einen anderen Server wird damit sehr einfach.
- Das Einbinden externer Abhängigkeiten wie MySQL, PostgreSQL, RabbitMQ etc. wird vereinfacht.
- Das Deployment der Container auf den oder die Server wird deutlich vereinfacht.
- Mit TestContainers können automatisierte Unit- und Integrationstests erstellt werden.
- Die automatisierten Tests können auch auf dem Continuous Integration System ausgeführt werden.
- Docker Container starten sehr schnell.
- Docker Images sind klein durch den Aufbau in Layern.
- Docker Container können auch auf den Entwicklermaschinen laufen, so dass die Entwickler sehr nah an der Echtumgebung arbeiten können.
Die unterschiedlichen Modelle
Beim Aufbau eines Softwaresystems können wir drei Modelle unterscheiden, bezogen auf die Anzahl von Containern:
- 1 Container ohne externe Abhängigkeiten
- 1 Container mit externen Abhängigkeiten
- Mehrere Container mit externen Abhängigkeiten
Mit externer Abhängigkeit bezeichne ich hier solche Container, die unser Softwaresystem zwar verwendet, die wir jedoch nicht selbst erstellen. Dazu gehören bspw. MySQL, PostgreSQL, RabbitMQ, Keycloak und viele andere. Der große Vorteil bei der Verwendung von Docker ist, dass wir diese Abhängigkeiten leicht bereitstellen können, weil sie ebenfalls als Container verfügbar sind. Dabei wird auch gezielt die gewünschte Version ausgewählt, so dass das Softwaresystem nicht überraschend fehlerhaft reagiert, weil „zufällig“ eine andere Version zum Einsatz kommt. Man kann schon erkennen: bei Docker dreht sich alles um die Reproduzierbarkeit der Umgebung.
Im Folgenden betrachten wir die drei Modelle etwas detaillierter.
1 Container ohne externe Abhängigkeiten
Bei diesem Modell besteht unser Softwaresystem aus einem einzigen Container. Das dazugehörige Image erzeugen wir im Buildprozess. Dabei ist dieser Container self-contained, verfügt also über keine Abhängigkeiten zu anderen Containern, weder eigene noch fremde.
Solche einfachen Systeme sind eher die Ausnahme, allerdings ein sehr guter Startpunkt, weil sie im Deployment sehr einfach zu handhaben sind. Das Thema Persistenz könnte hierbei zum Beispiel über das Dateisystem gelöst werden. Die Anwendung speichert ihre Daten einfach in Dateien ab. Damit diese Daten nicht beim Neustart des Containers verloren gehen, werden die entsprechenden Verzeichnisse über Volumesauf das Host Betriebssystem gemappt.
Es ist empfehlenswert, zunächst eine solch einfache Lösung anzugehen und damit Erfahrungen zu sammeln. Da sie weniger komplex ist, müssen noch nicht alle Docker Konzepte beherrscht werden.
1 Container mit externen Abhängigkeiten
Den nächsten Schritt stellt ein Softwaresystem dar, das selbst weiterhin nur aus einem Container besteht, jedoch über externe Abhängigkeiten zu anderen, fremden, Containern verfügt. Auf diese Weise kann bspw. das Thema Persistenz über eine SQL oder NoSQL Lösung realisiert werden.
Beim Deployment eines solchen Modells kommt das Thema Docker Compose ins Spiel. Damit können mehrere Container gemeinsam deployed werden. Ferner werden die Abhängigkeiten unter den Containern definiert, damit sichergestellt ist, dass die Container in der richtigen Reihenfolge gestartet werden. Nachdem im ersten Modell die Volumes erforderlich waren, ist nun die Kommunikation über Networks ein neues Thema.
Mehrere Container mit externen Abhängigkeiten
Der letzte Schritt besteht nun darin, das eigene Softwaresystem aus mehreren Containern zusammenzusetzen. Auch hierbei wird wieder Docker Compose verwendet, um zu definieren, welche Container zu starten sind. Um alle eigenen Images bereitzustellen, muss dies im Buildprozess berücksichtigt werden. Der Buildprozess wird dadurch anspruchsvoller.
Kleine Randbemerkung: nicht jede Software muss auf mehrere Container aufgeteilt werden. Ein gut strukturierter Monolith ist nach wie vor eine mögliche Lösung für viele Probleme.
Skalierung
Ein weiterer Vorteil von Docker liegt darin, dass wir grundsätzlich auch mehrere Instanzen eines Images starten können. Auf Seiten der externen Abhängigkeiten können so bspw. mehrere Instanzen von PostgreSQL gestartet werden, sogar in unterschiedlichen Versionen. Dies ist erforderlich, wenn eine externe Abhängigkeit selbst aus mehreren Containern besteht. So benötigt bspw. Keycloak eine PostgreSQL Datenbank. Setzen wir in unserem Softwaresystem also Keycloak ein und wollen selbst auch PostgreSQL verwenden, ist es sinnvoll mit zwei Instanzen von PostgreSQL zu arbeiten. Dann sind einerseits die Daten getrennt abgelegt und andererseits können unterschiedliche Versionen zum Einsatz kommen.
Mehrere Instanzen eines Containers können auch zur Lastverteilung verwendet werden. Eine solche horizontale Skalierung kann bspw. mit Kubernetes vorgenommen werden.
Fazit
Docker ist ein sehr mächtiges Werkzeug, das aus den Bereichen Continuous Integration und Continuous Deployment nicht mehr wegzudenken ist. Auch für die Automatisierung von Tests bietet Docker viele Vorteile, wie ich in diesem Blogbeitrag über den Einsatz von TestContainers dargestellt habe.
Ich möchte hier allerdings auch nicht verschweigen, dass CI/CD mit Docker eine gewisse Lernkurve hat. Hier können unsere Trainings helfen. Bei Bedarf kontaktiere uns gerne.