Skip to main content
Continuous Delivery in der Praxis – Versionsverwaltung mit Git

Git und die verschiedenen Branching- und Entwicklungs-Modelle

Dies ist der erste Teil einer Serie von Blog-Beiträgen mit Fokus auf Continuous Delivery (CD)*. In dieser Serie wird jedoch mehr auf die praktische Umsetzung als auf die schöne Theorie geachtet. Aus eigener Erfahrung bei mehreren Kundenprojekten und Beratungsgesprächen ist offensichtlich, dass die Theorien von Humble, Farley, Wolff und Fowler nicht immer eins zu eins in die Praxis übertragen werden können. Vor allem unterschiedliche Erfahrungswerte von Entwicklern/innen und anderen beteiligten Personen können die Einführung von CD erschweren. Die Autoren der bekanntesten CD-Büchern oder -Artikeln betonen selbst immer wieder, dass eine Anpassung an die Projektumstände erforderlich ist. Wie genau solche Anpassungen aussehen können, soll in dieser Serie beleuchtet werden.

*unter Continuous Delivery verstehen wir Continuous Integration + das automatische Deployment in einer Staging-Umgebung. Theoretisch lassen sich die Konzepte auch auf Continuous Deployment anwenden, dann eben mit dem automatischen Deployment direkt in die Produktiv-Umgebung.

Der Prozess des Continuous Delivery in einer groben Darstellung

Eine typische Kette von Tools im CD-Prozess könnte so aussehen:

  • Versionsverwaltung

  • Versionsverwaltungs-Server

  • Build-Umgebung

  • Staging-Umgebung

Dieser Prozess ist stark vereinfacht. Dazu gehört natürlich auch die individuelle Entwicklungsumgebung, ein Repository Manager für Dependencies, diverse weitere Build- und Analyse-Tools, die an die Build-Umgebung angeschlossen werden können und auch Staging- und Produktionsverwaltungs-Tools.

In dieser Serie konzentrieren wir uns jedoch auf die oben genannten vier Tools, die nach meiner Ansicht die wichtigsten Elemente darstellen, um eine effiziente Toolkette aufbauen zu können. Wir werden also vor allem Git (als Versionsverwaltung), Bitbucket (als Versionsverwaltungs-Server), Bamboo (als Build-Umgebung) und Kubernetes (als Staging-Umgebung) betrachten. Übrigens nicht weil es die besten Tools sind (da hat jeder eine andere Meinung zu), sondern weil ich persönlich diese am häufigsten in Projekten im Einsatz finde.

Der erste Teil – Branching- und Entwicklungs-Modelle

Der erste Teil der Serie konzentriert sich auf die Versionsverwaltung und damit einhergehend auf verschiedene Branching- und Entwicklungs-Modelle. Hier geht es also hauptsächlich darum, wie Entwickler/innen Änderungen am Code vornehmen und gleichzeitig möglichst effizient arbeiten können.

Arbeiten mit der Versionsverwaltung

Git, unser Versionsverwaltungs-Tool der Wahl für diese Serie, ist die Ablage für den Quellcode – also das Herzstück jeglicher Software. Entsprechend wichtig ist es, hier einen geregelten Ablauf zu etablieren. Ansonsten kommt man schneller in die sogenannte Merge-Hölle als einem lieb ist.

Für weniger erfahrene Entwickler/innen können Versionsverwaltungs-Server und IDE’s in diesem Zusammenhang eine große Unterstützung darstellen. Da es jedoch so viele IDE’s und andere Tools mit unterschiedlich tiefen Integrationen von Git gibt, werde ich hier nicht weiter darauf eingehen. Die Wahl der IDE sollte sowieso nach individueller Präferenz erfolgen. Dementsprechend gehe ich davon aus, dass sich jede/r Entwickler/in selbst am besten mit den Funktionen der eigenen IDE auskennt. Außerdem werden wir Versionsverwaltungs-Tools wie Bitbucket im nächsten Part der Serie genauer beleuchten und damit in diesem Artikel größtenteils außen vor lassen.

Versionsverwaltungen bieten ein beliebig komplexes Branching-System, mit denen mehrere Entwickler/innen theoretisch gleichzeitig am Code arbeiten können, ohne sich gegenseitig in die Quere zu kommen. In der Praxis ist das jedoch häufig einfacher gesagt als getan.

Das git-flow-Modell

Besonders bekannt ist „git-flow“. In diesem Modell gibt es 5 Branch-Typen:

  • master

  • develop

  • feature/x

  • release/x

  • hotfix/x

