Verteiltes Deployment mit Git und GitHub Actions

Oder auch: GitHub Pages für alle, die kein GitHub Pages nutzen möchten. Aber der Reihe nach. In diesem Artikel möchte ich eine Lösung vorstellen, mit der sich (statische) Web-Sites, die in einem Git-Repository bzw. genaugenommen auf GitHub vorliegen, automatisch nach dem Hochladen per “git push”-Kommando auf einen oder mehrere Server verteilen lassen. Für das Deployment werden die GitHub Actions genutzt, mit deren Hilfe sich Workflows automatisieren lassen. Genug der Buzzwords – erst einmal ein wenig zu den Hintergründen.

Mit Hugo zu GitHub Pages

Vor einiger Zeit habe ich begonnen, kleinere Web-Sites mit Hilfe von Hugo zu erstellen, einem Generator für statische Web-Seiten. Die Ergebnisse in Form von HTML-, CSS- und sonstigen Dateien werden danach nur noch auf einen Server gepackt, der nicht einmal besondere Anforderungen erfüllen muss, schließlich sollen die Dateien einfach nur ausgeliefert werden, ganz ohne PHP, Datenbanken oder sonstigen Komponenten, um “dynamische Web-Sites” zu bauen. Netterweise bietet GitHub mit GitHub Pages einen Service an, um statische Web-Seiten zu hosten, das ganze noch dazu kostenlos und verteilt auf mehrere Server, wie die DNS-Abfrage verrät – aktuell für eine von mir dort gehostete Site sind es vier unterschiedliche IP-Adressen, unter denen die Web-Site dank Round-Robin-DNS erreichbar ist. Vermutlich wird GitHub auch noch diverse Loadbalancer nutzen und weitere Verfahren verwenden, um Ausfallsicherheit zu ermöglichen, aber das soll einen hier nicht weiter beschäftigen.

Warum Deployment per Git?

Der springende Punkt ist, dass die Nutzung von GitHub Pages ganz einfach wahnsinnig praktisch ist, quasi einen (für Entwickler)  “natürlichen” Weg darstellt, um lokale Änderungen remote zur Verfügung zu stellen, indem einfach das Kommando git push genutzt wird. Ergo kein Hantieren mit FTP, SFTP, Web-Uploads oder ähnlichen Tools, die einen weiteren, zusätzlichen und gefühlt überflüssigen Arbeitsschritt bedeuten würden. Nun könnte es sein, dass man nicht unbedingt GitHub Pages für das Hosting seiner Web-Sites nutzen möchte, beispielsweise weil die Server (vermutlich) in den USA stehen, oder weil man kostenlosen Diensten grundsätzlich misstraut oder warum auch immer. Tatsächlich ordnet die Firefox-Erweiterung Flagfox mindestens eine IP-Adresse von GitHub Pages dem CDN-Anbieter Fastly zu, was den Server-Standort letztlich relativiert, da die Edge-Server von Fastly weltweit verteilt sind. Aber vielleicht möchte man einfach auch nur das Hosting selbst übernehmen, dabei aber auf den Komfort beim Deployment nicht mehr verzichten.

Anforderungen: Verteiltes Deployment

Letztlich waren zwei Aufgaben zu erfüllen. 1. Das Deployment soll – genau wie bei GitHub Pages – per “git push” erfolgen. 2. Die aktualisierten Dateien müssen auf mehrere Web-Server verteilt werden, und zwar ohne dass ein mehrfaches Herunterladen aus dem Git-Repository erfolgt. Des Weiteren wollte ich nicht lokal mit irgendwelchen SSH-Keys für die Server-Zugänge hantieren, so sollte das Verfahren z.B. bei einem Wechsel des Entwicklungs-Rechners nach wie vor funktionieren.

