Deployment mit git, GitHub und Webhooks

Deployment mit git, GitHub und Webhooks 1

Das freie Versionsverwaltungssystem Git eignet sich nicht nur für umfangreiches Codemanagment. Zudem bietet es die Möglichkeit, automatisch auf verschiedensten Umgebungen Code auszuliefern – dank der in GitHub integrierten Webhooks. Eine Anleitung für EntwicklerInnen.

Was genau passieren soll

Wir haben folgendes Setup:

  • eine lokale Entwicklungsumgebung
  • ein remote Testsystem
  • der Code läuft über GitHub

Wenn in der lokalen Entwicklungsumgebung ein git push origin master ausgeführt werden soll, dann sollte auf dem remote Testsystem ein git pull origin master getriggert werden.

git pull über http & www-data

Zu Beginn richten wir das lokale Testsystem ein. Das müssen wir so vorbereiten, dass der Befehl git pull auch über einen http-Zugriff ausgeführt werden kann. Damit das geht, müssen wir ein paar Dateien anlegen. Folgende Ordner-Struktur kann ich dafür empfehlen:

├─ /path/to/project/git-webhook/ <- unser neuer Ordner
├─ /path/to/project/my-git-repo/ <- unser versionierter Code

Aus Sicherheitsgründen sollte der Webhook-Ordner nicht in den versionierten Code integriert werden. Zum Grund hierfür gleich mehr.

Zunächst legen wir ein Shell-Script im Webhook-Ordner an. Dieses macht im Grunde nichts anderes, als in das entsprechende Verzeichnis zu gehen, um dort den Pull-Befehl auszuführen:

#!/bin/sh
cd /path/to/project/my-git-repo/
unset GIT_DIR # Removes directory binding to avoid conflicts
git pull origin master

Die Datei heisst bei mir git-pull.sh – ihr solltet sie über chmod a+x git-pull.sh ausführbar machen.

Als nächstes müssen wir überprüfen, ob der www-data-User (auf euren Maschinen kann der Webserver ein anderer User sein) die Rechte besitzt, den git pull Befehl auszuführen. Dafür wechseln wir in das Webhook-Verzeichnis und führen folgenden Befehl aus:

su -c ./git-pull.sh -s /bin/sh www-data

Dieser Befehl führt unser Script als User www-data aus. Wir gaukeln dem System sozusagen genau die Rechte vor, die es aus einem HTTP-Zugriff bekommt. In den meisten Fällen erscheint dabei folgende Fehlermeldung:

error: cannot open .git/FETCH_HEAD: Permission denied

Das bedeutet, dass bestimmte Verzeichnisse des Repositories für den User www-data nicht erreichbar sind. Das müssen wir ändern. Dafür wechseln wir in das entsprechende Projekt-Verzeichnis und führen den nachfolgenden Befehl aus:

chown -R www-data:www-data /path/to/project/my-git-repo/.git

Dieser Befehl ändert den Besitzer des Git-Verzeichnisses auf jenen des Benutzers und auf die Gruppe vom User des Webservers. Ein weiterer Test sollte keine weiteren Fehler mehr hervorrufen.

Zusätzlich ist es eine gute Idee, dass jede neu angelegte Datei die Besitzrechte vererbt bekommt:

chmod -R g+s /path/to/project/my-git-repo/

Wenn das erledigt ist, können wir eine PHP-Datei im Webhook-Verzeichnis anlegen. Ich nenne diese überraschenderweise git-pull.php:

<?php
exec( 'cd /path/to/project/git-webhook/ && ./git-pull.sh', $out );
print_r( $out );

Wir wechseln also in das Webhook-Verzeichnis (weil PHP das ja nicht wissen kann) und führen das Shell-Script aus. Mehr ist dabei erst einmal nicht zu tun. Wechseln wir also zum Browser unserer Wahl und rufen die PHP-Datei auf. Wenn ihr alles richtig gemacht habt, dann sollte dort jetzt “Everything is up to date” oder Ähnliches stehen. Damit könnt ihr euch sicher sein, dass ihr alles richtig gemacht habt.