Jeder Branch hat hier eine eigene spezielle Funktion und es gibt genaue Arbeitsabläufe, welcher Branch-Typ von welchem Branch-Typ abgezweigt und in welchen Branch-Typ wieder gemerged werden soll. Dieser Prozess soll die gemeinsame Arbeit an einem einzelnen Repository fördern.

Das Problem mit git-flow

Häufig kommt man in ein Projekt, in dem angeblich nach „git-flow“ gearbeitet wird. Tatsächlich wurden aber diverse Anpassungen oder Ausnahmen hinzugefügt. Zum Beispiel wird zwischendurch noch eine kleine Änderung direkt auf develop durchgeführt oder direkt von develop auf master gemerged, statt korrekterweise erst einen Release-Branch von develop zu ziehen und dann auf master zu mergen.

Beliebt ist auch ein Feature-Branch pro Entwickler/in zu verwenden, statt ein Feature-Branch pro Feature. Dies ist meines Erachtens häufig dem Umstand geschuldet, dass Teams in agilen Umgebungen die Aufgaben pro Entwickler/in aufteilen. Statt jedoch ein Branch pro Story (eine Story ist nunmal ein Feature) zu erstellen, wird die Story in Tasks unterteilt. Jeder Task wird einer Person zugewiesen, die sich dann wiederum einen Feature-Branch erstellt. Jira vereinfacht diese Art der Branch-Erstellung zusätzlich, indem aus einer Story heraus direkt ein Branch in Bitbucket erstellt werden kann. Jede/r Entwickler/in kann so einfach für seine Aufgabe die entsprechenden Änderungen am Code vornehmen und darauf basierend hinterher unter anderem seine Abrechnung schreiben.

Auf diese Weise werden zum einen viele unnötige Branches erstellt. Zum anderen ist jedoch die starke Abhängigkeiten zwischen den Entwickler- bzw. Story-Branches das größte Problem. Immerhin haben ja alle Tasks einer Story das gleiche Ziel. Entsprechend wird häufig für mehrere Tasks am selben Teil (zum Beispiel dieselbe Klasse) der Software gearbeitet. Merge-Konflikte sind in diesem Fall meistens vorprogrammiert.

Das Problem an git-flow ist also, dass sich einfach niemand wirklich daran hält. Dies ist nach meiner Beobachtung häufig der Komplexität der Arbeitsabläufe, gepaart mit der Unerfahrenheit der Entwickler/innen geschuldet.

Anpassungen am git-flow-Modell sind grundsätzlich jedoch nicht falsch, denn je nach Projekttyp machen Abwandlungen tatsächlich sehr viel Sinn. Git-flow stellt im Grunde das Maximum an Komplexität dar, welche häufig gar nicht benötigt wird. Warum also sollte ein Projekt zum Beispiel Release-Branches erstellen müssen, wenn nicht mehrere Versionen der Software parallel zur Verfügung gestellt werden müssen? Oder wieso Hotfix-Branches erstellen, wenn die Bugs sowieso mit der nächsten Version gefixed werden können? Wieso sollte man develop und master trennen, wenn wir sowieso häufiger bzw. direkt in die Produktion deployen wollen (Stichwort Continuous Deployment)?

Trunk-based Entwicklung

Wenn man sich mit den Büchern und Aussagen der Vordenker von Continuous Delivery bzw. Continuous Integration befasst, wird man häufig auf die Empfehlung stoßen, überhaupt kein Branching-Modell zu verwenden. Stattdessen wird eine „trunk-based“ Entwicklung empfohlen. Übertragen auf Git könnte man es auch als „master-based“ bezeichnen. Alle Entwickler/innen arbeiten nach diesem Modell gleichzeitig auf dem gleichen Branch (master in dem Fall). Dadurch fühlt sich die Entwicklung wesentlich effizienter und schneller an. Merge-Konflikte werden durch frühzeitiges Mergen von kleinen Änderungen größtenteils vermieden. Außerdem ist die trunk-based Entwicklung quasi eine Voraussetzung für echtes Continuous Deployment.

Probleme mit trunk-based Entwicklung

Jemand, der schon etwas Erfahrung mit Git hat, wird an dieser Stelle anmerken, dass es bei trunk-based Entwicklung noch viel häufiger zu Merges kommen wird. Das ist aber auch so gewollt, denn der Umfang der Änderungen ist kleiner, wodurch es zu insgesamt weniger Konflikten beim Mergen kommen soll.