Bei der Suche nach Hinweisen zum Deployment per Git bin ich somit öfter auf die Nutzung von Git-Hooks gestoßen, also der Möglichkeit, Skripte auszuführen, die mit Git-Aktionen verknüpft sind. Derlei Hooks gibt es einige, sowohl Client- als auch Server-seitig, aber irgendwie fühlte sich dies letztlich ein wenig umständlich an, beispielsweise wenn zwei Remote-Repositories angesprochen werden müssten, eines wie gewohnt auf GitHub, das andere auf einem Deployment-Server, der dann einen Hook ausführt, so dass die aktualisierten Dateien ins richtige Verzeichnis gepackt werden. Darüber hinaus wollte ich auch weiterhin nur ein Remote-Repository nutzen. Bis zur endgültigen Lösung habe ich auch noch zwei, drei kleine Deploy-Tools ausprobiert, die mich jedoch allesamt nicht überzeugten.

Vom Repository zu den Servern per GitHub Actions

Letztlich habe ich mich für einen GitHub Actions Workflow entschieden, der nach “git push” ausgeführt wird und sich letztlich den Bordmitteln git, SSH und rsync bedient, um die Dateien herunterzuladen und zu verteilen. Falls man lieber auf GitLab setzt oder Git ohne Web-UI pur einsetzt, sollte eine Adaption leicht möglich sein.

Als Voraussetzung muss zumindest ein Server – in meinem Fall eine virtuelle Maschine – bei einem Provider per SSH erreichbar sein. Darauf werden einerseits die Dateien aus dem Repository übertragen, andererseits übernimmt der Server auch die weitere Verteilung. Zwar stellt dieser Server einen Single Point of Failure dar, aber davon ausgehend, dass es bei einem redundanten System, bestehend aus mehreren Web-Servern wichtiger ist, dass Requests überhaupt verarbeitet werden können als dass der bestehende Auftritt während eines etwaigen Ausfalls des Deployment-Systems aktualisiert werden muss, ist dieses Risiko meines Erachtens vernachlässigbar. Darüber hinaus sollten Ausfälle per Monitoring erkannt werden, so dass in kurzer Zeit wieder ein funktionierendes System zur Verfügung steht.

Infrastruktur und Vorbereitungen

Die Infrastruktur des folgenden Beispiels besteht aus zwei Servern bzw. VMs. Dabei dient demmin.xyzcdn.xyz als Deployment- und Web-Server, während auf stralsund.xyzcdn.xyz ein Web-Server eingerichtet ist.

User, Gruppe & Co.

Auf beiden Servern wird ein dedizierter User-Accounts für alle Deployment-Aufgaben eingerichtet, ein Passwort ist dabei nicht nötig, das Einloggen muss jedoch möglich sein. Als gemeinsame Gruppe für Deploy-User und “normale” User, habe ich zunächst eine Gruppe “gitdeploy” angelegt und meinen User-Account der Gruppe hinzugefügt:

geschke@demmin:~$ sudo groupadd gitdeploy
geschke@demmin:~$ sudo usermod -aG gitdeploy $USER

Der Deploy-User erhält beim Anlegen direkt die zusätzliche Gruppe “gitdeploy“:

geschke@demmin:~$ sudo useradd -d /home/deployuser -G gitdeploy -c "Deploy User" -m -s /bin/bash deployuser

Alle Deployment-Aktionen werden mit dem User “deployuser” durchgeführt. Dabei benötigt der Deploy-User drei SSH-Key-Sets. Theoretisch würde zwar eines ausreichen, aber damit wäre es letztlich von der GitHub-Action aus möglich, sich auf alle Web-Server einzuloggen, was nicht unbedingt sinnvoll erscheint. Außerdem lassen sich so den unterschiedlichen Aktionen auf den beteiligten Servern jeweils unterschiedliche Keys zuordnen.

Schließtechnik

Mit dem Standard-SSH-Key kann sich der Deploy-User auf den Web-Servern einloggen und die Dateien per rsync kopieren. Dazu wird wie üblich ein SSH-Key angelegt:

