Blog Engineering Grundlagen der GitLab-CI-Pipeline: Aufgaben sequenziell parallel oder ohne Reihenfolge ausführen
Aktualisiert am: May 16, 2025
15 Minuten Lesezeit

Grundlagen der GitLab-CI-Pipeline: Aufgaben sequenziell parallel oder ohne Reihenfolge ausführen

Neu in der Continuous Integration? Erfahre, wie du deine erste CI-Pipeline mit GitLab erstellst.

cicd-cover

Nehmen wir an, dass du nichts über kontinuierliche Integration (CI) and why it's needed weißt und darüber, warum sie im Lebenszyklus der Softwareentwicklung benötigt wird.

Inhaltsverzeichnis

Stell dir vor, du arbeitest an einem Projekt, bei dem der gesamte Code aus zwei Textdateien besteht. Dabei ist es sehr wichtig, dass die Verkettung dieser beiden Dateien die Phrase „Hello world" enthält.

Wenn das nicht der Fall ist, wird das gesamte Development-Team in diesem Monat nicht bezahlt. Ja, so ernst ist es!

Der oder die verantwortliche Softwareentwickler(in) hat ein kleines Skript geschrieben, das jedes Mal ausgeführt wird, wenn wir unseren Code an die Kunden senden wollen.

Der Code ist ziemlich komplex:

cat file1.txt file2.txt | grep -q "Hello world"

Das Problem ist, dass das Team aus 10 Entwickler(inne)n besteht. Da bleiben menschliche Fehler nicht aus.

Vor einer Woche vergaß einer der Mitarbeiter(innen), das Skript auszuführen, und drei Kund(inn)en erhielten fehlerhafte Builds. Also hast du beschlossen, dieses Problem endgültig zu lösen. Glücklicherweise befindet sich der Code bereits auf GitLab, und du erinnerst dich, dass es eine integrierte CI gibt. Zudem hast du auf einer Konferenz gehört, dass viele Entwickler(innen) eine CI verwenden, um Tests durchzuführen...

Der erste Test in CI

Nach ein paar Minuten Suche und Lesen der Dokumentation scheint es, dass wir nur diese zwei Codezeilen benötigen, die wir in einer Datei namens .gitlab-ci.yml finden:

test:
  script: cat file1.txt file2.txt | grep -q 'Hello world'

Wir übertragen die Zeilen, und siehe da– unser Build ist erfolgreich:

build succeeded

Nun ändern wir in der zweiten Datei "World" zu "Africa" und prüfen, was passiert:

build failed

Der Build schlägt wie erwartet fehl!

Nun haben wir hier automatisierte Tests! GitLab CI führt unser Testskript jedes Mal aus, wenn wir neuen Code in das Quellcode-Repository in der DevOps-Umgebung übertragen.

Hinweis: Im obigen Beispiel gehen wir davon aus, dass file1.txt und file2.txt auf dem Runner-Host vorhanden sind.

Um dieses Beispiel in GitLab auszuführen, verwende den folgenden Code, der zunächst die Dateien erstellt und dann das Skript ausführt.

test:
before_script:
      - echo "Hello " > | tr -d "\n" | > file1.txt
      - echo "world" > file2.txt
script: cat file1.txt file2.txt | grep -q 'Hello world'

Aus Gründen der Übersichtlichkeit gehen wir davon aus, dass diese Dateien auf dem Host vorhanden sind und werden sie in den folgenden Beispielen nicht erstellen.

Ergebnisse von Builds zum Herunterladen bereitstellen

Die nächste Anforderung besteht darin, den Code zu paketieren, bevor wir ihn an unsere Kunden senden. Lass uns auch diesen Teil des Softwareentwicklungsprozesses automatisieren!

Alles, was wir machen müssen, ist, einen weiteren Job für CI zu definieren. Nennen wir den Auftrag mal „Package":

test:
  script: cat file1.txt file2.txt | grep -q 'Hello world'

