Git Deployment

Wer Webseiten betreibt, steht häufiger vor dem Problem sich zu überlegen, wie Updates eines Projekts auf die Live-Version am einfachsten übertragen werden können. Gerade bei kleineren Projekten neigt man vielleicht häufiger dazu, auf simple aber fehleranfällige Methoden wie dem einfachen FTP-Upload zurückzugreifen.

Darum geht’s

Es gibt selbstverständlich eine Reihe unterschiedlicher Methoden, das Deployment zu lösen. Hier möchten wir aber etwas genauer auf die Möglichkeiten eingehen, die man mithilfe von Git, dem Tool zur Versionsverwaltung von Softwareprojekten, erreicht.

In jedem der Fälle muss das Live Projekt allerdings als Git Repository eingerichtet sein. Ein einfaches git init im Wurzelverzeichnis des Projekts reicht in der Regel. Hat man bereits ein existierendes Repository, kann man dieses natürlich auch einfach auf den Server klonen.

Ein zusätzlicher Bonus bei der Einrichtung des Projekts mit Git: Hat man nach den Änderungen einen Fehler entdeckt, kann man häufig mit einem „revert“ Commit oder einem git checkout die Änderungen leicht rückgängig machen (eventuelle Datenbank Migrationen sind dabei natürlich nicht inbegriffen).

Um die Erklärungen etwas zu vereinfachen, gehen wir im Folgenden davon aus, dass wir einen Produktivserver haben, welcher das Wurzelverzeichnis des Liveprojekts im Verzeichnis /var/www/live liegen hat. Die Domain des Servers ist beispiel.de. Git und SSH sind auf dem Server installiert und /var/www/live wurde bereits mit git init in ein Git Repository gewandelt, welches wir forthin als „Live Repository“ bezeichnen.

Git „bare“ Repositorys

Wir möchten am liebsten, dass unser normaler Git „Workflow“ sich nicht (oder nur beschränkt) ändert. Idealerweise würde man mit einem einzigen git push zum gemeinsamen Repository des Entwicklerteams die Änderungen sofort im Live-System sehen können. Ein einfacher „push“ in das Live Repository ist aber in der Regel nicht so ohne Weiteres möglich.

Aus diesem Grund brauchen wir ein zweites, ein sogenanntes „bare“ Repository auf dem Produktivserver, welches wir auf allen lokalen Repositories als „Remote“ verwenden werden. Das bare Repository legen wir an im Verzeichnis /var/www/live.git:

$ mkdir /var/www/live.git
$ cd /var/www/live.git
$ git init --bare

Als nächstes tragen wir dieses „Bare Repository“ als Remote beim Live Repository ein:

$ cd /var/www/live
$ git remote add live-bare /var/www/live.git

Und pushen alle Commits (falls es bereits Commits gibt) in den neuen Remote:

$ git push live-bare master

Es sollte nun möglich sein, das Repository auf den lokalen Rechner zu klonen:

$ git clone ssh://beispiel.de:/var/www/live.git

Bei der Verwendung von SSH empfiehlt sich das Einsetzen von SSH-Keys, worauf wir hier aber nicht weiter eingehen werden.

Hooks konfigurieren

Nachdem wir das Bare Repository lokal geklont haben, können wir Änderungen in dieses auf den Server pushen. Diese landen zur Zeit aber noch nicht im Live Repository. Um das zu erreichen, bedienen wir uns eines kleinen Tricks: Statt die Änderungen direkt in das Live Repository zu pushen, nutzen wir einen Hook aus dem Bare Repository, um die Änderungen von dem Live Repository aus zu „pullen“.

Wir erzeugen uns also einen post-update Hook, mit den richtigen Dateirechten, im Bare Repository:

$ touch /var/www/live.git/hooks/post-update
$ chmod ugo+x /var/www/live.git/hooks/post-update

In der post-update Datei tragen wir folgendes Skript ein:

#!/bin/sh

# Ermitteln des aktuellen Branches
branch=$(git rev-parse --symbolic --abbrev-ref $1)

# Nur Branch "master" beachten. Könnte auch ein anderer Branch sein.
if [ "$branch" = "master" ]; then
	cd /var/www/live || exit
	unset GIT_DIR
	echo
	echo "*** Pulling changes from $branch master into $(pwd) repository."
	echo
	git pull live-bare "$branch"
fi
exec git update-server-info

Der Ablauf ist simpel: Wenn ein update auf dem Bare Repository im Branch „master“ stattgefunden hat, wechselt das Skript in das Verzeichnis des Live Repositorys und holt sich diese Änderungen vom Remote „live-bare“. Jeder Push zum „master“ Branch im Bare Repository ist somit sofort im Live Repository sichtbar.

Hat man auf dem Produktivserver auch noch eine Entwicklungskopie, z.B. im Verzeichnis /var/www/dev (welches dann über eine andere Domain erreichbar ist), könnte man das post-update Skript um eine Fallunterscheidung für den Branch „dev“ erweitern, so dass bei einem Push zu „dev“ nach /var/www/dev gewechselt wird und bei einem Push zu „master“ nach /var/www/live.

