Blog: Java, Software-Entwicklung & mehr

Erste Schritte mit GitLab CI/CD

14.02.2022 Kristina Fell

GitLab bietet einem die Möglichkeit, seine CI/CD Pipelines direkt in seinem GitLab-Projekt zu konfigurieren und auszuführen. So hat man alle Informationen zu seinem Git-Projekt an einem Fleck und kann über die GitLab UI im Browser bequem zwischen dem Quellcode, den Branches, der Merge-Historie und eben auch den Pipelines zum Ausführen von Tests und/oder Deployments wechseln.

Und wenn man sein Git-Repository bereits über GitLab verwaltet, ist das Hinzufügen einer Pipeline spielend leicht. Dazu wird einfach im Root-Verzeichnis des Projekts eine Datei mit dem Namen ".gitlab-ci.yml" angelegt. Dies kann man manuell machen oder direkt über einen Button in der Repository Übersicht in GitLab. Da diese Konfigurationsdatei für die Pipelines Teil des versionierten Codes ist, ergeben sich damit natürlich auch alle damit einhergehenden Vorteile. Das ist unter anderem die Änderungshistorie, mit der alle Änderungen an den Pipelines dokumentiert sind und bei Problemen entsprechend einfach nachvollzogen und zurückgedreht werden können. Auch ist sichergestellt, dass der Stand der Pipeline zum Stand des Codes passt, zum Beispiel wenn man ein neues Maven Goal definiert und dieses innerhalb der Pipeline ausführen möchte. Und sollte das Projekt in eine neue GitLab Installation umziehen, sind die Pipelines direkt wieder verfügbar.

Jobs - Die Schritte der Pipeline

Aber wie sieht so eine Konfiguration nun aus? Wir fangen ganz einfach an. Wir wollen unsere Anwendung nach einen Push zunächst Kompilieren und die Tests ausführen. Nehmen wir an, dass wir in unserem Projekt mit Maven arbeiten, dann könnte eine entsprechende Konfigurationsdatei, welche im YAML-Format geschrieben wird, wie folgt aussehen:


build:
  script:
    - mvn compile

test:
  script:
    - mvn test

Mit dieser Konfiguration definieren wir zwei Jobs: build und test. Diese Jobs entsprechen den Schritten, welche in der Pipeline ausgeführt werden. Mittels des Schlagworts "script" legen wir fest, was genau in diesem Schritt gemacht werden soll. Hier kann ein beliebiges Shell Skript angegeben werden, welches dann vom GitLab Runner ausgeführt wird.

Im Job "build" kompilieren wir in unserem Beispiel den Code einmal mit Hilfe von Maven, im Job "test" führen wir die Tests aus. Dies sind natürlich sehr einfache Beispiele. Die angegebenen Skripte können auch deutlich komplexer sein. Möchte man ein mehrzeiliges Skript für einen Job definieren, dann wird jede neue Zeile wieder mit einem Bindestrich begonnen.

Stages - Phasen der Pipeline

Bleiben wir zunächst bei unserer sehr einfachen 2-Schritt-Pipeline. So, wie wir sie definiert haben, sind die beiden Jobs gleichgestellt. Das heißt, wenn wir unseren Code zurück ins Remote Repository schreiben, wird die Pipeline gestartet und beide Schritte werden direkt parallel ausgeführt.

GitLab Stages

In dem Screenshot sehen wir, dass der Build Job fehlgeschlagen ist. Natürlich macht es nicht so viel Sinn, die Tests starten zu wollen, wenn sich der Code nicht kompilieren lässt. Also wollen wir den Test Job eigentlich erst anstoßen, nachdem der Build Job fertig ist. Dafür können wir Stages benutzen. Stages sind die Phasen in unserer Pipeline, welche nacheinander durchlaufen werden. Jede Stage kann dabei eine beliebige Anzahl an Jobs enthalten. Definieren wir also eine Stage für den Build und eine Stage für die Tests:


stages:
  - Build
  - Tests
  
build:
  stage: Build
  script:
    - mvn compile

test:
  stage: Tests
  script:
    - mvn test

Sehr gut. Nun wird unser Code zunächst kompiliert und danach erst die Tests gestartet. Und da die Jobs einer Stage nur dann automatisch angestoßen werden, wenn alle Jobs der vorangegangene Stage erfolgreich abgeschlossen sind, laufen die Tests auch nur dann, wenn der Code auch kompiliert werden kann.

GitLab Pipeline

