Lokale Umgebung: schnell qualitatives Feedback erhalten

Geschrieben von:

Adrien Wehrung

Lokale Umgebung (2)

Die Software-Industrie ist ein schnell wachsender Bereich, in dem die Innovation nicht zu wenigen geschlossenen Kreisen gehört, sondern einen Grundbedarf darstellt. Sie zu nutzen, ist für das Bestehen einer Firma zur Notwendigkeit geworden. Die Entwickler befinden sich im Herzen dieses Geschäfts, und deshalb ist es ihre tägliche Aufgabe, ihre Kreativität und Expertise einzusetzen, um neue Wege zu entdecken und die Produkte stetig weiterzuentwickeln, ohne das Bestehende dabei zu gefährden.

Zentral für diese Tätigkeit ist das Einholen von Feedback, kurz gesagt: geht das, was ich implementiere, in die richtige Richtung? Dieses Feedback gibt es auf zwei Hauptebenen:

  • Technische Ebene, z.B.: Erlaubt mir dieses Refactoring einen messbaren Performance-Gewinn? Behebt meine Implementierung den Bug? Kann sich meine Idee integrieren oder führt sie neue Probleme in das bestehende System ein?
  • Business-Ebene, z.B.: Wird sich durch dieses Feature die Kundenzufriedenheit erhöhen? Wie fühlt sich das Feature in der Benutzung an?

Die Antworten auf diese Fragen sind Signale, wodurch die Entwicklung gelenkt wird: Das ist eine der Grundideen von agiler Entwicklung. Ohne Kompass kann man zwar segeln, aber es bringt nichts, solange man nicht weiß, ob die Richtung stimmt. In anderen Worten ist ohne Feedback kein zielgerichteter Fortschritt möglich. Natürlich sind diese Fragen unterschiedlich schwer zu beantworten. Die meisten erfordern eine gute Zusammenarbeit zwischen allen beteiligten Rollen (Entwickler, Product-Owner, Auftraggeber). Wichtig ist vor allem, dass diese Arten von Feedback technisch sowie organisatorisch ermöglicht werden, um bestmöglich sicherzustellen, dass die Entwickler stets zielorientiert arbeiten können.

Ein großer Erfolgsfaktor bei der agilen Entwicklung ist die Qualität und Geschwindigkeit des Feedbacks: je präziser und schneller Antworten auf die obigen Beispielfragen geliefert werden können, desto effizienter kann das Team arbeiten. Dieser Artikel behandelt eine lokale Laufzeitumgebung des entwickelten Systems. Wir analysieren, inwiefern dieser Baustein dazu dient, sowohl auf technischen Ebene als auch auf Business-Ebene, aussagekräftiges Feedback schnell und günstig zu liefern. Wir kennzeichnen die Gefahren und Grenzen für seinen Einsatz und listen Ideen auf für eine gesunde lokale Entwicklungsumgebung.

Das schnellste Feedback zum günstigsten Preis

Der erste offensichtliche Vorteil, als Entwickler eine lokal laufende Umgebung zur Verfügung zu haben, ist die nahezu totale Freiheit, die mir gegeben wird. Absprachen oder Synchronisation mit anderen Teilnehmern sind nicht nötig, ich kann jederzeit beliebige Kombinationen von Komponenten hochfahren, ohne die Stabilität eines laufenden Systems zu gefährden oder die Arbeit eines Kollegen zu blockieren.

Auch bemerkenswert ist die Geschwindigkeit, mit der ich meine Änderungen in der laufenden Software sehen kann: Die zu überwindende "Distanz" ist minimal, denn der Code wird auf der gleichen Maschine geschrieben und gebaut, wo er auch läuft. Je nach Technologie kann die Dauer bis zu weniger als einer Sekunde reduziert werden, zwischen dem Moment der Codeänderung und der Auswirkung dieser Änderung!

Aus Management-Sicht ist auch der Gewinn auf mindestens zwei Wege spürbar: Der Erste ist die reine Kostenfrage, der Andere bezieht sich auf die Kommunikationsebene. Indem es unterstützt wird, dass die Entwickler auf ihren eigenen Maschinen testen können, wird bei der Bereitstellung von zusätzlichen Testumgebungen in der Cloud gespart. Meistens reichen eine oder zwei geteilte Testumgebungen, wenn ein Großteil des explorativen Testens auf der Entwicklermaschine stattfinden kann. Andererseits kann mit den Entwicklern schneller während der Entwicklung eines Features abgestimmt werden, ob die Implementierung zum Zielbild passt. In der Theorie sind Anforderungen stets klar formuliert und kommuniziert, sodass weiterführende Abstimmungen nicht notwendig sind. In der Praxis jedoch gehen einige verloren oder werden missinterpretiert. Sofern eine solide lokale Laufzeitumgebung benutzt wird, wird dieses Problem frühzeitig und kostengünstig angegangen.