geschke@demmin:~/services/www.xyzcdn.xyz/html$ cd
geschke@demmin:~$ sudo su
[sudo] Passwort für geschke:
root@demmin:/home/geschke# su deployuser
deployuser@demmin:/home/geschke$ cd
deployuser@demmin:~$ ssh-keygen -b 4096
Generating public/private rsa key pair.
Enter file in which to save the key (/home/deployuser/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase):
Enter same passphrase again:

Der Public-Key in der Datei .ssh/id_rsa.pub muss dazu in die Datei ~/.ssh/authorized_keys des Users “deployuser” auf allen Web-Servern eingetragen werden. Falls – wie im Beispiel – der Deployment-Server ebenfalls als Web-Server dient, muss der Key auch dort eingetragen werden, da der rsync-Prozess alle Web-Server gleich behandelt, d.h. es muss möglich sein, sich als Deploy-User auf allen Web-Servern einzuloggen. Um sich den Schritt über “sudo su” und “su deployuser” zu ersparen, was gerade bei ersten Tests notwendig sein mag, empfiehlt es sich, den Public-Key des üblicherweise genutzten User-Accounts in die authorized_keys-Datei einzutragen, in meinem Fall wäre dies der User “geschke”.

Das zweite SSH-Key-Set dient dazu, dass sich der Deploy-User von den GitHub-Servern das Repository besorgen kann. Denn falls dies als “private” eingerichtet ist und somit nicht für die Öffentlichkeit, also “public” zur Verfügung steht, kann bei GitHub ein “Deploy Key” angelegt werden, der lesenden Zugriff auf das Repository ermöglicht. Dieser Key wird analog angelegt, nur unter anderem Namen gespeichert:

deployuser@demmin:~$ ssh-keygen -b 4096 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/deployuser/.ssh/id_rsa): /home/deployuser/.ssh/id_rsa_deploy
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/deployuser/.ssh/id_rsa_deploy
Your public key has been saved in /home/deployuser/.ssh/id_rsa_deploy.pub

Der Public-Key, also der Inhalt der Datei “~/.ssh/id_rsa_deploy.pub” wird anschließend als Deploy-Key bei GitHub hinterlegt, zu finden im jeweiligen Projekt unter “Settings -> Deploy keys“:

Es können auch mehrere Deploy-Keys eingesetzt werden, hier reicht jedoch der Key des Deploy-Users auf dem Deployment-Server.

Das letzte Key-Paar dient dazu, der GitHub-Action das Einloggen als Deploy-User zu ermöglichen. Dazu muss der SSH-Private-Key (!) bei GitHub hinterlegt werden, denn letztlich handelt es sich um nichts Anderes als ein normales Login von einem fremden Rechner. Der einzige Unterschied ist, dass man üblicherweise den Key auf eben jedem Server, von dem aus das Login erfolgt, erzeugt. Dieses Verfahren ist in der Laufzeitumgebung einer GitHub-Action natürlich nicht möglich, daher muss das Key-Paar zuvor generiert werden. Der SSH-Private-Key wird anschließend als Secret im Bereich “Settings -> Secrets” hinterlegt und innerhalb der Action als Variable bereitgestellt. Der SSH-Public-Key hingegen muss auf dem Deploy-Server, und auch nur dort, in die authorized-Keys-Datei eingefügt werden.

Die Generierung erfolgt analog zu den bisher verwendeten SSH-Keys, jedoch mit anderem Namen und zur Unterscheidung diesmal mit optionaler Kommentarangabe:

deployuser@demmin:~$ ssh-keygen -b 4096 -C "deployuser@xyzcdn.xyz" 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/deployuser/.ssh/id_rsa): /home/deployuser/.ssh/id_rsa_github
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/deployuser/.ssh/id_rsa_github
Your public key has been saved in /home/deployuser/.ssh/id_rsa_github.pub