package:
  script: cat file1.txt file2.txt | gzip > package.gz

Nun haben wir zwei Tabs:

Two tabs - generated from two jobs

Wir haben jedoch vergessen anzugeben, dass die neue Datei ein Build-Artefakt ist, damit sie heruntergeladen werden kann. Wir können dies beheben, indem wir einen Abschnitt für Artifacts hinzufügen:

test:
  script: cat file1.txt file2.txt | grep -q 'Hello world'

package:
  script: cat file1.txt file2.txt | gzip > packaged.gz
  artifacts:
    paths:
    - packaged.gz

Checking... it is there:

Checking the download button

So klappt's. Wir haben jedoch noch ein Problem zu lösen: Die Aufträge laufen parallel, aber wir wollen unsere Anwendung nicht paketieren, wenn unsere Tests fehlschlagen.

Aufträge der Reihe nach ausführen

Der Auftrag „Paket" soll nur ausgeführt werden, wenn die Tests erfolgreich sind. Definieren wir die Reihenfolge, indem wir stages angeben:

stages:
  - test
  - package

test:
  stage: test
  script: cat file1.txt file2.txt | grep -q 'Hello world'

package:
  stage: package
  script: cat file1.txt file2.txt | gzip > packaged.gz
  artifacts:
    paths:
    - packaged.gz

Das sollte funktionieren! Außerdem haben wir vergessen zu erwähnen, dass die Zusammenstellung (die in unserem Fall durch Verkettung dargestellt wird) eine Weile dauert, sodass wir sie nicht zweimal ausführen wollen. Definieren wir also einen separaten Schritt dafür:

stages:
  - compile
  - test
  - package

compile:
  stage: compile
  script: cat file1.txt file2.txt > compiled.txt
  artifacts:
    paths:
    - compiled.txt

test:
  stage: test
  script: cat compiled.txt | grep -q 'Hello world'

package:
  stage: package
  script: cat compiled.txt | gzip > packaged.gz
  artifacts:
    paths:
    - packaged.gz

Lass uns jetzt auf unsere Artifacts an:

Unnecessary artifact

Wir brauchen diese „Kompilierungsdatei" nicht zum Herunterladen. Deshalb lassen wir unsere temporären Artefakte ablaufen, indem wir expire_in auf „20 Minuten" setzen:

compile:
  stage: compile
  script: cat file1.txt file2.txt > compiled.txt
  artifacts:
    paths:
    - compiled.txt
    expire_in: 20 minutes

Jetzt sieht unsere Konfiguration ziemlich beeindruckend aus:

  • Wir haben drei aufeinanderfolgende Phasen zum Kompilieren, Testen und Paketieren unserer Anwendung.
  • Wir übergeben die kompilierte Anwendung an die nächsten Stufen, damit die Kompilierung nicht zweimal ausgeführt werden muss (und somit schneller läuft).
  • Wir speichern eine paketierte Version unserer Anwendung in Build-Artefakten für die weitere Verwendung.

Welches Docker Image muss verwendet werden?

Es scheint, dass unsere Builds immer noch langsam sind. Werfen wir einen Blick auf die Protokolle.

ruby3.1

Was ist Ruby 3.1?

GitLab.com verwendet Docker-Images, um unsere Builds auszuführen, und standardmäßig wird das ruby:3.1-Image verwendet. Dieses Image enthält natürlich viele Pakete, die wir nicht brauchen. Nach einer Minute des Googlens finden wir heraus, dass es ein Image namens alpine gibt, das ein fast leeres Linux-Image ist.