MicrosoftTeams-image (33)

Gefahren und Limitierungen

Die Strategie, eine gute lokale Umgebung als erste Testumgebung einzusetzen, kann nur gewinnbringend sein, wenn einige Fallstricke vermieden werden können. Die vier folgenden Gefahren sind die häufigsten Punkte, die es einer lokalen Umgebung verwehren, ihr volles Potenzial zu bieten.

Komplizierte Einrichtung

Die Konfiguration der lokalen Umgebung wächst organisch mit dem Produkt: Es werden neue Komponenten immer weiter hinzugefügt und plötzlich müssen 3 Datenbanken, 12 Microservices, eine Message-Queue und ein Load-Balancer gestartet werden, um neue umfangreiche Features zu testen. Muss die Umgebung per Hand Stück für Stück eingerichtet werden, dann verschwindet der Gewinn an Produktivität, denn es ist wieder einfacher, direkt auf eine Cloud-Testumgebung zu testen. Bestehende Entwickler benutzen garantiert unterschiedliche Konfigurationen und neue Entwickler benötigen einen großen Aufwand, um ihre zum Laufen zu bringen.

Unrealistische Ersatzkomponenten

Nicht alle Komponenten, die in der "echten" Cloud-Umgebung benutzt werden, können lokal reproduziert werden, insbesondere wenn das Produkt auf viele Cloud-spezifische Services beruht. Zum Beispiel: wenn die Software auf AWS-KMS beruht, um Verschlüsselung zu betreiben, muss die lokale Umgebung diese fehlende Cloud-Komponente irgendwie ersetzen. Eine gute Architektur sollte es erlauben, hinter dem Interface eine andere (pseudo-)Implementierung anzusprechen, allerdings besteht mit solchen Abweichungen immer die Gefahr, dass diese Unterschiede in der Implementierung zu unerwarteten Bugs führen, wenn der Code später in der Cloud-Umgebung getestet wird. Diese sind auch entsprechend schwer zu beheben, denn nicht lokal reproduzierbar.

"Lokal-spezifischer" Code

Diese dritte Gefahr entsteht aus der vorherigen: wenn zu viele Unterschiede zwischen Cloud-Umgebung und lokaler Umgebung existieren, dann müssen häufig im Code Weichen geschrieben werden, sodass sich die Software aktiv unterschiedlich verhält, wenn sie lokal läuft. Dieser Code ist dann im ausgelieferten Artefakt enthalten, erhöht unnötig die Komplexität und verringert die Wartbarkeit des Produktes.

Ersatz für automatisierte Tests

Eine lokale Laufzeitumgebung ist kein Ersatz für Unittests. Mit einem wachsenden Projekt ist es sehr schnell unrealistisch, dass die Integrität aller vorherigen Features durch das explorative Testing gewährleistet werden kann. Sie kann zwar das Gefühl vermitteln, dass alles einfach und günstig unter Kontrolle ist, aber automatisierte Tests sind mit Abstand das beste Mittel, um bei jeder Iteration sicher sein zu können, dass alle vorherigen Features und Bugfixes immer noch so funktionieren wie vorher.

7 bewährte Erfahrungen für eine effiziente lokale Umgebung

Um diesen Gefahren entgegenzuwirken und die lokale Laufzeitumgebung möglichst gesund und praktisch zu halten, schlagen wir diese folgenden Praxen vor.

Konfiguration mit Docker-Compose in einem dedizierten Repo verwalten

Ein separates Repository (abseits der diversen Microservices) sollte zur Konfiguration der lokalen Umgebung verwendet werden. Dafür bietet sich Docker-Compose als Mittel der Wahl, sofern jeder Microservice einen Container produziert. Externe Services wie Datenbank oder Message-Queue existieren in der Regel auch als Container und können somit zu Docker-Compose mitsamt Konfiguration hinzugefügt werden. Wenn sich die Services häufen, kann es sich anbieten, getrennte Docker-Compose Dateien für Teile des Systems zu benutzen, sodass diese Teile einzeln hoch- und heruntergefahren werden können (am besten nach den von Domain Driven Design gegebenen Kontexten). Tools wie Tilt oder Portainer stellen eine Möglichkeit dar, die Verwaltung der Umgebung weiter zu vereinfachen.

Hoch- und Herunterfahren in einem Schritt

Per Default sollte es möglich sein, wenn nur das Repo für die lokale Umgebung vorhanden ist, mit einem Befehl die Umgebung zu starten: im besten Fall docker-compose up. Die gebauten Container werden aus der Projekt-Registry gezogen, die öffentlichen Images aus dem Docker-Hub, und die gesamte Konfiguration passiert über Docker-Compose. Diese Regel macht es nicht nur für neu hinzugekommene Entwickler einfach, die Umgebung zum Laufen zu bringen, sondern sie gewährleistet auch, dass sich die einzelnen Teammitglieder auf eine gleiche Konfiguration treffen und dass diese aktuell bleibt. Dies verringert drastisch den works-on-my-machine Effekt.