Wenn wir jetzt schon eine eigene Test Phase haben, können wir direkt noch einen separaten Job für die Integrationstests hinzufügen.


stages:
  - Build
  - Tests
  
build:
  stage: Build
  script:
    - mvn compile

test:unit:
  stage: Tests
  script:
    - mvn test -DskipITs

test:integration:
  stage: Tests
  script:
    - mvn test -Dtest=*IT

When - Konfiguration des Ausführungszeitpunkts

Nun werden die beiden Test Jobs parallel gestartet, sobald der Build erfolgreich durchgelaufen ist. Allerdings wollen wir die Integrationstests vielleicht nicht unbedingt bei jedem Push starten. Sie sind ja für gewöhnlich langläufiger, manchmal von weiteren Systemen abhängig, beschäftigen vielleicht sogar die Datenbank, ... Es könnte ganz gut sein, die Integrationstests nur auf Wunsch anzustoßen. Also setzen wir diesen Job lieber auf manuelle Ausführung:


stages:
  - Build
  - Tests
  
build:
  stage: Build
  script:
    - mvn compile

test:unit:
  stage: Tests
  script:
    - mvn test -DskipITs

test:integration:
  stage: Tests
  script:
    - mvn test -Dtest=*IT
  when: manual

Dazu haben wir das Stichwort „when“ hinzugefügt mit dem Wert „manual“.

Damit wird der Schritt Integrationstests nicht mehr automatisch gestartet, aber wir können ihn jeder Zeit über die Pipeline Ansicht in GitLab manuell starten. Schritte in derselben Phase werden übrigens in der Pipeline Ansicht in alphabetischer Reihenfolge aufgelistet, die Reihenfolge in der Konfigurationsdatei spielt dabei keine Rolle.

GitLab Pipeline When

Rules - Bedingungen für die Jobs

Da wir nun sicherstellen, dass unser Code kompiliert und alle Tests durchlaufen, können wir unsere Anwendung danach auch gleich ins Maven Repository deployen. Allerdings wollen wir das nicht von jedem Branch aus ermöglichen, sonst überschreiben wir am Ende den Snapshot unserer Anwendung immer wieder mit Ständen, die unterschiedliche Features mal beinhalten und mal noch nicht. Also erlauben wir den Deploy Schritt am besten nur für den Master Branch.


stages:
  - Build
  - Tests
  - Deploy
  
build:
  stage: Build
  script:
    - mvn compile

test:unit:
  stage: Tests
  script:
    - mvn test -DskipITs

test:integration:
  stage: Tests
  script:
    - mvn test -Dtest=*IT
  when: manual
  
deploy:
  stage: Deploy
  script:
    - mvn deploy -Dmaven.test.skip=true
  rules:
    - if: '$CI_COMMIT_REF_NAME == "master"'

Mit dem Stichwort „rules“ können wir Regeln festlegen, wann ein Job Teil der Pipeline sein soll und wann nicht. In diesem Fall sagen wir, dass der Job genau dann in der Pipeline enthalten sein soll, wenn die Pipeline vom Branch namens „master“ aus erstellt wurde. Das bedeutet, für jeden anderen Branch sieht die Pipeline genauso aus wie vorher.

Dies wäre auch mit Hilfe des Stichworts „only“ möglich, aber „only“ und sein Gegenstück „except“ werden nicht aktiv weiterentwickelt und der Weg mittels „rules“ wird von der GitLab Dokumentation selbst als präferierter Weg vorgeschlagen.

Natürlich lassen sich mehrere Regeln für einen Job definieren. Wenn wir zum Beispiel erlauben wollen, dass von einem Hotfix Branch aus manuell der Deploy Job gestartet werden kann, können wir einfach ein weiteres IF ergänzen:


deploy:
  stage: Deploy
  script:
    - mvn deploy -Dmaven.test.skip=true
  rules:
    - if: '$CI_COMMIT_REF_NAME == "master"'
    - if: '$CI_COMMIT_REF_NAME =~ /^hotfix-/'
      when: manual

Dabei ist zu beachten, dass Regeln der Reihe nach ausgewertet werden. Sobald eine Regel zutrifft, finden die nachfolgenden Regeln keine Anwendung mehr.

CI/CD Variablen

