
Entkopplung und Orchestrierung: Verwaltung von Multi-App-Stacks auf einer Plattform
TL;DR
|
Irgendwann im Lebenszyklus der meisten wachsenden Webprojekte kommt es zu einer Aufspaltung der Architektur. Das Frontend wird auf eine eigene Bereitstellung verlagert. Ein Hintergrundprozess wird in einen eigenen Prozess ausgelagert. Eine API-Schicht wird extrahiert. Technisch gesehen meist die richtige Entscheidung, doch die daraus resultierende operative Komplexität wird selten im Voraus berücksichtigt.
Sechs Monate später gibt es vier cloud-Dashboards, drei Abrechnungskonten und einen README-Abschnitt mit dem Titel „Umgebungsvariablen“, der drei Seiten lang und sichtlich veraltet ist.
Das Wichtigste zum Mitnehmen: Entkoppelte Architekturen beginnen nicht mit einer Anbietervielfalt. Sie bauen diese schrittweise auf – mit jeder einzelnen Entscheidung für das „beste Tool für diese bestimmte Aufgabe“ –, bis die Betriebsfläche zu groß wird, um sie übersichtlich zu verwalten.
Niemand hat von vornherein vor, sein Frontend auf Vercel, seine API auf Render, seine Hintergrundjobs auf Railway und seine Datenbank auf Supabase zu betreiben. Es geschieht Schritt für Schritt, wobei jede einzelne Entscheidung für sich genommen vernünftig ist.
Vercel eignet sich wirklich gut für das Deployment von Next.js. Render macht es einfach, ein Django- oder FastAPI-Backend zu betreiben, ohne einen Server konfigurieren zu müssen. Railway bietet eine übersichtliche Oberfläche für verwaltetes Postgres. Jede Wahl ist auf das jeweilige Problem zugeschnitten, und im Moment der Entscheidung werden die Integrationskosten aufgeschoben.
Die Integrationskosten kommen aber immer irgendwann. Sie zeigen sich in Form von Umgebungsvariablen, die über vier Anbieter hinweg synchronisiert bleiben müssen. Sie zeigen sich beim Debuggen eines CORS-Fehlers, der nur in der Preview-Umgebung auftritt, weil das Frontend dort auf einer anderen Subdomain läuft als in der Produktivumgebung. Sie zeigen sich bei der Nachanalyse eines Vorfalls, der auf eine fest codierte URL zurückzuführen ist, die auf das Staging-Backend statt auf die Produktivumgebung verwies.
Das zugrunde liegende Problem ist, dass die Komponenten der Anwendung als ein einziges System konzipiert, aber als separate Einheiten bereitgestellt wurden. Das Betriebsmodell passt nicht zur Architektur.
Wichtigste Erkenntnis: Wenn Anwendungskomponenten auf verschiedenen Plattformen laufen, ist die Vernetzung zwischen ihnen implizit, manuell und ungetestet. Eine deterministische interne Vernetzung, bei der die Adressen der Dienste bereits zum Zeitpunkt der Erstellung bekannt sind und sich zwischen den Umgebungen nicht ändern, beseitigt eine ganze Kategorie umgebungsspezifischer Fehler.
Wenn ein Next.js-Frontend eine Django-API aufruft, muss ihm jemand mitteilen, wo sich diese API befindet. In der Praxis ist das meist eine Umgebungsvariable: NEXT_PUBLIC_API_URL=https://api.myapp.com. Diese Variable hat in der lokalen Entwicklung, in der Preview-Umgebung, im Staging und in der Produktivumgebung jeweils einen anderen Wert. Diese Werte korrekt und synchron zu halten, ist manuelle Arbeit, die außerhalb der Versionskontrolle stattfindet – und sie scheitert auf eine Weise, die beim Debuggen nervt.
Die Fehlermuster sind meist nicht dramatisch. Sie sind subtil: Eine Preview-Umgebung, die stillschweigend die Produktions-API aufruft, weil jemand vergessen hat, die Variable zu aktualisieren. Eine Staging-Umgebung, die mit dem Backend der letzten Woche testet, weil die Bereitstellung nicht weitervermittelt wurde. Eine CORS-Richtlinie, die in der Produktivumgebung funktioniert, aber Anfragen in der Branch-Umgebung blockiert, weil die Ursprungsdomain unterschiedlich ist.
Deterministisches Netzwerkmanagement umgeht das Problem, indem es interne Adressen konsistent und bekannt macht. Wenn zwei Anwendungen im selben Upsun-Projekt laufen, können sie sich über einen festen internen Hostnamen erreichen, der sich zwischen den Umgebungen nicht ändert. Das Frontend weiß immer, wo sich das Backend befindet, ohne dass du eine Umgebungsvariable setzen oder synchronisieren musst. Die Adresse wird durch die Projektkonfiguration festgelegt, nicht davon, wer zuletzt das Secrets-Dashboard aktualisiert hat.
Das Wichtigste auf einen Blick: Die Definition eines entkoppelten Stacks in einer einzigen Konfigurationsdatei bedeutet, dass die Beziehungen zwischen den Anwendungen explizit, versionsverwaltet und in jeder Umgebung automatisch reproduziert werden – einschließlich der Preview-Umgebungen für jeden Branch.
Upsun ist eine Platform-as-a-Service, die die Infrastrukturschicht deines Anwendungsstacks verwaltet, sodass dein Team dies nicht tun muss. Bei Multi-App-Projekten bedeutet das, dass der gesamte Stack – Frontend, Backend und Dienste – in einer einzigen „.upsun/config.yaml“-Datei definiert wird. Hier ist ein Projekt mit einem Next.js-Frontend und einem Python-FastAPI-Backend, die sich eine Postgres-Datenbank teilen:
applications:
frontend:
type: nodejs:22
source:
root: "frontend" # path to this app within the repo
relationships:
api: # gives the frontend a fixed internal address for the backend
service: "backend"
endpoint: "http"
web:
commands:
start: "npm run start"
backend:
type: python:3.12
source:
root: "backend"
relationships:
database: # wires the backend to the Postgres service below
service: "db"
endpoint: "postgresql"
web:
commands:
start: "uvicorn main:app --host 0.0.0.0 --port $PORT"
services:
db:
type: postgresql:16 # platform manages patching within this version
routes:
"https://{default}/":
type: upstream
upstream: "frontend:http" # only the frontend is public; the backend stays internal-only
Im Block „relationships“ wird die Netzwerkkonfiguration definiert. Das Frontend hat eine Beziehung zum Backend namens „api“, was bedeutet, dass es das Backend unter einer vorhersehbaren internen Adresse erreichen kann. Das Backend hat eine Beziehung zur Datenbank. Keine der beiden Beziehungen erfordert eine fest codierte URL oder eine manuell gepflegte Umgebungsvariable außerhalb der Konfigurationsdatei.
Wenn ein Entwickler einen Branch eröffnet, startet die Plattform sowohl die Anwendungen als auch die Datenbank, verknüpft sie mit derselben Beziehungsstruktur und weist jedem eine konsistente interne Adresse zu. Das Frontend in der Preview-Umgebung ruft das Backend in der Preview-Umgebung auf, nicht das Backend in der Produktivumgebung.
Genau das ist es, was dir eine Multi-Provider-Konfiguration nicht ohne Weiteres bieten kann: eine Isolierung der Umgebungen, die sich über das gesamte Netzwerk erstreckt und nicht nur auf die Anwendungscontainer beschränkt ist.
Das Wichtigste auf einen Blick: Ein einziges Projekt für den gesamten Stack bedeutet eine einzige Deployment-Pipeline, einen einzigen Ort zum Überprüfen der Logs, eine einzige Rechnung und Umgebungsparität, die auch das Netzwerk umfasst. Der operative Aufwand für den Betrieb einer entkoppelten Architektur sinkt erheblich.
Die unmittelbaren praktischen Unterschiede liegen auf der Hand. Eine einzige Deployment-Pipeline deckt den gesamten Stack ab. Logs aus Frontend und Backend befinden sich am selben Ort. Die monatliche Rechnung kommt von einer einzigen Stelle.
Der weniger offensichtliche Unterschied betrifft das Onboarding und das Debugging.
Bei einer Konfiguration mit mehreren Anbietern benötigt ein neuer Entwickler, der zum Team stößt, Konten auf vier Plattformen, Zugriff auf vier Dashboards und ein praktisches Verständnis dafür, wie die einzelnen Teile miteinander verbunden sind. Der Abschnitt „Umgebungsvariablen“ in der README-Datei existiert, weil es keine andere Möglichkeit gibt, eine Netzwerktopologie zu dokumentieren, die sich über die Einstellungsbereiche von vier verschiedenen Anbietern erstreckt.
Bei einer Ein-Projekt-Konfiguration befindet sich die Topologie in der Konfigurationsdatei. Ein neuer Entwickler klont das Repository, und die Konfiguration sagt ihm genau, woraus der Stack besteht, wie die Komponenten zueinander in Beziehung stehen und welche Versionen gerade laufen. Der Abschnitt über Umgebungsvariablen in der README-Datei wird dadurch viel kürzer.
Das Gleiche gilt für das Debugging. Wenn an der Schnittstelle zwischen Frontend und Backend etwas schiefgeht, befinden sich die relevanten Protokolle an derselben Stelle. Das Netzwerk zwischen ihnen ist in derselben Datei definiert wie alles andere. Man muss nicht zwischen Dashboards wechseln oder Zeitstempel über vier separate Protokollierungsschnittstellen hinweg abgleichen.
Für die Migration muss nicht alles auf einmal umgestellt werden. Die Konfigurationsdatei beschreibt den Zielzustand des Stacks. Ein praktischer Ansatz ist daher, mit einer einzelnen Komponente zu beginnen – typischerweise dem Backend und seiner Datenbank –, diese in Upsun zu definieren und von dort aus die interne Vernetzung aufzubauen. Das README-Problem bei mehreren Anbietern wird schon ab dem ersten Dienst, der umgestellt wird, kleiner.
Können die Anwendungen in einem Projekt völlig unterschiedliche Sprachen und Laufzeiten verwenden?
Ja. Ein Projekt kann ein Node.js-Frontend, ein Python-Backend und einen Java-Worker-Prozess enthalten, die jeweils in einem eigenen, isolierten Container laufen. Die Plattform verwaltet die Vernetzung zwischen ihnen, unabhängig davon, womit sie erstellt wurden.
Woher kennt das Frontend die interne Adresse des Backends?
Upsun fügt Beziehungsinformationen zur Laufzeit als Umgebungsvariablen ein. Das Frontend erhält eine Reihe von Variablen, die beschreiben, wie das Backend erreicht werden kann: Host, Port und Schema. Diese sind über alle Umgebungen hinweg konsistent, sodass derselbe Code für das Programmieren in jedem Branch ohne Änderungen funktioniert.
Wie funktioniert die Netzwerkverbindung in Preview-Umgebungen?
Jede Branch-Umgebung erhält eine eigene, isolierte Instanz jeder Anwendung im Projekt, die mit derselben Beziehungsstruktur wie in der Produktivumgebung miteinander verbunden sind. Das Frontend in einer Branch-Umgebung ruft das Backend in derselben Branch-Umgebung auf. Die Beziehungsverbindungen in einer Branch-Umgebung verweisen auf die eigenen Dienste dieser Umgebung, nicht auf die der Produktivumgebung.
Bedeutet das, dass alle Dienste in einem Repository definiert werden müssen?
Nicht unbedingt. Upsun unterstützt Source-Operationen und externe Integrationen, die es ermöglichen, dass Komponenten in separaten Repositories liegen und dennoch als ein einziges Projekt orchestriert werden. Für die meisten Teams, die gerade erst anfangen, ist ein einziges Repository einfacher, aber die Plattform schreibt dies nicht vor.
Inwiefern unterscheidet sich das von Docker Compose?
Docker Compose löst die Orchestrierung der lokalen Entwicklung. Es verwaltet jedoch keine Bereitstellungen, keine Umgebungskonformität, keine Skalierung und keine Vernetzung über mehrere Live-Umgebungen hinweg. Eine einzige Upsun-Projektkonfigurationsdatei deckt all das ab – von der Branch-Umgebung des Entwicklers bis hin zur Produktivumgebung – und zwar mit derselben Konfiguration.