Schlüsselbrett, oder: GitHub Secrets

Zusätzlich zum SSH-Private-Key müssen noch weitere Angaben als Secret eingetragen werden, und zwar Username “deployuser” und Host, im Beispiel “demmin.xyzcdn.xyz“.

Damit sind die Vorarbeiten auch abgeschlossen, wobei man sich nur noch einmalig als “deployuser” auf allen beteiligten Web-Servern einloggen sollte, um die IP-Adressen der Hosts SSH bekannt zu geben, so dass der Fingerprint in der known_hosts-Datei gespeichert wird. Denn falls sich in der Anwendung der GitHub-Action diesbezüglich etwas geändert haben sollte, wird die Action die Abfrage nicht bestätigen, und der GitHub-User erhält eine entsprechende Fehlermeldung, was aus Sicherheitsgründen auch beabsichtigt ist.

GitHub Action Workflow: SSH und ein wenig Shell

Nun aber endlich zur GitHub-Action selbst, zu finden im Tab “Actions“. Der Vorschlag, ein Template zu nutzen, kann ignoriert werden, denn auch beim Anklicken von “Skip this and set up a workflow yourself” wird ein Vorlage bereitgestellt, anhand der eine erste Orientierung möglich ist. Das Yaml-Format dürfte bekannt sein, und falls man allen vorhandenen und nicht vorhandenen Leerzeichen und Einrückungen ausreichend Beachtung schenkt, sollte dies keine große Hürde darstellen. Das folgende Listing zeigt die komplette Action:

name: Deploy stuff 