Voraussetzung ist, dass die Entwickler/innen ihre Änderungen in kleinen Schritten erledigen, die sich jederzeit in den Haupt-Branch integrieren lassen, ohne Fehler bei anderen Entwicklern/innen hervorzurufen. Das erfordert viel Disziplin und eine hohe Organisation der Entwicklungsaufgaben in kleinteilige (funktionierende!) Schritte. Sollten die Änderungen zwischen zwei Commits zu groß werden, kann man also genauso gut auch wieder mit Branches arbeiten.

Feature Toggles zur Rettung?

Feature-Toggles sind zwar aus genereller Sicht losgelöst von der Versionsverwaltung, werden jedoch häufig mit trunk-based Entwicklung in Verbindung gebracht. Dabei wird jedes Feature eben nicht mittels Branches sondern direkt im Code isoliert. Wie man dies umsetzt, kann im Projekt unterschiedlich gehandhabt werden. Zum Beispiel gibt es die Möglichkeit beim Starten der Software einzustellen (zum Beispiel mittels einer Datei oder per Konsolenparameter), welche Features ein- bzw. ausgeschaltet sein sollen. So können Features, die noch nicht komplett umgesetzt wurden, beim Deployment in eine produktive Umgebung deaktiviert bleiben.

Die Probleme von Feature-Toggles

Tatsächlich habe ich persönlich jedoch noch kein Projekt kennengelernt, in dem Feature-Toggles wie vorgesehen eingesetzt werden.

Eigentlich sollten Feature Toggles nur für die Zeit der Entwicklung des Features existieren und bei Veröffentlichung sämtliche Referenzen wieder entfernt werden. Aber Theorie und Praxis sind wie immer zwei verschiedene Dinge.

Deshalb wird man sehr schnell vier Dinge feststellen können, falls Feature Toggles zwar eingesetzt, aber nicht korrekt angewendet werden:

  • Die Konfiguration der Feature Toggles landet in einer generellen Konfigurationsdatei -> diese wird unendlich lang und unübersichtlich

  • Im Code wird man immer mehr Stellen mit Bedingungen oder sonstigen Sonderlocken für die einzelnen Feature Toggles finden -> die Größe des Codes nimmt zu und wird unübersichtlicher

  • Die Bedingungen bzw. Sonderlocken müssen irgendwann immer mehr Feature Toggles referenzieren, weil diese sich gegenseitig bedingen -> die Logik wird zunehmend komplex

  • In der Theorie müssen eigentlich alle möglichen Kombinationen von Feature Toggles durchgetestet werden (Integrationstests), um sicherzustellen, dass gegenseitige Abhängigkeiten bzw. Teile der Anwendungen korrekt funktionieren -> Komplexität der Tests nehmen dadurch stark zu (und werden generell unübersichtlicher mit zunehmender Anzahl von Feature Toggles). Wer dies nicht testet, läuft Gefahr in Produktion zwei neue Features zu aktivieren, die sich gegenseitig negativ beeinflussen.

Es soll zudem auch Projekte geben, die sowohl Feature-Branches als auch Feature-Toggles verwenden. Den Vorteil sehe ich persönlich dabei nicht so ganz. Ich persönlich vermute auch, dass die Gefahr bei diesen Projekten noch größer ist, dass aus Feature-Toggles am Ende viele einzelne Konfigurationsoptionen werden.

Das dev-master-Modell

Eine andere, etwas abgeschwächtere Form des trunk-based developments ist das dev-master-Modell. Hierbei entwickeln alle Entwickler/innen zwar auch gleichzeitig auf einem Branch (nämlich dev bzw. develop), dem Team ist jedoch dabei stets bewusst, dass der develop-Branch einen instabilen Software-Stand enthalten kann. Nur funktionierende Commit-Stände auf develop, die zuvor mittels ausgiebiger Tests in der Build-Umgebung erfolgreich getestet wurden, werden auf master übertragen.

Ich persönlich sehe hier erneut die Gefahr, dass Entwickler/innen dazu neigen werden, ihre Änderungen nicht mehr so früh wie möglich zu pushen, sondern erst bei Fertigstellung des kompletten Features (denn wer möchte schon einen eventuell kaputten Stand einer/s anderen in seinen Entwicklungsstand integrieren und damit seine eigene Entwicklung gefährden? Ein Teufelskreis …). Im Grunde also wieder kontraproduktiv. Denn sollten sich die Entwickler/innen doch an die frühe Commit- und Push-Regeln aus der trunk-based Entwicklung halten, können diese auch direkt auf dem master-Branch arbeiten. Und wenn sie es eben nicht tun, können sie auch genauso gut wieder eigene Feature/Entwickler-Branches erstellen (letztenendes sind lokale Entwicklungsstände mit Commits auch nichts anderes als lokale Branches von dem origin-Branch des zentralen Repositorys).

