Direkt zum Hauptinhalt
Produkt

Datenbanken wie Code verzweigen: ein CI/CD-Muster für Lakebase, im Produktiveinsatz bei Glaspoort

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

  • Wie Glaspoort sein Lakebase-Branching-Setup entworfen hat, bei dem Entwicklung und Acceptance direkt aus der Produktion abgezweigt werden, anstatt sie hintereinanderzuschalten. Dies verhindert die "Reset-from-Parent-Falle", bei der das Aktualisieren einer veralteten Umgebung dazu führt, dass alles darunter gelöscht und neu aufgebaut werden muss.
  • Der CI/CD-Flow pro PR: Wie jeder Pull Request seinen eigenen, temporären Lakebase-Branch erhält, der aus der Produktion kopiert wird; wie Migrationen auf ein Live-App-Image angewendet und getestet werden, bevor etwas eine echte Umgebung berührt; und warum die Migrationen selbst (nicht die Datenbanken) als Single Source of Truth behandelt werden.
  • Die Abwägung zwischen zwei Promotion-Modellen: Mergen, sobald die CI erfolgreich ist, versus Mergen erst, wenn ein PR vollständig in die Acceptance-Umgebung überführt wurde. Der Beitrag erklärt, warum sich Glaspoort für den "Velocity-First"-Ansatz entschieden hat und welche Sicherheitsvorkehrungen (Stack-Revalidierung und eine Krisen-Pipeline) getroffen wurden, um die Sicherheit zu gewährleisten.

Das Problem, das wir nicht ignorieren konnten

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:

  • Aufbereitete Daten aus dem Lakehouse werden in einen Lakebase-Produktions-Branch synchronisiert, wo sie in einem schreibgeschützten Anwendungsschema landen.
  • Die Anwendung schreibt ihren eigenen Zustand zurück in ein separates Schema auf demselben Branch, sodass die aus dem Lakehouse gelesenen Daten und die von der App geschriebenen Daten nebeneinander existieren, ohne zu kollidieren.
  • Darüber hinaus betreiben wir drei logische Umgebungen: Entwicklung, Abnahme (Acceptance) und Produktion. Wir stellen Änderungen an dieser Datenbank auf dieselbe Weise bereit wie Änderungen am Anwendungscode – über Pull Requests, CI und kontrollierte Freigaben (gated promotion).

image3.png

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.

Lakebase-Branching in 60 Sekunden

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.

Die Reset-from-Parent-Falle

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:

  • Ein einzelner, langlebiger Entwicklungs-Branch (Development), der von der Produktion abgezweigt ist.
  • Ein Abnahme-Branch (Acceptance), der von der Entwicklung abgezweigt ist.
  • Feature-Branches, die von der Entwicklung abgezweigt sind.

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.

image1.png

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.

Das Glaspoort-Design: Immer von der Produktion abzweigen

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.

image2.gif

Animiert: der Lebenszyklus pro PR. Wird im veröffentlichten Blog abgespielt; hier als Standbild in diesem Dokument dargestellt.

Der Ablauf pro PR

Der Ablauf für Entwickler im Alltag ergänzt diese stabile Topologie um kurzlebige Branches:

  1. Ein Entwickler öffnet einen kurzlebigen PR (TTL auf 1 Stunde festgelegt).
  2. CI erstellt einen neuen, kurzlebigen pr-xxxx-Branch aus der Produktion. Wir zweigen alles aus der Produktion ab und führen Datenbanken niemals wieder zusammen (merge). Diese PR-spezifischen Branches sind Einweg-Branches: Wir archivieren sie, sobald der PR geschlossen wird. So bleiben wir unter dem Standardlimit von Lakebase von 10 nicht archivierten Branches pro Projekt.
  3. CI spielt die Migrationen auf diesem neuen Branch ab. Eine Git-Diff-Prüfung entscheidet, ob ein Migrations-Testlauf tatsächlich erforderlich ist, sodass PRs ohne Migrationsänderungen das Replay überspringen.
  4. Wir testen die Migration nicht nur isoliert. CI stellt das neue App-Image in einem Staging-Slot bereit, verweist auf den frisch migrierten Branch und führt die gesamte Testsuite für dieses Paar aus.
  5. Jeder PR validiert die Migration und das neue Anwendungs-Image gemeinsam, bevor eines von beiden eine echte Umgebung berührt.
  6. Wenn der PR gemergt wird, führt die CI dies erneut aus. Sie zweigt erneut von der Production ab, spielt die Migrationen wieder ab und testet erneut, da sich die Umgebung seit dem ersten erfolgreichen PR-Test geändert haben kann. Wenn die Migrationen erfolgreich sind, werden sie direkt im Ziel-Branch angewendet. Für den ersten Merge ist dieses Ziel „Development“.
  7. Die Überführung in die Acceptance- und anschließend in die Production-Umgebung erfolgt über manuell freigegebene Gates, bei denen die Migrationen jeweils für den nächsten anstehenden Branch erneut abgespielt werden.

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.

Warum das erneute Abspielen von Migrationen die „Source of Truth“ ist

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.

Die Weggabelung: zwei Promotion-Modelle

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)
FunktionsweiseEin 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.
VorteileTeammitglieder 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.
NachteileMehrere 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.

Randnotizen: Was uns unterwegs begegnet ist

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.

Was dies für Glaspoort ermöglicht hat

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

Erhalten Sie die neuesten Beiträge in Ihrem Posteingang

Abonnieren Sie unseren Blog und erhalten Sie die neuesten Beiträge direkt in Ihren Posteingang.