Lightweight Continuous Delivery mit Git

 

Git findet mittlerweile in fast allen Projekten einzug. Ich selbst kann das nur begrüssen und unterstütze tatkräftig wo immer nötig und möglich. Mit Git ist aber nicht nur ein produktiver Developer-Workflow möglich, sondern seit Git 2.3 lässt sich auch ein leichtgewichtiger Continuous-Delivery-Cycle abbilden. Ich verwende absichtlich nicht das despektierliche “Continuous Delivery für Arme” hier.

Als Use-Case nehmen wir einfach an, wir betreiben einen kleinen Web-Shop, der manuell und kontinuierlich mit neuen Produkten aktualisiert wird. Aktualisieren bedeutet in diesem Fall, wir patchen manuell Bilder, Texte und Preise in diesem Shop. Jede Änderung wird mit Git versioniert, getaggt und anschließend als Zip auf einen Server geladen, dort entpackt und damit ist die neue Version des Shops live.

today

Die wichtigste Hürde in diesem Szenario wurde bereits genommen – wir versionieren mit Git.

Gerade wenn wir von eCommerce sprechen ist aber TTM (Time to Market) ein entscheidender Faktor. Es geht also darum, Änderungen möglichst schnell nach draussen zu den Kunden zu bringen, weil wir uns damit von der Konkurrenz abheben (die noch viel viel langsamer ist als wir selber).

Unser Release Manager Klaus ist leider ein relativ bequemlicher Typ. Er arbeitet seit 25 Jahren in der Firma und hat noch nie etwas von Continuous Delivery gehört und auch Time-To-Market ist ihm herzlich egal, so lange er morgens nicht bei seinem Kaffee im Büro gestört wird. Deployments dauern also grundsätzlich viel viel länger als sie eigentlich dauern müssten.

Klaus hat leider sehr gute Beziehungen und deswegen unterbindet er jeglichen Versuch und erstickt alles im Keim, was auch nur im entferntesten an seinem Machtbereich rütteln könnte.

Klaus ist aber seit heute in seinem “wohlverdienten” Sommerurlaub – 6 Wochen Gardasee – und hat sich ohne eine große Übergabe verabschiedet. Übrigens war es letztes Jahr, während der kompletten 6 Wochen in denen Klaus in der Sonne gelegen hat nicht möglich, auch nur ein einziges Deployment zu machen. Diesmal sollte es anders kommen.

In der Firma bahnt sich eine kleine Revolution an (eigentlich ist es eine ganz natürliche Evolution), für Klaus allerdings sollte in Zukunft so einiges anders werden.

EIne kleine unabhängige Gruppe aus der IT plant die Rolle des Release-Managers schlichtweg abzuschaffen – Deployments werden kontinuiertlich gemacht, getriggert von der Entwicklung. Jede noch so kleine Änderung wird schnellstmöglich an die Kunden ausgespielt. Die Gruppe plant also im obigen Diagramm die Schritte 2, 3 und 4 einfach wegzulassen.

tomorrow

Die größte Schwierigkeit, es muss schnell gehen – wir haben nur einige Wochen Zeit und müssen in dieser Zeit einen grundsätzlichen Prozess etablieren und dem Management zeigen, wie viel besser schnelle kurze Release-Zyklen funktioneren. Es gibt nur diese eine Chance.

Der Sourcen des Shops ist bisher gehostet auf Github.

Wir möchten für den neuen Workflow Push-to-Deploy verwenden, also jeder Push ist ein Git Repository soll automatisch sofort den Shop updaten.

Um mit Push-to-Deploy arbeiten zu können brauchen wir auf unserem Production-Server ein weiteres Git-Repsitory, in das für jedes Release gepusht wird.

tomorrow_2

Spielen wir das Szenario einmal durch.

Einer der Entwickler loggt sich per SSH auf dem Live-System ein und legt zunächst ein neues Git-Repository an. Natürlich kennt der Entwickler die Best-Practices mit Git und initialisiert das Repository zunächst als Bare-Repository.

git init --bare
Initialized empty Git repository in /var/www/public

Dieses Repository wird jetzt als zweiter Remote beim Entwickler lokal konfiguriert.

git remote add prod ../live-repo

Jetzt hat der Entwickler die Möglichkeit, problemlos per Push Daten direkt auf den Live-Server zu übertragen.

git push prod master
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 298 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To ../live-repo
 * [new branch]      master -> master

Und damit sind die Änderungen live, oder? Sind sie das? Versuchen wir darauf zuzugreifen.
Irgendetwas scheint nicht zu funktionieren. Werfen wir nochmals einen Blick in das Verzeichnis.

ls
HEAD        config      description hooks       info        objects     refs

Wir haben ein Bare-Repository. Bare-Repositories haben keine Working-Copy. Wir können also gar nicht direkt auf die versionierten Dateien zugreifen. Wir brauchen eine andere Strategie.

Der Entwickler löscht das Repository nochmals und initialisiert es als Standard non-Bare Git-Repository.

git init
Initialized empty Git repository in /var/www/public

Jetzt haben wir eine Working-Copy und können problemlos direkt auf die Dateien zugreifen. Wenn wir unsere Änderungen pushen sollten wir die Änderungen sofort live sehen, richtig?