Ein Vorteil dieses Modells – im Gegensatz zur trunk-based Entwicklung – ist jedoch die Möglichkeit für die beiden Branches unterschiedliche Deployment-Ziele einzurichten. Änderungen an develop landen dann in der Staging-Umgebung, Änderungen am master in der Produktivumgebung. So eine Trennung ist jedoch auch mit trunk-based Development möglich, zum Beispiel mittels Tags und einem manuellen Deployment in Produktion. Ein Merge von dev in master um ein Deployment in Produktion anzustoßen ist streng genommen am Ende auch ein manueller Prozess.

Erfahrungen aus der Praxis

Wie kann man nun in der Praxis Git-Modelle sinnvoll mit Continuous Delivery vereinen? Aus eigener Erfahrung ist der optimale Einsatz von Git stark abhängig vom Team und dem Projekt selbst. Dazu gehören vor allem Faktoren wie die Teamgröße, die Erfahrung der Entwickler/innen, aber auch der Projekt- bzw. Software-Typ und die Häufigkeit von Änderungen.

Abhängig von der Teamzusammenstellung

Die folgenden Beobachtungen habe ich persönlich bezüglich der Teamzusammenstellung gemacht: :

  • Mit zunehmender Größe des Entwicklerteams ist es sinnvoll, auf ein umfassenderes Branching-Modell zu setzen.

  • Bei kleinen Teams, oder Projekten, an denen meist nur ein oder zwei Personen arbeiten ist trunk-based Development deutlich effizienter.

  • Mit unerfahreneren Entwicklern/innen im Team sollte eher auf ein Branching-Modell mit Feature-Branches gesetzt werden. Valide wäre hier auch eine Mischung. Das heißt die erfahrenen Entwickler/innen arbeiten auf trunk und die unerfahrenen erstellen Feature/Entwickler-Branches. Dabei sollte klar sein, dass die unerfahrenen Entwickler/innen von den erfahreneren Entwicklern/innen unterstützt werden. Das Ziel sollte jedoch trotzdem sein, dass möglichst alle auf trunk arbeiten.

  • Je erfahrener die Entwickler/innen sind, desto eher kann man auf trunk-based Entwicklung und Feature Toggles setzen. Dies wäre gleichzeitig auch der erste Schritt in Richtung Continuous Deployment.

Abhängig vom Projekttyp

Hier unterscheide ich folgende Projekttypen:

  • Laufende Software (Running Software)

    • Also zum Beispiel Server, Microservices, etc. Alles das, was letztlich deployed wird.

  • Unterstützende Software (Supporting Software)

    • Diese wird in der Regel in der laufenden Software eingebunden und nicht selbst deployed. Dies können zum Beispiel Bibliotheken oder aber auch WebComponents (in der Frontend-Entwicklung) sein.

Die Auswahl der Branching-Strategie für laufende Software ist abhängig von der Größe des Teams (s. o.). Wobei hierfür folgende zusätzliche Bedingung gilt:

Je seltener an dem Projekt gearbeitet wird, desto mehr Sinn macht eine trunk-based Entwicklung. So werden unnötige Branches und Merges vermieden.

Für unterstützende Software habe ich die Erfahrung gemacht, dass eine komplett andere Strategie am sinnvollsten ist. Diese Bibliotheken o.ä. werden nämlich in der Regel als Dependencies in den anderen Projekten eingebunden – also konkreter: Mittels einer Version referenziert.

Diese Versionierung ist auch das zentrale Element bei der folgenden Branching-Strategie:

  • Es gibt einen master-Branch. Nur von diesem werden echte Release-Versionen erzeugt und in ein Repository Manager hochgeladen.

  • Für jede neue Version wird ein neuer Versions-Branch erstellt, der von master abzweigt. Von diesem Branch wird zwar auch bereits eine neue Version gebaut und in einen Repository Manager hochgeladen, jedoch immer mit einem „beta“-Flag in der Version (Entwickler/innen aus der Gradle- bzw. Maven-Welt kennen diese auch als Snapshots).

