Eine CI/CD Pipeline für Static Sites mit GitHub Actions
Mit GitHub Actions lässt sich innerhalb kürzerster Zeit eine Continuous Integration bzw. Continuous Delivery Pipeline erstellen, die eine Static Site generiert und deployed. In diesem Beitrag zeige ich dir, wie ich die Pipeline für meinen Blog aufgebaut habe und welche Optimierungen ich vorgenommen habe, damit die Pipeline möglichst schnell durchläuft.
Das Ziel
CI/CD verfolgt das Ziel, einen konsistenten und automatisierten Prozess zu etablieren mit dem Anwendungen gebaut, getestet und deployed werden. Dadurch wird es möglich, dass Änderungen an einer Anwendung in kleinen, inkrementellen Schritten gemacht werden und so schnell wie möglich live gehen.
Das gleiche Ziel möchte ich auch mit der CI/CD Pipeline für meinen Blog erreichen. Egal ob Änderungen am Layout, neuer Content oder Korrekturen an bestehendem Content; mein Ziel ist es, dass ich diese jederzeit ins Git Repository einchecken kann und sie zwei Minuten später live sind.
Aufbau eines GitHub Actions Workflows
Um einen Workflow mit GitHub Actions zu erstellen, müssen wir in unserem Repository als erstes ein YAML File für den Workflow anlegen. Das File muss sich im Verzeichnis .github/workflows
befinden, damit es als Workflow erkannt wird. Der Name des Files ist egal. Ich nenne mein File build-and-deploy.yml
und füge zunächst einmal den folgenden Inhalt ein:
name: build-and-deploy
on: push
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
Mit name: build-and-deploy
geben wir unserem Workflow einen Namen. Ich habe den Workflow gleich wie das File benennt, das ist jedoch nicht nötig. Als nächstes definieren wir mit on: push
wann der Workflow ausgeführt werden soll. In diesem Fall bei jedem Push ins Git Repository.
Nun folgt der interessante Teil. Ein GitHub Actions Workflow besteht aus einem oder mehreren Jobs und diese wiederum aus einem oder mehreren Steps bzw. Actions. In unserem Fall besteht der Workflow aus einem Job mit dem Identifier build-and-deploy
. Wir geben mit runs-on: ubuntu-latest
an, dass der Job in einer Ubuntu Umgebung laufen soll und definieren anschliessend die Steps, die ausgeführt werden sollen. Zu Beginn starten wir mit einem einzigen Step der unseren Sourcecode auscheckt, damit wir anschliessend damit arbeiten können.
Wenn wir die Änderungen so commiten und auf GitHub pushen, wird automatisch der dazugehörige Workflow angelegt und ausgeführt. Wir können dies auf GitHub überprüfen:
Generieren der Static Site
Wir haben nun das Grundgerüst für unsere CI/CD Pipeline und können damit beginnen, die Static Site durch die Pipeline generieren zu lassen. Meinen Blog generiere ich mit dem Static Site Generator Hugo. Hugo sowie einige andere Abhängigkeiten habe ich mit Yarn installiert. Um die Static Site durch die Pipeline generieren zu lassen, müssen wir also zunächst alle Abhängigkeiten mit Yarn installieren. Dazu fügen wir einen zusätzlichen Step nach dem Auschecken des Sourcecodes hinzu:
name: build-and-deploy
on: push
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Install Dependencies
run: yarn --frozen-lockfile
Die Syntax sollte selbsterklärend sein: Mit name: Install Dependencies
geben wir dem Step einen Namen und mit run: yarn --frozen-lockfile
definieren wir, welcher Befehl ausgeführt werden soll. In diesem Fall installieren wir die Abhängigkeiten mit Yarn und geben durch --frozen-lockfile
an, dass genau die Versionen verwendet werden sollen mit denen wir lokal gearbeitet haben und die im Lockfile von Yarn spezifiziert sind.
Als nächstes können wir unsere Static Site generieren. Ich habe in meinem package.json
ein Build Skript definiert, dass ich mittels yarn build
aufrufen kann. Das Gleiche machen wir nun als weiteren Step in unserem Workflow:
name: build-and-deploy
on: push
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Install Dependencies
run: yarn --frozen-lockfile
- name: Build Site
run: yarn build
Sobald wir die Änderungen in unser Repository pushen wird der Workflow erneut gestartet und wir sehen dass die Static Site generiert wird:
Wir sehen auch, dass der Workflow bereits eine halbe Minute dauert. Und das ist ein vergleichsweise schneller Durchlauf. Das Installieren der Abhängigkeiten dauert teilweise um ein Vielfaches länger. Und mit zunehmender Anzahl Abhängigkeiten sowie weiteren Steps wird der Workflow in Zukunft eher mehr Zeit in Anspruch nehmen als weniger. Wie wir den Workflow optimieren können zeige ich dir später.
Deployment der Static Site auf Netlify
Nachdem wir die Static Site generiert haben wird es Zeit diese direkt aus unserer Pipeline heraus zu deployen. Als Hosting Provider setze ich Netlify ein. Um das Deployment auf Netlify zu automatisieren existiert eine CLI, die ich als Abhängigkeit im package.json
aufgelistet habe. Wir haben in unserer Pipeline also bereits alles installiert was wir für das Deployment brauchen und müssen lediglich einen zusätzlichen Step definieren:
name: build-and-deploy
on: push
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Install Dependencies
run: yarn --frozen-lockfile
- name: Build Site
run: yarn build
- name: Deploy Site
run: yarn netlify deploy -a $ -s $ -d public -p
Im Deployment Step referenzieren wir die beiden Secrets NETLIFY_AUTH_TOKEN
und NETLIFY_SITE_ID
. Damit die Pipeline auf diese Secrets zugreifen kann, müssen wir diese in den Projekt Einstellungen auf GitHub hinterlegen. Die Secrets findest du unter Settings > Secrets
:
Sobald wir die Secrets hinterlegt haben können wir unsere Änderungen ins Repository pushen und das Deployment wird ausgeführt:
Optimieren der Pipeline
Damit hätten wir unser Ziel eigentlich erreicht: Die Static Site wird in weniger als zwei Minuten gebaut und deployed. Doch unsere CI/CD Pipeline besteht momentan nur aus dem nötigsten. In Zukunft plane ich diverse weitere Schritte hinzuzufügen, die die Site testen und dabei zum Beispiel prüfen, ob sich aus Versehen falsche Links oder fehlende Bilder eingeschlichen haben. Ausserdem wird die Liste der Abhängigkeiten mit der Zeit sehr wahrscheinlich länger und die Ausführung der Pipeline wird dadurch länger und länger dauern.
Es macht deshalb Sinn das wir so früh wie möglich damit beginnen unsere Pipeline zu optimieren. Eine Optimierung die wir jetzt schon machen können ist das Cachen von Abhängigkeiten, damit diese nicht jedes Mal von neuem Installiert werden müssen. Um dies zu erreichen benutzen wir einen Cache Step:
name: build-and-deploy
on: push
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Restore Node Modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-modules-
- name: Install Dependencies
run: yarn --frozen-lockfile
- name: Build Site
run: yarn build
- name: Deploy Site
run: yarn netlify deploy -a ${{ secrets.NETLIFY_AUTH_TOKEN }} -s ${{ secrets.NETLIFY_SITE_ID }} -d public -p
Wie du siehst benutzen wir dieses Mal keinen einfachen Befehl sondern eine vordefinierte Action. Die Action die wir benutzen geben wir mittels uses: actions/cache@v2
an. In diesem Fall ist dies die integrierte Cache Action. Es existieren noch eine Vielzahl weiterer integrierter Actions sowie unzählige Actions, die von der Community erstellt worden sind. Du kannst dir deine Pipeline also nach Belieben aus existierenden Blöcken zusammenstellen. Auch für das Bauen einer Hugo Site oder das Deployment auf Netlify würden Community Actions existieren. Ich bevorzuge es jedoch, nur integrierte Actions zu verwenden und den Rest über eigene Befehle zu machen, damit ich jederzeit weiss was die Pipeline macht.
Der Cache Action geben wir mittels with
mehrere Parameter mit. So zum Beispiel den Pfad, der gecached werden soll sowie einen Key der benutzt werden soll, um zu Prüfen ob Daten im Cache vorhanden sind. Wenn wir die Änderungen pushen, sehen wir das unsere Pipeline nun zwei neue Steps ausführt:
Der Restore Node Modules
Step lädt Daten aus dem Cache sofern vorhanden und der Post Restore Node Modules
Step cached nach erfolgreicher Ausführung der Pipeline automatisch unsere zuvor installierten Abhängigkeiten. In diesem Fall waren noch keine Daten im Cache vorhanden. Doch wenn wir nun neue Änderungen pushen können wir sehen dass die Abhängigkeiten aus dem Cache geladen werden und das Installieren der Abhängigkeiten selbst anschliessend nur noch wenige Sekunden dauert:
Fazit
Wie du gesehen hast lässt sich mit GitHub Actions innerhalb kürzester Zeit eine CI/CD Pipeline einrichten, die eine Static Site baut und deployed. Das Ganze ist selbstverständlich nicht auf Static Sites limitiert. Du kannst mit GitHub Actions so gut wie alles automatisieren dass du auch manuell machen kannst. Als Ziel solltest du dir dabei immer setzen, die Pipeline so optimiert wie möglich zu gestalten. Jede unnötige Sekunde Laufzeit ist eine zu viel. Nur so stellst du sicher, dass deine Pipeline auch in Zukunft, wenn deine Anwendung wächst, noch schnell genug ist und du von den Vorteilen von CI/CD profitierst.