Jetzt müssen wir noch den Webhook einrichten. Hier gibt es einen ausführlichen Guide dafür. Um die Sache aber kurz zusammenzufassen:

  1. Geht auf https://github.com/[user]/[repo]/settings/hooks/
  2. Fügt einen Webhook über den Button oben rechts hinzu
  3. Gebt die Adresse des PHP-Scripts an
  4. Gebt pro forma einen Secret-Key ein – etwas Kryptisches, wie 47c74185ced5f6fdc418acd22eccf8a52943da91. Dazu kommen wir später.
  5. Wir brauchen das Event nur beim Push-Event. Alle anderen Events sind für uns nicht relevant.
  6. Aktiviert den Webhook und speichert ihn

Sobald der Webhook aktiv ist, wird eine Payload an das PHP-Script gesendet. Ihr könnt über die Hook-Details den Request und die Response einsehen.

Sicherheit

Grundsätzlich sind wir damit fertig. Dennoch ist es mehr als sinnvoll, noch ein paar Sicherheitsbarrieren einzubauen. Als erste und einfachste Barriere nutze ich HTTP-Auth, die das Verzeichnis grundsätzlich schützt. Wichtig ist, dass der HTTP-Aufruf im Webhook-Setup von github entsprechend geändert wird.

Meine fertige Struktur sieht dann normalerweise so aus:

├─ /path/to/project/git-webhook/
|  ├─ .htaccess
|  ├─ .htpasswd
|  ├─ git-pull.sh
|  ├─ git-pull.php
├─ /path/to/project/my-git-repo/ <- unser versionierter Code

Als weitere Schutzmaßnahme nutze ich den Secret-Key vom Request und überprüfe diesen über das PHP-Script. Sollte der Secret nicht matchen, dann bricht das Script natürlich ab:

<?php
// set the secret key
$hook_secret = '47c74185ced5f6fdc418acd22eccf8a52943da91';

// set the exception handler to get error messages
set_exception_handler( function( $e ) {
    header( 'HTTP/1.1 500 Internal Server Error' );
    echo "Error on line {$e->getLine()}: " . htmlSpecialChars($e->getMessage() );
    die();
} );

// check if we have a signature
if ( ! isset( $_SERVER[ 'HTTP_X_HUB_SIGNATURE' ] ) )
    throw new Exception( "HTTP header 'X-Hub-Signature' is missing." );
else if ( ! extension_loaded( 'hash' ) )
    throw new Exception( "Missing 'hash' extension to check the secret code validity." );

// check if the algo is supported
list( $algo, $hash ) = explode( '=', $_SERVER[ 'HTTP_X_HUB_SIGNATURE' ], 2 ) + array( '', '' );
if ( ! in_array( $algo, hash_algos(), TRUE ) )
    throw new Exception( "Hash algorithm '$algo' is not supported." );

// check if the key is valid
$rawPost = file_get_contents( 'php://input' );
if ( $hash !== hash_hmac( $algo, $rawPost, $hookSecret ) )
    throw new Exception( 'Hook secret does not match.' );

// execute
exec( 'cd /path/to/project/git-webhook/ && ./git-pull.sh', $out );
print_r( $out );

Findige Entwicklerinnen finden bestimmt noch mehr Möglichkeiten das Setup abzusichern.

Fazit

Natürlich könnte man auch Capistrano oder ähnliche Werkzeuge als Deployment-Software nutzen. Ich bin jedoch ein Fan davon, mit möglichst wenigen Tools zu arbeiten, um maximal integrativ zu sein. Mit git, GitHub und deren Webhooks lassen sich automatische Deployments von kleinen Projekten einrichten – einfach und recht flott.

Newsletter abonnieren

Das könnte dich auch interessieren

