Teaser GitLab CI/CD

Einfache CI/CD-Pipelines bauen (mit GitLab CI/CD)

Von am 30.08.2021

Oftmals haben Tasks wie Testing oder Deployment äußerst repetitiven Charakter, sodass deren Automatisierung fast schon die logische Konsequenz dieser Erkenntnis zu sein scheint. Es ist schlichtweg sehr zeitaufwändig und mühsam, nach jedem Commit in einem Git-Repository manuell Tests auszuführen und die neue gebuildete Version auf den Production-Server zu kopieren. Die gute Nachricht: All das kann man längst in Code gießen. In diesem Artikel soll anhand von Gitlab CI/CD erläutert werden, wie eine solche Lösung umgesetzt werden kann und wie man dabei Schritt für Schritt vorgehen könnte. Die vorgestellten Konzepte lassen sich in der Regel auch auf andere CI/CD-Services anwenden, wenngleich es Unterschiede in der Konfiguration gibt.

Schritt 0: Erfordernisse feststellen

Zunächst muss man festhalten, dass Projekte sehr unterschiedlich sein können und entsprechend individuelle Abfolgen von Instruktionen in einer CI/CD-Pipeline erforderlich bzw. erwünscht sind. Es gilt also zunächst festzustellen, was genau eigentlich automatisiert werden soll. Ein beliebtes Schema, aus welchen Phasen sich die Pipeline zusammensetzt, beinhaltet etwa die Abschnitte „Test“, „Build“ und „Deploy“. Diese Abfolge kann nach Bedarf verkürzt und erweitert werden. Der Artikel orientiert sich jedoch stark daran und liefert Beispiel-Lösungen für Problemstellungen aus den genannten drei Phasen.

Schritt 1: Runner

Zunächst gilt es zu beachten, dass bei gitlab.com – im Gegensatz zu selbst (oder von einer Organisation) gehosteten GitLab-Instanzen – bereits ohne eigenes Zutun sogenannte „Runner“ zur Verfügung stehen. Dabei handelt es sich um Software, welche für das Ausführen bzw. das Abarbeiten der Tasks innerhalb einer Pipeline verantwortlich ist. Möchte man keine Begrenzung der Dauer, wie lange Pipelines pro Monat arbeiten dürfen, so lohnt es sich ebenso wie bei selbst gehosteten Instanzen, einen eigenen Runner aufzusetzen. Hierzu muss die Runner-Software auf einem mit dem Internet verbundenen Gerät installiert und eingerichtet werden. Es kann sich hierbei um die Maschine selbst handeln, auf welche später deployt werden soll, oder aber auch um ein separates Gerät. Detaillierte Instruktionen finden sich in der offiziellen Dokumentation.

Nach erfolgreicher Installation kann ein Runner mit einem GitLab-Repository verbunden werden, sodass dieser schließlich Jobs entgegennehmen und abarbeiten kann. Unter Settings > CI/CD befindet sich ein Punkt „Runners“. Hier stehen nun entweder bereits verfügbare Runner aufgelistet, oder aber man muss die Verbindung (oder korrekt genannt „Registrierung“) erst mittels Registrierungstoken vornehmen. Per Klick auf einen Button erhält man genauere Informationen, welche Kommandos auf der Host-Maschine des Runners auszuführen sind.

Beispiel für einen verfügbaren Runner

Schritt 2: Variablen anlegen

Ebenfalls unter Settings > CI/CD ist der Punkt „Variables“ zu finden. Hier können allerlei Werte, wie etwa SSH-Keys, Zugangsdaten, etc. gespeichert werden, sodass diese beispielsweise nicht von regulären Repository-Besucher*innen eingesehen werden können, so wie es der Fall wäre, wenn diese fix in die Konfigurationsdatei geschrieben wären. Das ist bei öffentlichen Repositorys relevant, aber auch bei privaten Repositorys, in welchen nicht alle Mitglieder Zugriff auf derlei Daten haben sollen. Vorsicht ist geboten, zumal Pipelines lesbare Job-Logs produzieren, in welche die Mitglieder immer noch Einsicht nehmen könnten. Die Lösung hierfür besteht im „Maskieren“ von Variablen. Es können freilich auch nicht-geheime Werte in Variablen gespeichert werden, welche man schlichtweg nicht „hard-coden“ will.