Bitbucket/Github Webhooks

Nutzt man Bitbucket oder Github, ist das Bare Repository auf unserem Produktionsserver nicht nötig, da die Repositorys bei den Hostern als unser Bare Repository genutzt werden können. Damit wir bei einem Push zu den Hostern trotzdem unser Live-System automatisch aktualisieren können nutzen wir die sogenannten „Webhooks“, die von beiden Diensten bereitgestellt werden.

In den Einstellungen eines Repositories bei Bitbucket oder Github findet man den Eintrag „Webhooks“ bzw. „Webhooks & Services“. Dort kann man angeben, dass bei bestimmten Ereignissen des Repositories eine URL aufgerufen wird. Im Falle Bitbucket mit einem HTTP POST Request und einem JSON formatiertem Payload.

Im Folgenden schauen wir uns ein Beispiel mit Bitbucket an und fangen damit an ein (nicht existentes) Beispiel Bitbucket Repository als Remote in unser Live Repository einzutragen:

$ cd /var/www/live
$ git remote add bitbucket git@bitbucket.org:beispiel/live.git

Wir gehen davon aus, dass das Projekt bereits vollständig auf Bitbucket gepusht worden ist (z.B. von einem lokalen Rechner aus).

Wenn auf dem Server PHP genutzt wird, ist das Einrichten eines Webhooks ziemlich leicht. Man könnte z.B. die Datei /var/www/live/webhook.php anlegen und mit folgendem Inhalt füllen:

<?php

if(!isset($_GET['key']) || $_GET['key'] != 'SECRETKEY') {
	die();
}

// Header und Keys im Fall Bitbucket
if(!isset($_SERVER['HTTP_X_EVENT_KEY'])) {
	die();
}
$event_key = $_SERVER['HTTP_X_EVENT_KEY'];

// Nur bei einem Push
if($event_key != 'repo:push') {
	die();
}

// falls man den JSON Payload braucht
//$jsondata = file_get_contents('php://input');
//$data = json_decode($jsondata, true);

// Wir befinden uns bereits in dem Verzeichnis, aber falls es woanders liegt: ...
//chdir('/var/www/live');

// Pull ausführen
exec('git pull bitbucket master');

Die webhook.php erwartet ein key Argument mit einem selbst gewähltem „Passwort“. In den Webhooks Einstellungen bei dem Bitbucket Repository können wir nun einen neuen Webhook anlegen, und dem die folgende URL übergeben:

http://beispiel.de/webhook.php?key=SECRETKEY

Den „Trigger“ belassen wir auf den voreingestellten Eintrag „Repository push“.

Als letztes müssen wir uns noch den Punkt „Deployment keys“ in den Repository Einstellungen anschauen („Deploy keys“ bei Github). Deployment Keys sind dazu da um einen Lese-Zugriff per SSH auf die Repositorys zu erhalten. Es handelt sich dabei um normale SSH Keys.

Das ist vor allem dann nötig, wenn das Repository Privat ist. Ist das Repository öffentlich, kann man auch die öffentliche HTTPS URL im Live Repository als Remote eintragen – ein Deployment Key ist dann nicht nötig.

Den Deployment Key sollte man ohne „Passphrase“ erzeugen, da sonst das automatisierte Pullen nicht möglich ist:

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (~/.ssh/id_rsa): deploy_key
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
 ...

Dabei sollte man aufpassen, die standard Key Datei id_rsa nicht zu überschreiben, falls man dort bereits einen Key hat.

Gibt man wie oben gezeigt „deploy_key“ ein erzeugt das Tool zwei Dateien im aktuellen Verzeichnis: deploy_key und deploy_key.pub.

Den Inhalt der deploy_key.pub kopiert man in einen neuen Eintrag bei den „Deployment keys“ in den Bitbucket Repository Einstellungen. Die deploy_key lädt man auf den Produktivserver hoch. Man könnte die Datei nach ~/.ssh/id_rsa laden, wobei ~ das Heimverzeichnis des Users ist, der am Ende den Pull durchführen wird, d.h. der User der das PHP Skript ausführt. Möchte man die id_rsa nicht ändern, kann man einen neuen Host Eintrag in der ~/.ssh/config anlegen, in etwa wie folgt:

Host bitbucket-deployment
  HostName bitbucket.org
  IdentityFile ~/.ssh/deploy_key

Und den Remote im Live Repository ändert man:

$ git remote set-url bitbucket git@bitbucket-deployment:beispiel/live.git

Hat man alles richtig gemacht, sollte ein Push zum Bitbucket Repository nun automatisch einen HTTP POST Request zur webhook.php ausführen, welches wiederum einen git pull vom Bitbucket Repository durchführt.

Die verschiedenen Strategien kann man natürlich in beliebiger Art kombinieren. Es wäre auch möglich mehrere Remotes lokal einzutragen um eine striktere Trennung zwischen Entwicklung Remote und Produktiv Remote zuzulassen. Das bleibt einem im Endeffekt aber selbst überlassen.

Quellen