Wie Glaspoort Datenbankänderungen so präzise wie Anwendungscode bereitstellt: durch Branching jeder Umgebung aus der Produktion, temporäre Datenbanken pro PR und Migrationen als Single Source of Truth.
von Hadi Farhat, Gideon Spierings, Ricardo de Vries und Raymon Veldman
Glaspoort baut und betreibt Glasfaserinfrastruktur in den Niederlanden. Alles dreht sich darum, die Anzahl der Glasfaseranschlüsse zu erhöhen. Lange Zeit verbrachte das Datenteam seine Tage damit, BI-Berichte zur Unterstützung dieses Ziels zu erstellen, während die Frage hinter jedem Bericht bereits veraltet war, als der Bericht fertiggestellt wurde. Das Ergebnis war eine unkontrollierte Zunahme von Einmalberichten und Nutzer, die keine Anlaufstelle für ihre Folgefragen hatten.
Also haben wir diese Abhängigkeit aufgelöst. Anstatt den nächsten Bericht bereitzustellen, haben wir eine maßgeschneiderte Frontend-Anwendung entwickelt, in der Projektmanager direkt sehen, wo die Chancen für ihre Projekte liegen. Das Neue befindet sich unter der Haube: Wir nutzen Databricks-Produkte direkt als Bausteine in der App. Genie, um mit den Daten zu chatten und schnelle Analysen zu erstellen. AI/BI-Dashboards für Erkenntnisse und Self-Service-Analysen. Automatisierte Workflows mit Agent Bricks, die Projektmanager benachrichtigen, sobald bei ihren Projekten etwas auffällt, und Lakebase, die Databricks-OLTP-Datenbank, für die Transaktionsdaten der Anwendung.
Diese Kombination führt zwei Welten zusammen, die bis vor Kurzem getrennt waren: die Analyseumgebung und ein operatives Frontend, in dem Analysedaten auf Transaktionsdaten treffen. Das Datenteam verbringt seine Zeit nun mit Genie-Spaces und Metadaten statt mit Einmalberichten. Aber nichts davon bleibt ohne ein solides Fundament schnell: CI/CD, Datenqualitätstests, Infrastructure as Code und Data Governance. Ein Teil dieses Fundaments erforderte den sorgfältigsten Entwurf, und genau um diesen Teil geht es im Rest dieser Geschichte: wie wir Änderungen an der Datenbank hinter der App bereitstellen.
Hinter dieser Anwendung befindet sich eine Databricks-Lakebase-Datenbank. Es handelt sich um eine serverlose Postgres-OLTP-Datenbank, die direkt neben dem Lakehouse läuft, anstatt nur daran angeflanscht zu sein. Der Datenfluss ist einfach zu beschreiben und, wie wir feststellen mussten, im Betrieb interessanter, als es aussieht:

Genau bei diesem letzten Punkt stellt sich die interessante Frage. In dem Moment, in dem man entscheidet, dass eine OLTP-Datenbank dieselbe Sorgfalt verdient wie Anwendungscode, muss man eine schwierige Frage beantworten:
Wie ermöglichen wir es, dass jeder PR gegen eine Datenbank getestet wird, die wie die Produktion aussieht, ohne dass sich die Entwickler gegenseitig in die Quere kommen und ohne die aktuellen Daten zu verlieren, die den Test überhaupt erst aussagekräftig machen?
Die Fehlerszenarien hierbei sind jedem bekannt, der schon einmal eine Datenbank im Team geteilt hat: PRs, die isoliert funktionieren, aber fehlschlagen, wenn sie zusammengeführt werden. Entwicklungs- und Abnahmeumgebungen, die sich unbemerkt von der tatsächlichen Produktion entfernt haben. Und der gefürchtete Refresh-Tag, an dem das Wiederherstellen sauberer Daten bedeutet, Umgebungen abzubauen, jede Verbindungszeichenfolge (Connection String) neu zu verkabeln und jede Berechtigung manuell neu zu vergeben.
In diesem Beitrag geht es darum, wie wir das meiste davon vermieden haben, und um die eine Entscheidung, über die wir immer noch aktiv diskutieren.
Wenn Sie Lakebase-Branches noch nicht genutzt haben, ist dies das einzige mentale Modell, das Sie für diesen Beitrag benötigen.
Ein Lakebase-Branch ist ein Copy-on-Write-Postgres-Branch von einem übergeordneten Branch (Parent). Beim Erstellen werden die Daten nicht kopiert; der Zustand wird kostengünstig und sofort abgezweigt (forked), und der Branch weicht erst dann von seinem Parent ab, wenn Sie darauf schreiben. Jeder Branch ist vollständig isoliert, mit eigenem Endpunkt und eigenen Daten. Sie können einen Branch in Sekundenschnelle erstellen und ebenso schnell wieder löschen.
Wenn das nach Git klingt, ist das genau der Punkt. Die Analogie, die allem Folgenden zugrunde liegt, ist einfach: Ein Feature-Branch in Git entspricht einem Datenbank-Branch in Lakebase. Ein PR erhält seinen eigenen Code-Branch und seinen eigenen Datenbank-Branch, Tests laufen gegen beide, und wenn alles passt, wird die Änderung in die Produktion übernommen (promoted).
Es gibt eine Einschränkung, die Sie für den nächsten Abschnitt im Hinterkopf behalten sollten, da sie einen entscheidenden Unterschied zum Git-Branching darstellt: Um einen Branch von seinem Parent zurückzusetzen, müssen Sie zuerst die eigenen Child-Branches dieses Branches löschen. Ein Parent-Branch kann nicht zurückgesetzt werden, solange Branches von ihm abhängen.
Hier ist das Design, zu dem die meisten Teams zuerst greifen, weil es der Art und Weise entspricht, wie wir Umgebungen auf einem Whiteboard zeichnen:
Es ist eine saubere Hierarchie: Produktion an der Wurzel, dann Dev, und darunter hängen Acceptance und Features. Dies führt jedoch auch zu zwei vorhersehbaren Fehlerszenarien: Die Branches driften auseinander (Drift), und die Behebung dieses Drifts ist so aufwendig, dass Teams sie irgendwann nicht mehr durchführen.
Erstens driften Dev und Acceptance von der Produktion ab. Die Produktion erhält kontinuierlich neue synchronisierte Daten und echte Anwendungsschreibvorgänge; die langlebigen Dev- und Acceptance-Branches tun dies nicht. Innerhalb von ein oder zwei Sprints testen Sie gegen eine Datenbank, die derjenigen, auf die Sie deployen, nicht mehr ähnelt.
Die naheliegende Lösung besteht darin, Dev aus der Produktion zu aktualisieren (Refresh). Hier wird die Einschränkung aus dem letzten Abschnitt jedoch zu einer echten Belastung. Um die Entwicklung von ihrem Parent zurückzusetzen, müssen Sie zuerst die Child-Branches der Entwicklung löschen. In dieser Topologie bedeutet das: Acceptance und jeden Feature-Branch, der an Dev hängt. So wird aus einem routinemäßigen „Ich brauche frische Daten“ eine Kaskade: Löschen von Acceptance und allen Feature-Branches, Zurücksetzen der Entwicklung, Neuerstellung der Umgebungen, Neuverkabelung jedes Connection-Strings, der auf die alten Branches verwies, und erneutes Zuweisen aller Postgres-Berechtigungen (Grants), da diese auf dem gerade gelöschten Branch lagen.

Keiner dieser Schritte ist für sich genommen schwierig. Zusammen und auf regelmäßiger Basis führen sie jedoch zuverlässig dazu, dass das gesamte Team Aktualisierungen stillschweigend vermeidet. Das bedeutet, dass alle wieder mit veralteten, abgedrifteten Daten testen. Das war das ursprüngliche Problem. Die naive Topologie kostet Sie nicht nur einen schlechten Nachmittag, sondern verhindert auch die Disziplin, die für eine saubere Umgebung sorgt.
Die Lösung ist eine minimale Änderung in der Denkweise über die Topologie, die jedoch enorme Auswirkungen hat: Jeder langlebige Umgebungs-Branch ist ein Child-Branch der Produktion, nicht einer anderen Umgebung.
Development und Acceptance sind beides Branches, die direkt aus der Produktion erstellt wurden. Sie befinden sich nebeneinander unter der Produktion und sind nicht übereinander gestapelt. Keiner ist der Parent des anderen, sodass das Aktualisieren des einen Sie niemals dazu zwingt, den anderen zu löschen.
Diese einzige Änderung entschärft die Falle. Wenn Dev oder Acceptance abdriften, setzen wir sie aus der Produktion zurück (heute eine einfache UI-Aktion) – etwa einmal pro Sprint oder wann immer wir frische Daten benötigen. Da nichts unter Dev oder Acceptance hängt, müssen keine Child-Branches zuerst gelöscht werden, keine Connection-Strings im Team neu verteilt werden und es gibt keinen Marathon zur erneuten Vergabe von Berechtigungen. Das Zurücksetzen ist unkompliziert, also tun wir es auch, wodurch die Umgebungen sauber bleiben. Sollte bei einem Reset eine Änderung übersehen werden, wendet das nächste Migrations-Replay sie einfach wieder an.

Animiert: der Lebenszyklus pro PR. Wird im veröffentlichten Blog abgespielt; hier als Standbild in diesem Dokument dargestellt.
Der Ablauf für Entwickler im Alltag ergänzt diese stabile Topologie um kurzlebige Branches:
Ein Detail ist besonders erwähnenswert für Teams, die aus einer Git-zentrierten Welt kommen. Wir behalten in Git nur einen Master-Branch. Es gibt keinen langlebigen Develop- oder Release-Branch in der Versionsverwaltung. Der Promotion-Pfad von Dev über Acceptance zu Production wird vollständig über manuell freigegebene Gates in Azure DevOps abgebildet, die die CI anstoßen und eine Änderung von einem Branch zum nächsten verschieben. Die Umgebungstopologie liegt in Lakebase und der Pipeline; Git bleibt einfach.
Das, was all dies so sicher macht, ist, dass wir Datenbanken niemals wieder ineinander mergen. Wir überführen eine Änderung nicht, indem wir Daten von einem Feature-Branch nach Development kopieren. Wir überführen sie, indem wir die Migrationen erneut auf dem Ziel-Branch ausführen. Wir verwenden vitest, um den Smoke-Test für den Staging-Slot auszuführen.
Branches sind in diesem Modell bewusst kurzlebig und verwerfbar. Die dauerhafte, verbindliche Beschreibung des Schemas ist keine langlebige Datenbank, die im Laufe der Zeit abgewichen sein könnte. Es ist die geordnete Reihe von Migrationen. Deshalb ist ein Reset auch dann sicher, wenn etwas übersehen wird: Wenn in einem aktualisierten Branch eine Änderung fehlt, wird sie beim nächsten Abspielen der Migrationen einfach wieder angewendet. Die Migrationen sind so zentral, dass die Live-Datenbanken daraus jederzeit reproduziert werden können, anstatt dass sie selbst als unersetzlich gelten.
Das ist der eigentliche Durchbruch. Das Abzweigen von der Production hält die Daten aktuell. Die Migrationen als Source of Truth zu behandeln, sorgt für ein korrektes Schema. Zusammen bedeutet das, dass keine Umgebung jemals zu speziell ist, um sie nicht einfach wegzuwerfen und neu aufzubauen.
Wir glauben, dass dieser Teil für andere Teams am nützlichsten ist, da es sich um eine echte Richtungsentscheidung handelt und nicht um eine Best Practice mit nur einer richtigen Antwort. Sobald Sie die obige Topologie eingerichtet haben, müssen Sie sich entscheiden, wann ein PR gemergt werden darf. Wir haben zwei Modelle evaluiert, die gegensätzliche Vor- und Nachteile abwägen.
| Option 1: Mergen nach erfolgreicher CI (PR-Stacking) | Option 2: Mergen nach CD auf Dev und Acceptance (Promotion pro PR) | |
|---|---|---|
| Funktionsweise | Ein PR kann gemergt werden, sobald die CI auf seinem pr-xxxx-Branch grün ist. | Ein PR kann erst gemergt werden, wenn er die Phasen Development und Acceptance durchlaufen hat. |
| Vorteile | Teammitglieder können sofort auf den Änderungen der anderen aufbauen. | Jeder PR bewegt sich in seinem eigenen Tempo; Hotfixes werden bevorzugt behandelt und müssen sich nicht hinten anstellen. |
| Nachteile | Mehrere gemergte PRs stapeln sich bei der Überführung in Dev und Acceptance. Ein Hotfix kann die Warteschlange nicht ohne eine parallele Pipeline umgehen. | Teammitglieder müssen warten, bis Acceptance grün ist, bevor sie auf einer Änderung aufbauen können. |
Kurz gesagt: Option 1 optimiert die Entwicklungsgeschwindigkeit in einem eng zusammenarbeitenden Team. Option 2 optimiert die Unabhängigkeit und bietet einen sauberen Hotfix-Pfad. Wenn Ihr Team an sich überschneidenden Änderungen arbeitet und der CI vertraut, bringt Sie das Stacking am schnellsten voran. Wenn Sie häufig Fehlerbehebungen ausliefern müssen, die nicht auf andere laufende Änderungen warten können, macht sich die Promotion pro PR bezahlt.
Wir haben uns für Option 1 entschieden. Wir sind ein kleines Team, das an sich überschneidenden Änderungen arbeitet. Darauf zu warten, dass Acceptance grün wird, bevor jemand auf der eigenen Arbeit aufbauen kann, wäre ein hoher Preis, den wir täglich zahlen müssten, während das Stacking-Risiko nur gelegentlich auftritt. Und wenn es auftritt, ist es eingegrenzt: Jedes Promotion-Gate führt die gestapelten Migrationen auf einem frischen Fork der Production aus, bevor der eigentliche Branch berührt wird, sodass ein Stack als Einheit validiert wird. Für wirklich dringende Fälle unterhalten wir eine separate Notfall-Pipeline, die mit derselben Preflight-Prüfung direkt in die Production führt. Standardmäßig Geschwindigkeit, Notausstieg in Bereitschaft.
In den Arbeitssitzungen kamen immer wieder einige Themen zur Sprache. Sie sind zwar nicht der Kern der Geschichte, aber es sind die Details, die darüber entscheiden, ob das Muster den ersten Praxistest in der Production übersteht.
Datenbank-Authentifizierung und Token-Rotation. Die App speichert kein langlebiges Datenbank-Secret. Ein statisches Passwort in Azure Key Vault wird verwendet, um über `databricks postgres generate-database-credential` kurzlebige Datenbank-Anmeldedaten (ein Token mit einer TTL von 60 Minuten) zu generieren. Da das Token nach einer Stunde abläuft, muss der Client es proaktiv aktualisieren. Wir lösen dies über eine asynchrone Passwort-Funktion im Connection-Pool, die die Anmeldedaten zwischenspeichert und rotiert, bevor sie ablaufen.
Berechtigungen und Zugriffsbereitstellung. Wir verwalten die Tabellen-DDL nicht über Migrationen, und Berechtigungen auf Objektebene werden derzeit manuell und nicht über Code angewendet. Das ist der eindeutigste Punkt auf unserer Verbesserungsliste. Da wir diese Berechtigungen jedoch in der Production anwenden und Umgebungs-Branches durch erneutes Forken von der Production zurückgesetzt werden, erben Child-Branches diese bei jeder Aktualisierung automatisch. Der manuelle Berechtigungsschritt erfolgt einmal in der Production und nicht wiederholt bei jedem Child-Branch, sodass dies selten genug vorkommt, um vorerst damit leben zu können.
Datenbankzugriff: ein einziger Anwendungsbenutzer. Wir stellen nicht für jeden Endbenutzer eine Postgres-Rolle bereit. Die App stellt die Verbindung über einen einzigen Anwendungsbenutzer her, und die Autorisierung für einzelne Benutzer erfolgt in der Anwendungsschicht und nicht in der Datenbank. Dies hält die Bereitstellung einfach. Der Nachteil ist, dass die benutzerbezogene Zugriffskontrolle oberhalb von Postgres erzwungen wird und nicht durch Postgres selbst.
Ein Anwendungsfall, für den früher ein ganzes Team und monatelange Arbeit nötig waren, geht jetzt mit einem kleinen Team in wenigen Tagen live. Unsere Iterationen haben sich verzehnfacht, und ein einziges Team liefert kontinuierlich neuen Mehrwert auf einer Plattform, die mit dem Unternehmen mitwächst.—Raymon Veldman, Business Control & IT Manager
Die sichtbarste Veränderung ist mehr Geschwindigkeit bei weniger Sorgen. Jeder PR erhält nun in wenigen Minuten eine eigene Datenbank im Format der Production. Dadurch müssen sich Entwickler nicht mehr für gemeinsam genutzte Dev- und Acceptance-Umgebungen abstimmen, und die Diskussionen darüber, wer Acceptance kaputtgemacht hat, gehören der Vergangenheit an. Da Umgebungs-Branches immer Child-Branches der Production sind, gibt es keinen „Reset-from-Parent“-Tag mehr: kein Löschen von Child-Branches, kein mühsames Anpassen von Connection Strings, keine verlorenen Nachmittage für das erneute Zuweisen von Berechtigungen. Die Migrationen werden bei jedem Promotion-Schritt erneut abgespielt. Wir stellen das neue App-Image in einem Staging-Slot für den frisch migrierten Branch bereit und führen die vollständige Testsuite aus, bevor irgendetwas eine echte Umgebung berührt. Was die Production erreicht, hat sich also bereits zweimal bewährt. Und wenn etwas Dringendes ansteht, nehmen Hotfixes denselben sicheren Weg wie alles andere auch – nur schneller, anstatt die Pipeline zu umgehen.
Sie entscheiden sich für Lakebase als Anwendungsdatenbank und schlagen sich mit denselben CI/CD-Fragen herum? Fangen Sie hier an: langlebige Umgebungen, die direkt von der Production abgezweigt werden, kurzlebige Branches pro PR und Migrationen als einzige Source of Truth. Erfahren Sie mehr über Databricks Lakebase
(Dieser Blogbeitrag wurde mit KI-gestützten Tools übersetzt.) Originalbeitrag
Abonnieren Sie unseren Blog und erhalten Sie die neuesten Beiträge direkt in Ihren Posteingang.