Möglichst wenig Unterschiede zwischen Cloud- und lokale Konfiguration

So weit wie möglich sollte jeder Microservice nicht "wissen" können, ob er in der lokalen Umgebung oder in der Cloud läuft. Zu diesem Zweck empfiehlt es sich, Umgebungsvariablen zu benutzen. Im Spring-Boot Umfeld können Werte für Application-Properties aus Umgebungsvariablen gelesen werden, was es ermöglicht, das gleiche Profil für Cloud-Umgebung und lokale Umgebung zu benutzen. Beim Hochfahren werden einfach unterschiedliche Umgebungsvariablen geladen.

Aktualisieren der Abhängigkeiten in einem Schritt

Mit mehreren Entwicklern und manchmal mehreren Teams, die an einem gleichen Produkt arbeiten, verändert sich der "aktuellste" Stand sehr schnell. Deswegen ist es kritisch, dass es einfach für jeden Entwickler ist, diesen aktuellsten Stand zu holen: Der Wert des Testens in einer Umgebung (egal ob lokal oder nicht) ist verringert, wenn diese nicht den Stand darstellt, gegen den das neue Feature integriert wird. Sofern die anderen Regeln eingehalten wurden, lässt sich diese auch gut umsetzen: Befehle wie git pull und docker-compose pull (eventuell in einem Sammelskript, falls mehr dazu kommt) machen diese Aktualisierung zu einer trivialen Routine.

Komponenten Bauen in einem Schritt

Auf der Ebene des Microservices, für den ein Feature geschrieben wird, ist es wichtig, dass sich die Änderungen schnell und einfach in die lokale Umgebung integrieren lassen. Eine Standardlösung besteht darin, über das Build-Skript (z.B. mvn clean package oder yarn build) das Artefakt bauen zu lassen und Docker-Volumes zu nutzen, um es in dem Container zu ersetzen, ohne diesen neu zubauen. Ein Neustart des Containers reicht dann, um die Änderung in der lokalen Umgebung wirksam zu haben.

Persistente Datenhaltung mit einfachem Zurücksetzen

Zur Suche nach Einheitlichkeit gehört nicht nur die Konfiguration, sondern auch die Datenhaltung: ohne Daten sehen die meisten Softwareprodukte ganz anders aus als im realen Anwendungsfall. Also die Frage: wie sorge ich dafür, dass ich eine glaubwürdige und projektübergreifend stabile Datenbasis habe, mit welcher mein Produkt in der lokalen Umgebung interagiert? Die Verwendung von Docker Volumes und Fixtures ist eine geeignete Vorgehensweise: Fixtures liefern vorgefertigte Stamm- und Nutzungsdaten, die beim Hochfahren der Umgebung in die Datenbanken eingespielt werden. Docker Volumes sorgen dafür, dass die erzeugten Daten persistent gespeichert werden. Ob jeder Microservice die Fixtures für seinen Hoheitsbereich einspielt oder ob es zentral bei der Konfiguration der lokalen Umgebung gesteuert wird, hängt vom konkreten Anwendungsfall ab.

Realistischer Ersatz für Cloud-Komponenten

Die Cloud-spezifischen Komponenten können die größte Herausforderung darstellen, wenn es darum geht, einen Ersatz für sie in der lokalen Umgebung zu finden. Durch diverse Tools können diese Komponenten simuliert und lokal verwendet werden. Ein bei uns eingesetztes Framework ist Localstack: Dieses Projekt bietet in einem Container eine Vielzahl an Services an, welche einen lokal laufenden Ersatz für die "echten" Cloud-Services bieten. Sollte ein Service nicht vorhanden sein, bieten sich im Grunde immer zwei Möglichkeiten an, um ihn lokal abzubilden: wenn sein Interface einfach genug ist, kann einmalig und händisch einen Ersatz geschrieben werden. Wenn nicht, ist es vielleicht eine Stelle, wo die lokale Umgebung dazu gebracht werden muss, eine echte Cloud-Komponente anzusprechen. Damit stürzt nicht das ganze Konzept wegen eines kleinen Details und die Cloud-Kosten halten sich weiterhin in Grenzen.

Fazit

Eine gesunde lokale Entwicklungsumgebung, womit der Entwickler die Komponenten interagieren lassen und damit risikolos experimentieren kann, ist eine große Hilfe. Durch das schnelle Feedback fallen die schlechten Ideen früher auf. Durch die komplette Freiheit werden innovative Ansätze leichter ermöglicht. Sofern man sich den Grenzen dieser Strategie bewusst ist, bleibt sie ein mächtiges Werkzeug, welches in jedem agilen Projekt dazu beitragen kann, ein besseres Produkt schneller auszuliefern.