BEVOR SIE WEITERLESEN – ÜBERLEGEN SIE KURZ – funktioniert dieser Ansatz oder nicht? Wenn ja warum? Wenn nein, warum?

git push prod master
 ! [remote rejected] master -> master (branch is currently checked out)

Wir haben anscheinend wieder etwas übersehen. In der Standardeinstellung erlaubt es Git nicht, Änderungen auf den aktuell ausgecheckten Branch zu pushen. Das macht auch Sinn. Wir haben auf dem Server ein ganz normales Git Repository in dem theoretisch auch ein Entwickler arbeiten könnte. Stellen Sie sich die erstaunten Blicke (oder die herzlichen Flüche) vor, wenn Sie Ihr aktuelles Refactoring in das Repository pushen und damit dem Entwickler seine Änderungen auf dem Branch auf dem er gerade arbeitet zerschiessen?

Was tun wir also?

Eine erste Idee könnte sein, diesen Sicherheitsmechanismus einfach auszuhebeln. Am besten versuchen wir das einmal und zwar direkt auf dem Server im Live-Repository. Alles andere wäre auch langweilig.

git config receive.denyCurrentBranch ignore

Anschließend funktioniert der Push.

git push prod master
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 298 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To ../live-repo
 * [new branch]      master -> master

Werfen wir direkt nach dem Push einen Blick in das Repository.

ls
index.html

Die Änderungen sind da, auch im Browser scheint alles zu funktionieren. Machen wir einen zweiten Versuch und releasen eine weitere Änderungen.
Im Entwickler-Repository:

git add .
git commit -m "another change"
git push prod master
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 299 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To ../live-repo

Zurück im Live-Repository sehen wir diesen Zustand.

git status
On branch master
Changes to be committed:
  (use "git reset HEAD ..." to unstage)

	deleted:    index.html

Untracked files:
  (use "git add ..." to include in what will be committed)

	index.html

Eine Datei gelöscht? Eine Datei editiert? Was?

Ein Push funktioniert leider nicht so, wie man das für diesen Fall erwarten würde.

Ein Push aktualisiert das Repository, die Branch und HEAD-Referenz, nicht jedoch die Working Copy.
Kurz gesagt – das Repository ist nach dem Push bereits weiter als die Working-Copy die noch den alten Stand widerspiegelt. Die Working-Copy ist das was wir sehen. Mit einem git status vergleicht Git wie immer den Zustand der Working-Copy, Index und Repository und sieht, dass die Datei im Repository anders aussieht als die Datei in der Working-Copy und zeigt die Unterschiede an. Dieser Plan funktioniert also auch nicht..

Git 2.3 FTW

Mit Git 2.3 haben wir endlich eine schöne Möglichkeit, dieses Szenario zu realisieren.

Zunächst säubern wir das Live-Repository wieder.

git checkout -f
git status
On branch master
nothing to commit, working directory clean

Interessanterweise ist das Repository jetzt genau in dem Status in dem wir es eigentlich haben wollten und sogar die letzten Änderungen sind eingespielt. Mit dem git checkout -f haben wir nichts anderes gemacht als den Inhalt der Working Copy mit dem letzten Stand aus dem Repository zu überschreiben. Im Repository war schon der neueste Stand – also für jetzt ist alles gut.

Anschließend konfigurieren wir die neue Option updateInstead

git config receive.denyCurrentBranch updateInstead

Die Option updateInstead zwingt Git, nicht nur das Repository und die Head- und Branch-Referenzen zu aktualisieren sondern zu guter letzt auch die Working-Copy selbst.

Wir machen eine weitere Änderrung im Entwickler-Repository.

git add .
git commit -m "last change"
git push prod master
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 284 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To ../live-repo
   8e1e540..60ababb  master -> master

Wir prüfen sofort den Stand des Live-Systems und dem Repository

git status
On branch master
nothing to commit, working directory clean

Die Working-Copy ist sauber, alles scheint funktioniert zu haben. Im Browser ist ebenfalls die neueste Änderung.

Fazit

Die neue Option macht für einfache Use-Cases definitv Sinn, zuvor mussten solche Schritte manuell mit hässlichen Post-Receive-Hooks realisiert werden.

Klaus ist mittlerweile im Vorruhestand und geniesst seinen Lebensabend am Gardasee – Deployments für den Shop werden mehrmals täglich gemacht – die neue Strategie hat dem ganzen Team einen signifikanten Vorsprung gegenüber der Konkurrenz verschafft. Git Rules.

 

Links

Git Push to Deploy

 Git Training – Basics – November in München

Git Training – Workflows und Praktischer Einsatz

Thomas F. Nicolaissons exzellenter Artikel zu dem Thema

Effective Trainings & Consulting - Martin Dilger



Hat Ihnen dieser Blog-Eintrag gefallen? Ich stelle in diesem Blog Informationen über Tools, Frameworks und Werkzeuge zur Verfügung, die mich produktiver machen. Vielleicht kann ich auch Ihnen helfen, produktiver zu werden.


Ich unterstütze Sie als freier Mitarbeiter bei der Entwicklung von Software-Projekten, Agiler Arbeit sowie Schulungen / Fortbildungen.


Jeden Tag ein bisschen produktiver - ab heute