on:
  push:
    branches: [ master ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: deploy repository
        uses: appleboy/ssh-action@master
        env:
          DEPLOY_HOSTS: ${{ secrets.DEPLOY_HOSTS }}
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          envs: DEPLOY_HOSTS
          script: |
            if [ -d website-xyzcdn-xyz ]
            then
              cd website-xyzcdn-xyz
              GIT_SSH_COMMAND='ssh -i ~/.ssh/id_rsa_deploy -o IdentitiesOnly=yes' git pull
              chgrp -R gitdeploy public
              cd ..
            else
              GIT_SSH_COMMAND='ssh -i ~/.ssh/id_rsa_deploy -o IdentitiesOnly=yes' git clone git@github.com:geschke/website-xyzcdn-xyz.git
              chgrp -R gitdeploy website-xyzcdn-xyz/public
            fi
            for HOST in ${DEPLOY_HOSTS[@]}; do
              echo "Deploy to host $HOST..."
              rsync -chav --delete --exclude .git website-xyzcdn-xyz/public/* deployuser@$HOST:/srv/services/www.xyzcdn.xyz/html/
           
            done

Die Action soll insbesondere nur als Schema verstanden werden und nicht zur Übernahme per Copy&Paste motivieren. Zumindest die Pfade, Repository-Namen etc. müssen auf jeden Fall angepasst werden, dies betrifft auch die Verwendung in einem anderen Projekt, da diese Angaben nur für jeweils ein Repository bzw. Projekt gültig sind.

Ausgeführt werden soll die Action nach einem Push auf den Master-Branch. Der einzige Job in der Action trägt den Namen “deploy” und besteht letztlich aus nur einem Schritt, zumindest wenn man die Bezeichnungen der Job-Definition zugrunde legt. Mit “runs-on: ubuntu-latest” wird festgelegt, in welcher virtuellen Umgebung die Action ausgeführt wird. Zum Einsatz kommt die SSH Remote Commands Action aus dem “Marketplace”. Darin sind einige Tausend unterschiedlichster Actions enthalten, so dass man mit einiger Wahrscheinlichkeit eine passende Action für die eigenen Anforderungen finden wird. Falls nicht, können Actions auch selbst erstellt werden, dabei unterscheidet GitHub zwischen Docker- und JavaScript-Actions. Die hier verwendete SSH-Action gehört zu der ersten Gruppe, rein tatsächlich wird beim Ablauf der Action ein (temporäres) Docker-Image gebaut und anschließend gestartet.

Im Bereich “with:” werden der Action Parameter übergeben, hier sind dies der Host, Username und Private-Key, mit der sich die Action auf dem Deploy-Server einloggen kann. Hier kommen die in den Secrets gespeicherten Angaben zum Einsatz. Zusätzlich wird als Umgebungsvariable DEPLOY_HOSTS bereit gestellt, die zunächst in der Step-Beschreibung definiert werden muss (“env:“). Darin sind alle Web-Server mit ihrem vollständigen Hostnamen angegeben, auf die die Dateien verteilt werden sollen.

Schließlich werden die Kommandos ausgeführt, die im Bereich “script:” enthalten sind. Der Ablauf dürfte fast selbsterklärend sein, daher nur in Kurzform.

Zunächst wird geprüft, ob das Verzeichnis, das beim Clone-Vorgang entsteht, bereits existiert. Falls ja, wird dieses genutzt und ein “git pull” ausgeführt, um alle Updates zu erhalten. Falls nein, wird es neu ausgecheckt. Bei beiden Vorgängen kommt der zuvor angelegte SSH-Key für das Deployment zum Einsatz, dessen Public-Key bei GitHub hinterlegt wurde.

Anschließend werden die Dateien auf alle Hosts kopiert, die im GitHub-Secret “DEPLOY_HOSTS” eingetragen sind. Dabei kommt das Tool rsync zum Einsatz, das nur die Dateien überträgt, die geändert worden sind. Die Übertragung erfolgt per SSH, hier wird der bereits angelegte Standard-SSH-Key genutzt, mit dem sich der User “deployuser” auf den Web-Servern einloggen kann.

Falls es bei der Ausführung des Workflows zu Fehlern kommt, bricht die Action ab und GitHub verschickt eine Mail an den Besitzer des Repositories. Die Ausführung kann auch live beobachtet werden, zu finden unter “Actions -> Workflows -> All Workflows -> <Name des Workflows>”, wobei das Setup der virtuellen Umgebung, der Bau des Docker-Images mit anschließendem Start nur wenige Sekunden benötigt.

Noch ein Wort zum Auschecken des Repositories: Beim Anlegen eines Workflows zeigt das Template im Bereich “steps:” folgende Standard-Action an:

# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2

Der Kommentar deutet bereits darauf hin, wieso diese Action hier nicht genutzt wird. Das Auschecken findet im temporären Workspace statt, so dass rsync bei jedem Lauf alle Dateien erneut komplett übertragen würde. In diesem Fall könnte man direkt auf rsync und dessen effiziente Dateiübertragung verzichten.

Wie so oft steckte aber auch bei den ersten Testläufen der Teufel im Detail. Sofern alle Dateien und Verzeichnisse auf den Ziel-Servern nur vom User “deployuser” erreichbar sein sollten, war alles in Ordnung. Ich wollte jedoch auch mit meinem üblicherweise eingesetzten User-Account darauf zugreifen, daher sind die Gruppen-Rechte entsprechend gesetzt. Bei der Synchronisierung mit rsync konnten jedoch die Zeitstempel nicht gesetzt werden, da das Verzeichnis, in dem sich die Dateien befanden, noch einem anderen User-Account gehörte, daher mussten die Rechte entsprechend angepasst werden.

Es darf deployt (deployed? bereitgestellt!) werden!

Damit wäre der erste Schritte getan für ein “Github Pages im Eigenbau” – das verteilte Deployment funktioniert soweit zufriedenstellend. Auf den Servern fungiert ein Nginx-Container als Web-Server, während der Traefik-Proxy über allen Diensten den Einstiegspunkt darstellt. Dazu aber mehr demnächst… vielleicht…

 

Schreibe einen Kommentar

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

Tags: