Zum Inhalt springen

Wie man eine Microservice-Umgebung lokal betreibt

Einen Microservice lokal zu starten ist leicht: sein README sagt es. Der Ärger fängt an, wenn der Ablauf, den du testen willst, durch vier geht und jeder davon zwei weitere aufruft, die nicht deine sind.

Diese Seite ist kein Tutorial zu einem bestimmten Werkzeug: sie sagt, welche Teile es braucht, warum der schwierige Teil nicht der ist, nach dem er aussieht, und wie jeder gelöst wird — mit Aseptic am Ende, weil das ist, was diese Seite dokumentiert, aber die vier Probleme sind dieselben, was immer du benutzt.

Eine lokale Umgebung, die etwas taugt, muss vier Dinge lösen. In dieser Reihenfolge der tatsächlichen Schwierigkeit, die fast nie die Reihenfolge ist, in der man sie angeht:

  1. Gemeinsame Infrastruktur. Kafka, Redis, PostgreSQL, was dein System eben nutzt. Der sichtbarste Teil und der am besten gelöste: Container.
  2. Start jedes Service. Sein Befehl, sein Profil, seine Variablen, sein Port. Repetitive Arbeit, aber mechanisch.
  3. Kommunikation zwischen den Services. Wohin jeder ruft. Hier geht der Vormittag drauf, und darüber redet fast niemand.
  4. Daten. Ein angelegtes Schema und etwas darin, womit sich testen lässt.

Eine docker-compose.yml mit den gemeinsamen Teilen und docker compose up. Das machen alle, und das ist richtig: Container sind genau das passende Werkzeug dafür.

Zwei Details, die später beißen:

  • Ports kollidieren. Jedes Team deklariert 5432 und 6379. Sobald du zwei Projekte offen hast, oder Dinge hoch- und runterfährst, fangen die Konflikte an.
  • Infrastruktur wird zwischen Projekten geteilt, Daten nicht. Dasselbe Postgres für zwei verschiedene Systeme wiederzuverwenden endet in einem Schema, von dem niemand weiß, wem es gehört.

Jeder Service hat seine eigene Art: ./mvnw quarkus:dev, ./gradlew bootRun -Dspring.profiles.active=local, npm start, eine Binärdatei. Mit unterschiedlichen Profilen, Variablen und Optionen.

Das ist es, was üblicherweise in einer vierzigschrittigen LOCAL_SETUP.md landet: es funktioniert, aber es altert schlecht. Niemand aktualisiert das Dokument, wenn sich eine Option ändert, und die nächste Person entdeckt die Lücke über Fehler.

Ein Detail, das viel mehr entscheidet, als es aussieht: nicht jeder Service will gleich laufen. Den, an dem du arbeitest, willst du nativ, mit Hot Reload und angehängtem Debugger. Die anderen drei müssen nur oben sein: im Container ist gut, isoliert und ohne deine Maschine vollzumüllen.

Hier liegt die eigentliche Arbeit, und sie ist die, die dir kein Container-Werkzeug abnimmt.

Dein Service bff ruft orders auf. In der Produktion kommt diese URL von der Service Discovery oder vom Ingress. Lokal muss jemand entscheiden, wohin sie zeigt, und die Optionen sind nicht gleichwertig:

  • Auf den Service, der auf deiner Maschine läuft. Das willst du, wenn du beide Seiten änderst. Aber dann musst du ihn auch starten, mit seinem ganzen Rattenschwanz an Abhängigkeiten.
  • Auf die gemeinsame Entwicklungsumgebung. Das erspart dir den Start, im Tausch gegen eine Verbindung zu ihrem Netzwerk, gegen Daten, die sich unter deinen Füßen ändern, und dagegen, dass du auch unten bist, wenn jene Umgebung unten ist.
  • Auf einen Mock. Perfekt, wenn dir die Antwort jenes Service egal ist, oder wenn du ihn gar nicht erreichen kannst. Im Tausch dafür, den Stub zu pflegen.

Teuer ist nicht das Wählen: teuer ist, die Wahl anzuwenden. Jede Option heißt, eine URL in der Konfiguration eines Repositories zu ändern, das nicht deines ist, daran zu denken, sie nicht zu committen, und das rückgängig zu machen, wenn du etwas anderes probieren willst. Multipliziere mit Services und mit Malen pro Tag.

Und es gibt eine Falle: die richtige URL hängt davon ab, wo der Aufrufer läuft. Ein nativer Service erreicht einen Container über localhost:port; ein Container, der einen anderen Container aufruft, über den Servicenamen; und ein Container, der etwas Natives auf deiner Maschine aufruft, braucht den speziellen Hostnamen. Dieselbe Abhängigkeit, drei verschiedene URLs, je nachdem, wie jedes Ende läuft.

Schema angelegt, Migrationen gelaufen und ein paar minimale Daten. Das löst man üblicherweise mit den Migrationen der Anwendung selbst plus einem Seed-Skript.

Die Regel, die man besser nicht bricht: die lokale Umgebung fasst keine gemeinsamen Datenbanken an. Eine versehentlich gegen die gemeinsame Entwicklungsumgebung abgefeuerte Migration ist ein verlorener Nachmittag für das ganze Team.

Ansatz Gut für Wo er zu kurz greift
docker-compose für alles Infrastruktur; ein kleines, stabiles System Bei jeder Änderung das Image deines Service bauen; kein bequemer Hot Reload und kein bequemer Debugger; die Kommunikation bleibt handgeschriebene Konfiguration
Skripte + LOCAL_SETUP.md Schnell loslegen Es altert: niemand aktualisiert es und es scheitert auf jeder Maschine anders
Testcontainers Echte Integrationstests Die Umgebung entsteht und stirbt mit dem Test; sie ist kein Ort zum Arbeiten
Lokales Kubernetes (Kind, minikube) Der Produktion ähneln Viel Maschine und viel Build-Zyklus für den Alltag; die Änderungsschleife wird länger
Lokaler Orchestrator Die tägliche Schleife mit mehreren Services Jeder Service muss einmal beschrieben werden

Keiner davon ist dumm: sie lösen verschiedene Dinge und koexistieren oft. Was keiner der ersten vier tut, ist pro Abhängigkeit zu entscheiden, wohin jeder Service ruft, und das anzuwenden, ohne die Repositories anzufassen.

Aseptic ist ein Desktop-Orchestrator, der sich um die vier Teile kümmert und den dritten als das Hauptproblem behandelt, das er ist.

  1. Du beschreibst jeden Service einmal. Du zeigst auf den Repository-Ordner, und seine Technologie, seine Ports, die Infrastruktur, die er braucht, und wen er aufruft werden autoerkannt. Du prüfst es, und fertig.
  2. Du gruppierst in einem Szenario die Services eines Ablaufs — „Checkout“, „Kundenanlage“ — und sie starten zusammen, der Reihe nach und wartend, bis jeder gesund ist.
  3. Für jede Abhängigkeit wählst du lokal, Cloud oder Mock, und die Engine schreibt die URL beim Start um, indem sie sie als Konfiguration injiziert —-D-Optionen oder Umgebungsvariablen—, mit der richtigen Perspektive je nachdem, wo jedes Ende läuft. Deine Repositories werden nicht angefasst.
  4. Die gemeinsame Infrastruktur (Kafka, Redis, PostgreSQL) verwaltet die App in Docker, mit automatischem Port-Remapping bei Kollisionen.
  5. Jeder Service, nativ oder im Container, nach deiner Wahl: nativ der, an dem du arbeitest, im Container die im Hintergrund.

Und was du baust, wird geteilt: ein Szenario wird in eine in sich geschlossene Datei exportiert, sodass wer neu dazukommt klont, importiert und startet, statt die LOCAL_SETUP.md zu lesen.