Neben den selbst definierten existieren automatisch erzeugte Variablen, welche meist mit dem aktuellen Repository zusammenhängen bzw. Informationen über dieses liefern. Diesbezüglich existiert eine (meiner Meinung nach hilfreiche) Liste. In der Praxis erweist sich der Gebrauch der besagten automatisch erzeugten Variablen auch dann als nützlich, wenn der Wert über die gesamte Projektlaufzeit hinweg konstant bleibt, da der Code in der Pipeline-Konfigurationsdatei somit in anderen Projekten auf einfache Weise wiederverwendet werden kann.

Schritt 3: Pipeline-Konfiguration

Um nun Jobs zu definieren, fügt man im Root des Repositorys eine Datei mit dem Namen .gitlab-ci.yml hinzu. Zunächst werden die übergeordneten Phasen (die sogenannten „Stages“) aufgelistet. Dies könnte beispielsweise wie folgt aussehen:

stages:
  - test
  - build
  - deploy

Die vergebenen Stage-Namen können danach in den einzelnen Arbeitsschritten („Jobs“) referenziert werden, um den jeweiligen Job einer Stage zuzuordnen. Es können prinzipiell mehrere Jobs pro Stage definiert werden. Ferner kann einfach bestimmt werden, auf welche Git-Branches sich ein Job beschränkt.

beispiel_job:
  stage: test
  only:
    - build
    - deploy
    - a_feature_branch

Um nun tatsächlich Instruktionen für einen Job zu definieren, wird der Job um ein script ergänzt. Nun kann ein Runner prinzipiell so aufgesetzt worden sein, dass dieser Docker unterstützt. Dies bietet den Vorteil, eine vordefinierte Umgebung nutzen zu können, in welcher die Anweisungen abgearbeitet werden. Hier ein Beispiel für Unit-Tests in einem Node.js-/npm-Projekt; der Befehl npm install installiert hier diverse Abhängigkeiten, npm run test startet die eigentlichen Unit-Tests:

unit-tests:
  image: node:lts-alpine
  stage: test
  before_script:
    - npm install
  script:
    - npm run test
  only:
    - develop
    - main

Schlägt eine Instruktion fehl, so wird die gesamte Pipeline an der betreffenden Stelle abgebrochen. Über das UI ist es später möglich, einen Job bzw. die Pipeline neu zu starten. Liegt der Fehler außerhalb des Repositorys kann auf diese Weise ein neuer Versuch unternommen werden, die Jobs abzuarbeiten. Im Fall von fehlgeschlagenen Unit-Tests hingegen entspricht ein Abbruch der Pipeline üblicherweise dem gewünschten Verhalten. Immerhin soll meist kein fehlerhafter Code deployt werden.

Um nun z. B. eine React-App zu veröffentlichen, möchte man anschließend an den Test-Job ein für die Production-Umgebung optimiertes Skript erzeugen und selbiges im darauffolgenden Job auf den Server kopieren – beispielsweise mittels lftp. Um erzeugte Dateien in späteren Jobs verfügbar zu machen, kommen „Artifacts“ zum Einsatz. Hier wird nun der erzeugte build-Folder zu einem Artifact erklärt und auf den Production-Server kopiert. Man beachte den Einsatz von Variablen:

build-job:
  stage: build
  image: node:lts-alpine
  before_script:
    - npm install
  script:
    - npm run build
  artifacts:
    paths:
      - build
    expire_in: 1 day
  only:
    - main

deploy-job:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add openssh
    - apk add --no-cache lftp
  script:
    - mkdir /root/.ssh
    - chmod 700 /root/.ssh
    - touch /root/.ssh/known_hosts
    - chmod 600 /root/.ssh/known_hosts
    - ssh-keyscan -H $HOST >> /root/.ssh/known_hosts
    - lftp -e "mirror -e -P 4 --transfer-all --reverse -X .* --verbose build/ $HOST/; quit" -u $USER,$PASSWORD sftp://$HOST -p 22
  only:
    - main

Fazit

Auch in kleineren Projekten kann viel Zeit gespart werden, indem man sich wiederholende Arbeitsschritte, welche etwa oft nach Code-Commits anfallen, automatisiert. Zwar erscheint der Einstieg in die Thematik manchen Programmierer*innen sicherlich herausfordernd, jedoch kann eine Konfigurationsdatei, so sie einmal wie gewünscht funktioniert, beliebig weiterverwendet und adaptiert werden, sodass sich die Arbeit schon bald lohnt.

The comments are closed.