Auf diese Art und Weise können wir neue Beta-Versionen vorab in der laufenden Software testen, bevor wir eine offizielle Version der Bibliothek o. ä. mittels des master-Branches erstellen.

Sollte nur eine einzelne Person für die Bibliothek o. ä. verantwortlich sein oder immer nur eine Person gleichzeitig daran arbeiten, kann hier auch das dev-master-Modell Sinn machen.

Außerdem sollte damit auch klar sein, dass es nicht unbedingt Sinn macht, ein Entwicklungs-Modell innerhalb eines Teams bzw. Projekts auf mehrere Software-Projekte anzuwenden. Wenn es für Entwickler/innen möglich sein soll verschiedene präferierte Entwicklungsumgebungen, Tools oder auch Programmiersprachen (ein häufiges Szenario bei der Entwicklung von Microservices) einsetzen zu dürfen, wieso sollten sie dann nicht auch das Entwicklungs-Modell einsetzen dürfen, mit dem sie am effizientesten arbeiten?

Weitere wichtige Faktoren

Unabhängig von der Umsetzung sind natürlich noch andere Faktoren ausschlaggebend:

  • Sinnvolle Aufteilung von Features, die gleichzeitig entwickelt werden sollen -> Möglichst keine gegenseitigen Abhängigkeiten

  • Häufiges Committen, Pushen und Mergen (eventuell sogar über Feature-Branches hinweg) -> Möglichst wenig Konflikte

  • Viel Kommunikation – vor allem bei Personen, die am selben Feature arbeiten

  • Storys und Tasks möglichst klein schneiden -> Die Umsetzung eines Tasks sollte nicht länger als einen Tag dauern. Ansonsten werden die zu mergenden Änderungen wieder zu groß und die Gefahr für Konflikte steigt

Ist der Einsatz von git-flow eine schlechte Idee?

Nein. Git-flow ist ein gut durchdachtes Konzept, welches in einigen Projekten auch erfolgreich angewendet wird.

Ist der Einsatz von trunk-based Development eine schlechte Idee?

Nein. Trunk-based Development ist auch ein bekanntes Konzept, welches in einigen Projekten erfolgreich angewendet wird.

Ist der Einsatz von trunk-based Development mit Feature Toggles eine schlechte Idee?

Nein. Feature Toggles sind ein beliebtes Konzept, welches in einigen Projekten erfolgreich angewendet wird.

Was ist denn nun das beste Modell?

Das, was dem Team am besten zusagt. Fangt doch einfach mit git-flow an. Falls es euch zu komplex erscheint, nehmt Anpassungen vor oder versucht mit trunk-based Entwicklung zu starten. Falls ihr doch ständig Probleme habt, erstellt Feature-Branches abgeleitet von master.

Mit den verschiedenen Branch- bzw. Entwicklungs-Modellen gibt es eben auch unterschiedliche Vor- und Nachteile, mit denen man unterschiedlich umgehen kann. Ob man nun zum Beispiel produktive Versionen dadurch kennzeichnet, dass sie auf dem master-Branch existieren oder eben speziell getaggt wurden, ob man mehr Merge-Probleme bei der Arbeit auf einem oder mehreren Branches erwartet, ob man als Ziel Continuous Deployment oder Continuous Delivery hat, ob man Features lieber in Branches oder mittels Feature Toggles im Code isoliert ist also alles eben nicht generell für jedes Team oder Projekt zu beantworten. Entsprechend kann es auch nicht die eine richtige Empfehlung zur Wahl des Branch- bzw. Entwicklungs-Modells geben.

Meine Erfahrungen aus dem Abschnitt „Erfahrungen aus der Praxis“ können jedoch als Hilfestellung für die Auswahl herangezogen werden. Letztlich ist aber die Wahl auch nicht für immer in Stein gemeißelt. Wer sowieso in einem agilen Umfeld unterwegs ist, sollte auch kein Problem haben zwischen den Branch- bzw. Entwicklungsmodellen zu wechseln oder neue auszuprobieren.

Der nächste Part

Damit endet der erste Teil der Serie zu Continuous Delivery in der Praxis. Im nächsten Part werde ich darauf eingehen, wie man verschiedene Branching- bzw. Entwicklungs-Modelle sinnvoll in Bitbucket anwenden kann. Dazu gehören zum Beispiel verschiedene Vorgehensweisen bei Merge-Requests und automatische Merge-Strategien.

content by Marvin Becker

Teil 2 ist veröffentlicht

Interesse geweckt? Dann lass uns sprechen.
Folge uns auf