Initial ist GitLab als Webplattform zum Verwalten von Git-Repositories entstanden. Im Laufe der letzten Jahre kamen ein Issue-Tracker sowie ein Wiki hinzu. Mit GitLab CI wurden anschließend eine Komponente für Continuous Integration & Delivery und mit der Container-Registry eine Ablage für Docker-Images integriert. Weiterhin kam mit Mattermost ein Chat-Tool zur Zusammenarbeit in Teams und der Kommunikation mit Bots hinzu. GitLab existiert in verschiedenen Produktvarianten. Die Grundversion der Software steht als Open Source zur Verfügung. Neu entwickelte Features werden zumeist erst in den kommerziellen Varianten zur Verfügung gestellt und teilweise nach einiger Zeit in der Open-Source-Variante ergänzt.Die einfachste Möglichkeit, GitLab zu verwenden, ist das Erstellen eines kostenlosen Accounts auf der Cloud-Version gitlab.com. Diese verwenden wir auch im Rahmen dieses Artikels. Dort können auch private Repositories gehostet und Build-Pipelines erstellt werden.
GitLab basiert auf einem Konzept Namens „Idea to Production“ (s. Abb. 1), welches die Einzelschritte beschreibt, die ein neues Feature durchläuft, bis es auf einem Produktivsystem deployt wird. Ziel ist es, diesen Prozess weitgehend zu automatisieren und dem Nutzer dabei gleichzeitig einen nachvollziehbaren Überblick über den aktuellen Status zu bieten. Durch ein schnelles Feedback und einem hohen Grad an Automatisierung soll eine schnelle Umsetzung mit gleichzeitig hoher Codequalität erreicht werden.
Die Beispielanwendung
Im Rahmen des Artikels verwenden wir eine Spring Boot 2-Anwendung, die mit Maven gebaut wird. Für diese Anwendung soll ein neues Feature entwickelt werden. Dafür wird zunächst ein Ticket im GitLab eigenen Issue-Tracker angelegt. Die anschließende Entwicklung des Features erfolgt auf einem Feature-Branch, der bei jedem Commit gebaut und getestet wird. Für das Ausführen der Tests wird eine Postgres-Datenbank benötigt und bereitgestellt. Nach Abschluss der Entwicklung wird der Feature-Branch in den Master gemergt. Anschließend wird der Master-Branch gebaut und die Anwendung als Docker-Image paketiert. Das Image wird in der GitLab eigenen Docker-Registry abgelegt und mittels Docker-Compose auf einem Docker-Host deployt. Der Quellcode der Anwendung ist hier zu finden.
Erstellung von Issues, Merge-Requests und Feature-Branches
Nach der Erstellung eines Tickets im GitLab eigenen Issue-Tracker, sobald mit der Entwicklung begonnen werden soll, kann über die GitLab-Oberfläche ein sogenannter Merge-Request und ein zugehöriger Git-Feature-Branch erstellt werden (s. Abb. 2). Ein Merge-Request ist quasi ein Antrag, einen Feature-Branch in einen anderen Branch zu mergen. Vor Abschluss der Entwicklung des Features wird dieser Merge-Request jedoch zunächst mit Work-in-Progress (WIP) gekennzeichnet, sodass klar ist, dass daran aktuell noch gearbeitet wird. Der Feature-Branch kann nach jedem Commit automatisch gebaut, getestet und bei Bedarf auch deployt werden. Sofern die verwendete Deployment-Umgebung dies unterstützt, ist es möglich, für einen Feature-Branch eine spezifische Review-Umgebung zu erstellen, auf der der aktuelle Entwicklungsstand unter einer eindeutigen URI erreichbar ist. Nach Abschluss der Entwicklung wird das WIP-Flag aus dem Merge-Request entfernt. Anschließend kann ein Entwickler die Änderungen reviewen, den Feature-Branch mergen und den Merge-Request schließen, was im Ticket entsprechend gekennzeichnet wird.
Erstellung einer Pipeline
Nach jedem Commit durchläuft das Projekt die Build-Pipeline. Eine Build-Pipeline in GitLab besteht aus Stages und Jobs. Zunächst müssen die Build-Pipelines in der GitLab-Weboberfläche aktiviert werden (Settings -> General -> Permissions -> Pipelines). Die deklarative Beschreibung der eigentlichen Pipeline befindet sich in einer Datei namens „.gitlab-ci.yml“, welche sich mit dem eigentlichen Projekt-Quellcode im Repository befindet. Die Stages definieren die Reihenfolge der Pipeline. Sie werden am Anfang der Datei „.gitlab-ci.yml“ definiert und können dann in den Jobs referenziert werden. Hierdurch wird definiert, in welcher Stage ein Job ausgeführt wird. Ein Job hat genau eine Stage, eine Stage wiederum kann mehrere Jobs enthalten – diese werden dann parallel ausgeführt. Eine Definition von Stages sieht beispielsweise so aus:
Aus obigen Stages (und zugeordneten Jobs) wird in der Weboberfläche die in Abbildung 3 gezeigte Darstellung generiert. Ein Job ist ein einzelner Build-Schritt, der sich mindestens aus den folgenden Komponenten zusammensetzt:
- Der Name eines Jobs muss innerhalb der Datei „.gitlab-ci.yml“ eindeutig sein (hier
build_jar
). - stage: Die Stage, in welcher der Job ausgeführt werden soll. Sind Jobs voneinander abhängig, sollten diese demnach in aufeinanderfolgenden Stages ausgeführt werden.
-
image (optional): Der Name des Docker-Images, welches die Build-Umgebung definiert. Neben vorhandenen Images, zum Beispiel vom Docker-Hub, können auch zuerst eigene Images erstellt werden, welche zusätzliches Tooling enthalten. (hier
maven:3-jdk-8
). Wenn kein Image im Job selbst definiert worden ist, muss dies global, am Anfang der Datei „.gitlab-ci.yml“ definiert worden sein. - script: Hier werden die eigentlich Befehle für diesen Job definiert. Es können mehrere Befehle angegeben werden, die dann nacheinander ausgeführt werden.
-
artifacts (optional): Nachdem ein Job ausgeführt wird, wird der komplette Build-Kontext (der Docker-Container) wieder abgebaut. Manchmal ist es notwendig, dass Ergebnisse des Builds darüber hinaus aufbewahrt werden. Dies lässt sich über das
artifacts
-Statement konfigurieren. - tags (optional): Ähnlich wie bei anderen CI-Systemen lassen sich Jobs auf spezifischen Runnern ausführen. Das Mapping zwischen Jobs und Runnern erfolgt über einen oder mehrere Tags. Werden keine Tags angegeben, so läuft der Build auf sogenannten „shared Runnern“, so diese in GitLab CI definiert worden sind.
GitLab stellt für die Builds einige Umgebungsvariablen bereit, beispielsweise CI_COMMIT_REF_NAME
(Branch-Name) oder
CI_COMMIT_SHA
(git Commit SHA), zusätzlich können in der „.gitlab-ci.yml“ oder über die Weboberfläche weitere Variablen
definiert werden. Um sie zu verwenden, muss dem Variablennamen jeweils ein $ vorangestellt werden.
Build-Job
Als ersten Job bauen wir das Projekt und verpacken es in eine jar-Datei:
Das resultierende Artefakt kann im Anschluss bequem über die GitLab-Weboberfläche als Datei heruntergeladen werden. Moderne Build-Systeme verwenden zahlreiche Abhängigkeiten, welche erst aus dem Internet heruntergeladen werden müssen, bevor der eigentlich Build-Prozess starten kann. Dies dauert oftmals mehrere Minuten. Da sich diese Abhängigkeiten im Laufe der Zeit selten ändern, lohnt es sich, die bereits heruntergeladenen Dateien zu cachen und jeweils vor dem Start des Jobs dem Kontext wieder zur Verfügung zu stellen, um den Build zu beschleunigen. GitLab CI bietet hier die Möglichkeiten an, Abhängigkeiten entweder auf Job- oder auf Pipeline-Ebene zu cachen. In diesem Beispiel verwenden wir das Cachen auf Pipeline-Ebene: Alle Jobs dieser Pipeline verfügen über einen gemeinsamen Pfad, welcher über alle Jobs hinweg verwendet werden kann. Die Konfiguration dafür findet auf der obersten Ebene in der Datei „.gitlab-ci.yml“ statt:
Alle Jobs dieser Pipeline bekommen das Verzeichnis „.m2/repository“ zur Verfügung gestellt. Damit dieses von Maven dann auch verwendet wird, sollte es über eine Variable am Anfang der Pipeline-Definition ebenfalls angegeben werden:
Test
Als Nächstes erstellen wir einen Job zum Ausführen von Integrationstests. Für die Tests wird eine Postgres-Datenbank benötigt. Deshalb wird zur Laufzeit des Jobs ein zusätzlicher Docker-Container mit der Datenbank gestartet. Wir verwenden dafür das Standard-Postgres-Image von DockerHub. Dieses bekommt drei Variablen übergeben:
- den Namen der anzulegenden Datenbank (
POSTGRES_DB
), - den Namen des anzulegenden Benutzers (
POSTGRES_USER
) und - dessen Passwort (
POSTGRES_PASSWORD
). Die Definition dieser Variablen erfolgt über den Abschnitt
Damit steht für den Build eine Datenbank zur Verfügung, die wir aus der Anwendung heraus ansprechen können. Das Spring-Profil
cibuild
, welches über die Datei „application.yml“ definiert wird, dient dazu, Parameter festzulegen, die spezifisch für
die Build-Umgebung sind. Wir hinterlegen dort den Hostnamen postgres
der im Docker-Container laufenden Datenbank:
Tests lassen sich parallel ausführen, indem sie der gleichen Stage (hier test
) zugeordnet werden:
Bisher bietet GitLab keine eigene Möglichkeit, sich den Verlauf der Testabdeckung über verschiedene Builds hinweg in einem Diagramm visualisieren zu lassen. Man kann sich aber die Testabdeckung über die Weboberfläche ausgeben lassen, indem man unter „Settings -> CI/CD -> General Pipeline Settings -> Test coverage parsing“ einen regulären Ausdruck angibt, mit dem die Testabdeckung in der Log-Ausgabe des Builds gefunden werden kann.
Paketieren des Docker-Images
Meistens möchte man aber nicht nur die Anwendung als Archive zur Verfügung stellen, sondern gleich an das Bauen und Paketieren ein automatischen Deployment anhängen. Hierzu bietet es sich an, ein Docker-Image der gebauten Anwendung zu erstellen. Dieses Image wird in der GitLab-Docker-Registry hinterlegt und kann dann für weitere Build-Schritte verwendet werden. Der Build eines Docker-Images wird meistens über ein Dockerfile spezifiziert. Im einfachsten Fall kann diese (z. B. für eine Spring Boot-Anwendung) so aussehen:
Das Bauen des Docker-Images und das anschließende Pushen in die Docker-Registry finden ebenfalls über einen definierten Schritt in der Datei „.gitlab-ci.yml“ statt:
Der Build-Schritt enthält ein paar Variablen, die zur Laufzeit im Build-Prozess zur Verfügung stehen. Die Anmeldung an die GitLab
interne Docker-Registry ($CI_REGISTRY
) findet über einen Token $CI_JOB_TOKEN
statt. Dieser Token ist nur für einen
kurzen Zeitraum gültig (per Default bis 5 Minuten nach dem Beenden des Build-Jobs). $DOCKER_IMAGE_TAGGED
ist eine selbst definierte Variable:
Die Variable referenziert das aktuelle Docker-Image: registry-url/image:version
. Die Version ist hier der aktuell
$CI_COMMIT_SHA
. Hierdurch lässt sich jedes Docker-Image eindeutig einem bestimmten Codestand zuordnen. Build-Jobs
lassen sich ebenfalls auf bestimmte Branches (Master oder nicht) beschränken.
Oftmals möchte man einen Release-Build speziell taggen (z. B. durch ein Docker-Image mit dem Tag lastest). Dies lässt
sich mit der folgenden Konfiguration erreichen:
Dieser Schritt wird nur bei Änderungen im Master-Branch ausgeführt. Das im vorherigen Schritte gebaute Docker-Image wird
aus der Registry gepullt, mit dem latest
-Tag versehen und wieder in die Registry gepusht. Im Normalfall sollten hier
keine Daten hochgeladen werden müssen, sondern nur der latest-Tag neu referenziert werden.
Verschiedene Pipelines für Master- und Feature-Branches
GitLab CI sieht als Konvention vor, dass alle Änderungen im Code in einem Deployment resultieren. Änderungen an einem
Feature-Branch werden als sogenannte Review Application deployt und können dann nach dem Review in den Master-Branch
gemerged werden. Änderungen im Master-Branch führen zu einem Deployment in die eigentliche Umgebung – zumeist erst in
Staging und danach in Produktion.
In der GitLab CI DSL lassen sich Jobs so konfigurieren, dass sie nur auf Änderungen reagieren, die nur, oder nicht
im Master auftreten (dazu später mehr). Dies führt zu zwei unterschiedlichen Pipelines, jeweils für ein Deployment aus
einem Feature- (s. Abb. 4) oder einem Master-Branch (s. Abb. 5).
Das Deployment der Anwendung selbst ist für GitLab CI hierbei vollkommen transparent. Im aktuellen Beispiel wird das Deployment
durch docker-compose
durchgeführt. Hierbei führt ein docker-compose up -d
zu einem neuen Deployment. Ein
docker-compose down
entfernt ein zuvor getätigtes Deployment wieder. Das Skript „prepare-docker-compose.sh“
sorgt dafür, dass die frisch deployte Anwendung unter der in APP_DEPLOY_URL
stehenden URL erreichbar ist.
Deployment eines Master-Branches
Abschließend möchten wir den Release-Zustand des Images deployen. Das Deployment soll gleichzeitig als Eintrag im Environment-Bereich der Weboberfläche zu finden sein:
Wie bereits erwähnt, wird in diesem Beispiel das Deployment über eine docker-compose-Datei durchgeführt. Die Variable
$APP_URL
definiert eine URL, unter der alle Deployments des Projektes erreichbar sind. Dieser Link wird sowohl dynamisch
in der Datei „docker-compose.yml“ verwendet als auch in der Definition der production-Umgebung.
Deployment von Feature-Branches
Neben dem finalen Deployment eines Master-Branches in Produktion bietet GitLab CI auch die Möglichkeit, einzelne Feature-Branches als Review-Deployments bereitzustellen. Hierzu sind zwei Angaben notwendig:
- der eigentliche Deployment-Schritt, welcher sehr ähnlich zum Deployment eines Master-Branches ist,
- ein Schritt, der definiert, wie ein vorhandenes Deployment wieder rückgängig gemacht werden kann.
Letzteres ist sowohl notwendig, wenn ein Review-Deployment manuell beendet wird, als auch dann, wenn ein Feature-Branch in den Master-Branch gemergt wird:
Im Unterschied zum Deployment eines Master-Branches ist hier die URL des Deployments abhängig vom Branch-Namen. Der
Environment-Name review/$CI_COMMIT_REF_NAME
bewirkt zum einen, dass alle Review-Deployments in der Weboberfläche in
einer Gruppe review
zusammengefasst werden, zum anderen gibt die Variable $CI_COMMIT_REF_NAME
den aktuellen Namen
des Feature-Branches an 3-add-loginvia-account
und ermöglicht so eine einfache Zuordnung.
$CI_ENVIRONMENT_SLUG
ist hier ein String von maximal 64 Zeichen Länge, der sich aus dem Branch-Namen ableitet
(z. B. review-3-addlogi-2w6jqu
aus dem Branch-Namen 3-add-login-via-account
). Weiterhin ist beim Environment eine
on_stop
-Action angegeben. Diese ist wie folgt definiert:
Diese Aktion führt ein docker-compose down
basierend auf dem aktuellen Deployment-Kontext durch. Das when: manual definiert,
dass diese Aktion manuell über die Weboberfläche ausgeführt werden kann. action: stop
im environment
-Bereich definiert
weiterhin, dass diese Aktion ebenfalls ausgeführt wird, wenn die spezifische Umgebung gestoppt wird.