Wir geben also explizit an, dass wir dieses Image verwenden wollen, indem wir image: alpineto.gitlab-ci.yml`.

Wir haben so drei Minuten gespart:

Build speed improved

Es sieht so aus, als gäbe es viele öffentliche Images:

Wir können also einfach eines für unseren Technologie-Stack nehmen. Es ist sinnvoll, ein Image anzugeben, das keine zusätzliche Software enthält, da dies die Downloadzeit verringert.

Umgang mit komplexen Szenarien

Nehmen wir nun aber an, wir haben neue Kund(inn)en, die möchten, dass wir unsere Anwendung in ein .iso-Image statt in ein .gz-Image packen. ISO-Images können mit dem Befehl mkisofs erstellt werden. Da CI die ganze Arbeit erledigt, können wir einfach einen weiteren Job hinzufügen. Darauf basierend sollte unsere Konfiguration so aussehen:

image: alpine

stages:
  - compile
  - test
  - package

# ... "compile" and "test" jobs are skipped here for the sake of compactness

pack-gz:
  stage: package
  script: cat compiled.txt | gzip > packaged.gz
  artifacts:
    paths:
    - packaged.gz

pack-iso:
  stage: package
  script:
  - mkisofs -o ./packaged.iso ./compiled.txt
  artifacts:
    paths:
    - packaged.iso

Beachte, dass die Auftragsnamen nicht unbedingt gleich sein sollten. Wären sie identisch, wäre es nicht möglich, die Aufträge innerhalb derselben Phase des Softwareentwicklungsprozesses parallel laufen zu lassen. Sollte es daher doch mal vorkommen, kannst du das getrost als Zufall betrachten.

Wie dem auch sei, zurück zu unserem Job, da läuft es nicht recht rund – der Build schlägt fehl:

Failed build because of missing mkisofs

mkisofs ist nicht im alpine Image mit dabei, also müssenw ir es erstmal installieren.

Umgang mit fehlender Software/Paketen

Laut der Alpine Linux website ist mkisofs Teil der Pakete xorriso und cdrkit. Dies sind die Befehle, die wir ausführen müssen, um ein Paket zu installieren:

echo "ipv6" >> /etc/modules  # enable networking
apk update                   # update packages list
apk add xorriso              # install package

Für CI sind dies die gleichen Befehle wie für alle anderen. Die vollständige Liste der Befehle, die wir dem Skriptabschnitt übergeben müssen, sollte wie folgt aussehen:

script:
- echo "ipv6" >> /etc/modules
- apk update
- apk add xorriso
- mkisofs -o ./packaged.iso ./compiled.txt

Um es jedoch semantisch korrekt zu machen, sollten wir die Befehle, die sich auf die Paketinstallation beziehen, in before_script unterbringen. Beachte, dass, wenn du before_script auf der obersten Ebene einer Konfiguration verwendest, die Befehle vor allen Aufträgen ausgeführt werden. In unserem Fall wollen wir nur, dass sie vor einem bestimmten Auftrag ausgeführt werden.

Directed Acyclic Graphs: Schnellere und flexiblere Pipelines

Wir haben die Stufen so definiert, dass die Paketaufgaben nur ausgeführt werden, wenn die Tests bestanden wurden. Was aber, wenn wir die Phasenabfolge ein wenig aufbrechen und einige Aufträge früher ausführen wollen, auch wenn sie in einer späteren Phase definiert sind? In einigen Fällen kann die herkömmliche Phasenabfolge die Gesamtausführungszeit der Pipeline verlangsamen.

Stell dir vor, dass unsere Testphase einige umfangreichere Tests enthält, deren Ausführung viel Zeit in Anspruch nimmt und diese Tests nicht unbedingt mit den Paketaufgaben zusammenhängen. In diesem Fall wäre es effizienter, wenn die Paketaufgaben nicht auf den Abschluss dieser Tests warten müssten, bevor sie beginnen können. An dieser Stelle kommen Directed Acyclic Graphs (DAG) ins Spiel: Um die Phasenreihenfolge für bestimmte Aufträge zu unterbrechen, kannst du Abhängigkeiten von Aufgaben definieren, die die reguläre Phasenreihenfolge übergehen.

GitLab verfügt über ein spezielles Keyword „needs", das Abhängigkeiten zwischen Aufträgen schafft und es ermöglicht, Aufträge früher auszuführen, sobald ihre abhängigen Aufträge abgeschlossen sind.

Im folgenden Beispiel werden die Paketaufgaben ausgeführt, sobald der Testjob abgeschlossen ist. Wenn also in Zukunft jemand weitere Tests in der Testphase hinzufügt, beginnen die Paketjobs zu laufen, bevor die neuen Testjobs abgeschlossen sind

pack-gz:
  stage: package
  script: cat compiled.txt | gzip > packaged.gz
  needs: ["test"]
  artifacts:
    paths:
    - packaged.gz

pack-iso:
  stage: package
  before_script:
  - echo "ipv6" >> /etc/modules
  - apk update
  - apk add xorriso
  script:
  - mkisofs -o ./packaged.iso ./compiled.txt
  needs: ["test"]
  artifacts:
    paths:
    - packaged.iso

Unsere finale Version von: .gitlab-ci.yml:

image: alpine

stages:
  - compile
  - test
  - package

compile:
  stage: compile
  before_script:
      - echo "Hello  " | tr -d "\n" > file1.txt
      - echo "world" > file2.txt
  script: cat file1.txt file2.txt > compiled.txt
  artifacts:
    paths:
    - compiled.txt
    expire_in: 20 minutes

test:
  stage: test
  script: cat compiled.txt | grep -q 'Hello world'

pack-gz:
  stage: package
  script: cat compiled.txt | gzip > packaged.gz
  needs: ["test"]
  artifacts:
    paths:
    - packaged.gz

pack-iso:
  stage: package
  before_script:
  - echo "ipv6" >> /etc/modules
  - apk update
  - apk add xorriso
  script:
  - mkisofs -o ./packaged.iso ./compiled.txt
  needs: ["test"]
  artifacts:
    paths:
    - packaged.iso

Wir haben gerade eine Pipeline erstellt! Wir haben drei sequenzielle Stufen, die Aufträge pack-gz und pack-iso innerhalb der package-Stufe laufen parallel:

Pipelines illustration

Wie wertest du deine Pipeline auf?

So kannst du deine Pipeline aufwerten.

Automatisierte Tests in CI-Pipelines einbinden

Eine wichtige Regel der DevOps-Strategie für die Softwareentwicklung besteht darin, wirklich großartige Anwendungen mit erstaunlicher Benutzererfahrung zu entwickeln. Fügen wir also einige Tests in unsere CI-Pipeline ein, um Fehler frühzeitig im gesamten Prozess zu erkennen. Auf diese Weise können wir Probleme beheben, bevor sie zu groß werden und bevor wir an einem neuen Projekt weiterarbeiten.

GitLab macht uns das Leben leichter, indem es fertige Vorlagen für verschiedene Tests anbietet. Alles, was wir tun müssen, ist, diese Vorlagen in unsere CI-Konfiguration aufzunehmen.

In diesem Beispiel schließen wir auch Accessibility-Tests ein:

stages:
  - accessibility

variables:
  a11y_urls: "https://about.gitlab.com https://www.example.com"

include:
  - template: "Verify/Accessibility.gitlab-ci.yml"

Passe die Variable a11y_urls an, um die URLs der Webseiten aufzulisten, die mit Pa11y und der Codequalität getestet werden sollen.

   include:
   - template: Jobs/Code-Quality.gitlab-ci.yml

Mit GitLab kannst du den Testbericht direkt im Widget-Bereich der Zusammenführungsanforderung sehen. Wenn du die Codeüberprüfung, den Pipelinestatus und die Testergebnisse an einem Ort hast, wird alles reibungsloser und effizienter.

Accessibility report

Widget für die Zusammenführung von Accessibility-Anfragen

Code quality widget in MR

Widget für Zusammenführungsanfragen in Codequalität

Matrix-Builds

In einigen Fällen müssen wir unsere Anwendung in verschiedenen Konfigurationen, Betriebssystemversionen, Programmiersprachenversionen usw. testen. In diesen Fällen verwenden wir den parallel:matrix-Build, um unsere Anwendung in verschiedenen Kombinationen parallel mit einer Job-Konfiguration zu testen. In diesem Artikel werden wir unseren Code mit verschiedenen Python-Versionen unter Verwendung des Schlüsselworts matrix testen.

python-req:
  image: python:$VERSION
  stage: lint
  script:
    - pip install -r requirements_dev.txt
    - chmod +x ./build_cpp.sh
    - ./build_cpp.sh
  parallel:
    matrix:
      - VERSION: ['3.8', '3.9', '3.10', '3.11']   # https://hub.docker.com/_/python

Während der Pipeline-Ausführung wird dieser Auftrag viermal parallel ausgeführt, wobei jedes Mal ein anderes Python-Image verwendet wird (siehe unten):

Matrix job running

Unit-Tests

Was sind Unit-Tests?

Unit-Tests sind kleine, gezielte Tests, die einzelne Komponenten oder Funktionen von Software prüfen, um sicherzustellen, dass sie wie erwartet funktionieren. Sie sind wichtig, um Fehler in einem frühen Stadium des Softwareentwicklungsprozesses aufzuspüren und zu überprüfen, ob jeder Teil des Codes für sich genommen korrekt funktioniert.

Beispiel: Stell dir vor, du entwickelst eine Taschenrechner-App. Ein Unit-Test für die Additionsfunktion würde prüfen, ob 2 + 2 gleich 4 ist. Wenn dieser Test erfolgreich ist, bestätigt er, dass die Additionsfunktion korrekt funktioniert.

Best Practices für Unit-Tests

Wenn die Tests fehlschlagen, schlägt die Pipeline fehl und die Benutzer werden benachrichtigt. Entwickler(innen) müssen die Auftragsprotokolle, die in der Regel Tausende von Zeilen enthalten, überprüfen und feststellen, wo die Tests fehlgeschlagen sind, um sie zu korrigieren. Diese Prüfung ist zeitaufwendig und ineffizient.

Du kannst deinen Auftrag so konfigurieren, dass er Unit-Test-Berichte verwendet. GitLab zeigt die Berichte in der Zusammenführungsanforderung und auf der Detailseite der Pipeline an, sodass du den Fehler einfacher und schneller identifizieren kannst, ohne das gesamte Protokoll überprüfen zu müssen.

JUnit Test-Report

Dies ist ein beispielhafter JUnit Test-Report:

pipelines JUnit test report v13 10

Strategien für Integrations- und End-to-End-Tests

Zusätzlich zu unserer regulären Entwicklungsroutine ist es sehr wichtig, eine spezielle Pipeline nur für Integrations- und End-to-End-Tests einzurichten. Damit wird überprüft, ob alle verschiedenen Teile unseres Codes reibungslos zusammenarbeiten, einschließlich der Microservices, der UI-Tests und aller anderen Komponenten.

Wir führen diese Tests jede Nacht durch. Wir können es so einrichten, dass die Ergebnisse automatisch an einen speziellen Slack-Kanal gesendet werden. Auf diese Weise können die Entwickler(innen), wenn sie am nächsten Tag kommen, schnell alle Probleme erkennen. Es geht darum, Probleme frühzeitig zu erkennen und zu beheben!

Testumgebung

Für einige der Tests benötigen wir möglicherweise eine Testumgebung, um unsere Anwendungen ordnungsgemäß zu testen. Mit GitLab CI/CD können wir die Bereitstellung von Testumgebungen automatisieren und so eine Menge Zeit sparen. Da es in diesem Blog hauptsächlich um CI geht, werde ich nicht näher darauf eingehen, aber du kannst diesen Abschnitt in der GitLab-Dokumentation nachlesen.

Implementierung von Sicherheitsscans in CI-Pipelines

Folgend siehst du die Möglichkeiten zur Implementierung von Sicherheitsscans in CI-Pipelines.

SAST und DAST-Integration

Wir legen großen Wert darauf, dass unser Code sicher ist. Wenn unsere letzten Änderungen Schwachstellen aufweisen, wollen wir das so schnell wie möglich wissen. Sicherheitsscans sind hier eine sinnvolle Lösung und wir empfehlen dir, sie auch in deine Pipeline aufzunehmen. Sie überprüfen den Code bei jeder Übertragung und warnen dich vor möglichen Risiken. Wir haben eine Produktübersicht zusammengestellt, die dich durch das Hinzufügen von Scans, einschließlich statischer Anwendungssicherheitstests (SAST) und dynamischer Anwendungssicherheitstests (DAST), zu deiner CI-Pipeline führt.

Klicke auf das Bild unten, um zur Übersicht zu gelangen.

Scans product tour

Außerdem können wir mithilfe von KI noch tiefer in Schwachstellen eindringen und Vorschläge zu ihrer Behebung erhalten.

Weitere Informationen findest du in dieser Demo.

product tour explain vulnerability

Zusammenfassung

Es gibt noch viel mehr zu erläutern, aber lass uns hier erst einmal aufhören. Alle Beispiele sind bewusst einfach gehalten, um das Konzept GitLab CI vorzustellen, ohne die Dinge zu verkomplizieren. Fassen wir zusammen, was wir gelernt haben:

  1. Um Arbeit an GitLab CI zu delegieren, solltest du einen oder mehrere Jobs in.gitlab-ci.yml. definieren.
  2. Jobs sollten Namen haben – also denk dir was Gutes aus! Jeder Auftrag enthält eine Reihe von Regeln und Anweisungen für GitLab CI, die durch spezielle Schlüsselwörter definiert sind.
  3. Aufträge können nacheinander, parallel oder ungeordnet über DAG ausgeführt werden.
  4. Du kannst Dateien zwischen Aufträgen weitergeben und sie in Build-Artefakten speichern, sodass sie über die Schnittstelle heruntergeladen werden können.
  5. Du kannst Dateien zwischen Aufträgen weitergeben und sie in Build-Artefakten speichern, sodass sie über die Schnittstelle heruntergeladen werden können.

Nachstehend findest du eine genauere Beschreibung der von uns verwendeten Begriffe und Schlüsselwörter sowie Links zu den entsprechenden Dokumenten.

Beschreibungen der Keywords

Keyword/term Beschreibung
.gitlab-ci.yml Datei mit allen Definitionen dazu, wie dein Projekt aufgebaut sein sollte
script Definiert ein Shell-Script, das ausgeführt werden soll
before_script Wird verwendet, um den Befehl zu definieren, der vor (allen) Aufträgen ausgeführt werden soll
image Definiert das zu verwendende Docker-Image
stages Legt eine Pipelinestufe fest (Standard: test)
artifacts Definiert eine Liste von Build-Artifacts
artifacts:expire_in Wird verwendet, um hochgeladene Artifacts nach der angegebenen Zeit zu löschen
needs Dient zur Definition von Abhängigkeiten zwischen Aufträgen und ermöglicht die Ausführung von Aufträgen außerhalb der Reihenfolge
pipelines Eine Pipeline ist eine Gruppe von Builds, die stufenweise (Batches) ausgeführt werden

Wir möchten gern von dir hören

Hat dir dieser Blogbeitrag gefallen oder hast du Fragen oder Feedback? Erstelle ein neues Diskussionsthema im GitLab Community-Forum und tausche deine Eindrücke aus. Teile dein Feedback

Bist du bereit?

Sieh dir an, was dein Team mit einer einheitlichen DevSecOps-Plattform erreichen könnte.

Kostenlose Testversion anfordern

Finde heraus, welcher Tarif für dein Team am besten geeignet ist

Erfahre mehr über die Preise

Erfahre mehr darüber, was GitLab für dein Team tun kann

Sprich mit einem Experten/einer Expertin