In der Definition unserer Regel für den Deploy Job verwenden wir direkt ein weiteres hilfreiches Feature, die vordefinierten CI/CD Variablen von GitLab. CI/CD Variablen sind Variablen, welche innerhalb der Pipeline Konfiguration verwendet werden können. Diese lassen sich an zwei unterschiedlichen Stellen in der GitLab UI definieren. Zum einen gibt es Projekt-weite Variablen, welche in den Einstellungen unter dem Punkt „CI/CD“ hinzugefügt werden können. Zum anderen kann man diese Variablen direkt an eine Pipeline übergeben, wenn man diese über den Punkt „Run Pipeline“ manuell startet.

Besonders hilfreich sind aber auch die CI/CD Variablen, welche von GitLab vordefiniert sind und automatisch in jeder Pipeline zur Verfügung stehen. „$CI_COMMIT_REF_NAME“ ist eine davon und der Wert entspricht dem Namen des Branchs oder Tags, von welchem aus die Pipeline erstellt wurde. Es gibt einige solcher vordefinierten Variablen, zum Beispiel um den Autor des letzten Commits zu ermitteln, den Namen des Default Branchs, die ID der aktuellen Pipeline und vieles mehr. Eine Liste findet sich in der GitLab Dokumentation unter dem Punkt „Predefined Variables“

https://docs.gitlab.com/ee/ci/variables/predefined_variables.html

Extends - Vererbung von Eigenschaften

Würden wir nun weitere Jobs ergänzen, für welche dieselben Regeln gelten sollen, können wir die Regeln natürlich einfach an diesen neuen Job kopieren. Da das aber Mehraufwand wäre, wenn wir dieses Regelset ändern wollen, wollen wir lieber von Vererbung Gebrauch machen. Es ist nämlich auch möglich, einen Job die Eigenschaften eines anderen Jobs erben zu lassen. Dabei ist wichtig zu wissen, dass die Eigenschaften des erbenden Jobs höhere Priorität haben als die Eigenschaften des Jobs, von dem geerbt wird. Wir können die geerbten Eigenschaften also auch wieder überschreiben.

Hier ein kleines Beispiel:


stages:
  - Build
  - Tests
  - Deploy
  
[...]

.master_only:
  stage: Build
  rules:
    - if: '$CI_COMMIT_REF_NAME == "master"'
    - if: '$CI_COMMIT_REF_NAME =~ /^hotfix-/'
      when: manual
  
deploy:
  extends: .master_only
  stage: Deploy
  script:
    - mvn deploy -Dmaven.test.skip=true

In diesem Beispiel haben wir jetzt den Job ".master_only" hinzugefügt. Wichtig zu beachten ist der Punkt am Anfang des Jobnamens. Damit markieren wir „.master_only“ als einen versteckten Job, welcher selbst in der Pipeline nie auftauchen wird. Dies eignet sich sehr gut, wenn man ein paar Konfigurationsdetails auslagern möchte, um sie in mehreren Jobs wiederzuverwenden. Ein Job muss allerdings nicht versteckt sein, damit ein anderer Job von ihm erben kann.

Mit dem Stichwort "extends" im Deploy Job geben wir an, von welchen Jobs der Deploy Job Eigenschaften erben soll. Hier kann auch ein Array von Jobnamen angegeben werden, wobei zwingend auf die Reihenfolge zu achten ist. Denn der letzte Job in der Liste hat auch das letzte Wort. Das soll heißen, wenn von mehreren Jobs die Eigenschaft „rules“ geerbt wird, werden die Regeln des letzten Jobs in der Liste übernommen.

In unserem Beispiel haben wir sowohl in „.master_only“ als auch in „deploy“ die Stage definiert. Da der erbende Job die höhere Priorität hat, ist „deploy“ weiterhin Teil der Stage „Deploy“. Da aber an dem Deploy Job keine eigenen Regeln definiert sind, werden die Regeln von „.master_only“ übernommen. Damit sieht die Pipeline also genauso aus wie vorher, aber wir können die definierten Regeln nun ganz einfach für weitere Jobs wiederverwenden.

Mit diesen ersten Grundlagen dazu, wie wir mit Hilfe von GitLab unsere CI Pipelines als Teil unseres versionierten Codes implementieren können, können wir bereits einige hilfreiche Pipeline Definitionen für unsere Continuous Integration konfigurieren. Der Umfang der Funktionalitäten von GitLab CI/CD bietet aber natürlich noch zahlreiche weitere Möglichkeiten und wenn man den Eindruck hat, dass es eine sinnvolle Ergänzung im eigenen Projekt sein könnte, sollte man auf jeden Fall einen Blick in die GitLab Docs werfen

Quellen:

GitLab: http://www.gitlab.com/
GitLab Docs: https://docs.gitlab.com/