Vom Kundenwunsch zum Quellcode. 10 Fragen an unseren Plugin-Entwickler Andre Peiffer.

Wie übersetzt man Kundenanforderungen in Quellcode? Was sind die Herausforderungen dabei? Und gibt es etwas, das einen WordPress-Entwickler aus der Fassun ...

Mehr erfahren

Adventskalender Tag 10 – Rekursion in Hooks verhindern

Der heutige Adventskalenderbeitrag richtet sich speziell an die Entwickler unter uns, die mit Rekursionen zu kämpfen haben. Angenommen, wir befinden uns i ...

Mehr erfahren

Adventskalender Tag 7 – Mini-Plugin: Bekannte Spam-IPs in WordPress blockieren

Bereits am Dienstag in unserem dritten Türchen hat Toscho einige nützliche SQL-Statements aufgezeigt um die Kommentare in WordPress zu analysieren und si ...

Mehr erfahren

Kommentare

7 Kommentare

  1. Ich hab einen Cronjob mit git-pull 🙂

  2. Hallo,

    irgendwo habe ich einen gedanklichen Klemmer.
    Im Skript auf dem Entwicklungsserver wird ein ‘pull‘ über den remote ‘origin‘ und dem branch ‘master‘ durchgeführt. Was ist wenn mehrere feature branch vorhanden sind ?
    Siehe gitflow

    Ist es nicht besser ein remote anzulegen welcher 2 URL hat ?
    > git remote add set-url

    Mit freundlichen Grüßen

    Stephan

    1. Hallo Stephan,

      das kann man ja nach belieben machen. Mit dem Shell-Script kannst du ja anstellen, was du willst. Es wird ja bei jedem Push in das Repo getriggert.

      Viele Grüße,
      Thomas

  3. Hey Thomas,
    vielen Dank für dein Skript. Aktuell klemmt das von Dir erstellte PHP-Skript noch ein wenig, bzw. der Fehler leigt bei mir.

    Dazu hab ich mehrere Fragen:

    In meinen tcpdump sehe ich das von Gitlab über die Webhooks der Secret-Token im Post gesendet wird. Aber wie der den von deinem PHP-Skript analysiert bzw. geparsed. Ich erhalte ein “X-Hub-Signature’ is missing.”
    Auszug aus meinem tcpdump:
    POST /git-webhook/git-pull.php HTTP/1.1
    Content-Type: application/json
    X-Gitlab-Event: Push Hook
    X-Gitlab-Token: HalloTcpdumpKannstDuMeinenSecretTokenSniffen?
    Connection: close
    Host: git-lab-client-02
    Content-Length: 2504

    Eine X-Hub-Signature’ enthält der POST nicht. eine Anpassung in deinem Skript auf X-Gitlab-Token: führte jebenfalls nicht zum Erfolg.
    Ich versuchte mir ein simples PHP-Skript zu bauen, indem nur mal abgefragt wird ob im HEADER/POST ein X-Gitlab-Token enthalten ist.
    <?php
    if(isset($_SERVER[ 'X-Powered-By:' ]))
    echo "X-Gitlab-Event: angegeben”;
    else
    echo “kein X-Gitlab-Event: angegeben”;
    ?>
    Selbst das führte nicht zum Erfolgt. Wo ist mein Denkfehler?
    Grüße Andreas

    1. X-Powered-By wurde natürlich ebenfalls von ‘X-Powered-By:’ auf ‘X-Gitlab-Token:’ angepasst

      1. Hey Thomas, ich hab das Problem gelöst. Hab nochmals vielen Dank!

      2. eine kleine Anmerkung hab ich noch zum Thema error: cannot open .git/FETCH_HEAD: Permission denied

        ggf. SELinux überprüfen und mit setenforce 0 auf Permissive setzen

Schreibe eine Antwort an Chris Antwort abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Du kannst folgende HTML Tags verwenden: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>