Jenkins-Tutorial: Installation, Build- und Release-Jobs - Feature-Bild

Jenkins-Tutorial: Installation, Build- und Release-Jobs

Im letzten Artikel über die Vor- und Nachteile von Monorepos habe ich gezeigt, wie ich ein Modul eines Git-Monorepos mit Jenkins gebaut habe – einmal über das Grafische User Interface von Jenkins – und einmal programmatisch über die Jenkins Job DSL. Einige Leser haben mich gefragt, wie man diesen DSL-Code in einen Jenkins-Job überführt.

Aus diesem Grund – und da ich im dritten Teil der Artikelserie über Statische Code-Analyse ebenfalls Gebrauch von Jenkins machen werde – werde ich euch in diesem und den zwei folgenden Artikeln die Installation sowie die wichtigsten Funktionen von Jenkins in Form eines Tutorials vorstellen. Im Detail werde ich euch zeigen:

  • Wie installiert man Jenkins?
  • Wie konfiguriert man manuell Jenkins-Build- und Release-Jobs für ein Maven-Projekt?
  • Wie programmiert man Jenkins-Build- und Release-Jobs mit der Jenkins Job DSL?
  • Wie generiert man Views mit der Jenkins Job DSL?
  • Wie konfiguriert man das Skript-Sicherheitssystem?
  • Wie aktualisiert man bei Änderungen im Job-DSL-Code die bestehende Jobs automatisch?
  • Wie generiert man für neue Java-Projekte vollautomatisch neue Jenkins-Jobs?

Installation von Jenkins als Docker Container mit Ansible

Im offiziellen Tutorial zur Installation von Jenkins werden für Linux zwei Installationsarten vorgestellt: als Docker Container und über den Package Manager. Die Installation über den Package Manager ist ziemlich schnell erledigt und empfiehlt sich für erste Experimente. Ich möchte Jenkins langfristig und reproduzierbar installieren und werde es deshalb – analog zu meiner WordPress-Installation – als Docker Container mit Ansible installieren.

Dazu sind folgende Schritte nötig:

  • Anlegen einer Ansible-Rolle
  • Erstellung einer docker-compose.yml-Datei auf dem Server
  • Start des Containers auf dem Server

Außerdem müsst ihr die Jenkins-Installation zugänglich machen, dazu gibt es verschiedene Möglichkeiten:

  • Zugriff auf den Container-Port über einen SSH-Tunnel (einfach und sicher)
  • Öffnen des Jenkins-Ports des Servers (unsicher)
  • Anlegen einer Subdomain (z. B. jenkins.happycoders.eu) und Konfiguration von haproxy, um Zugriffe auf die Subdomain auf den Jenkins-Container weiterzuleiten (aufwändig)

Schritt 1: Anlegen der Ansible-Rolle

Solltet ihr mit Ansible nicht vertraut sein, empfehle ich euch mein Tutorial zum Setup eines Root-Servers mit Ansible durchzuarbeiten. Alternativ könnt ihr diesen Schritt überspringen und die Schritte 2 bis 4 manuell ausführen. Der Vorteil von Ansible ist, dass die Schritte jederzeit automatisiert erneut ausgeführt werden können.

Ich lege in meinem Ansible-Projekt das Verzeichnis roles/jenkins/ und darunter die Unterverzeichnisse files/ und tasks/ an.

Schritt 2: Erstellung einer docker-compose-Datei auf dem Server

Aus dem unter https://jenkins.io/doc/book/installing/#on-macos-and-linux angegebenen docker run-Kommando erstelle ich folgende docker-compose.yml-Datei:

version: '3.3'

services:
  jenkins:
    ports:
      - 127.0.0.1:8080:8080
    volumes:
      - jenkins_home:/var/jenkins_home
    image: jenkins/jenkins
    restart: always

volumes:
  jenkins-data:

Wichtig ist hier beim Port-Mapping die IP-Adresse 127.0.0.1 mit anzugeben, da Port 8080 ansonsten offen aus dem Internet erreichbar wäre (es sei denn, ihr wollt das).

Manuelle Installation der docker-compose-Datei ohne Ansible

Bei der manuellen Installation wird die oben gezeigte Datei unter /opt/docker/jenkins/docker-compose.yml gespeichert.

Automatisierte Installation der docker-compose-Datei mit Ansible

Zur Installation mit Ansible wird die Datei zunächst im Unterverzeichnis roles/jenkins/files/ eures Ansible-Repositories als docker-compose.yml abgelegt. Danach legt ihr die Tasks-Datei roles/jenkins/tasks/main.yml mit folgendem Inhalt an:

- name: Create Jenkins docker directory
  file:
    path: /opt/docker/jenkins
    state: directory
    owner: root
    group: root
    mode: 0755

- name: Copy Jenkins docker-compose file
  copy:
    src: docker-compose.yml
    dest: /opt/docker/jenkins/
    owner: root
    group: root
    mode: 0644

Nun müsst ihr nur noch die Rolle „jenkins“ zum Ansible Playbook eures Servers hinzufügen und es ausführen. In folgendem Screenshot habe ich der Einfachheit halber alle anderen Rollen meines Servers auskommentiert:

Ausführen des Ansible Playbooks
Ausführen des Ansible Playbooks

Schritt 3: Start des Containers auf dem Server

Manueller Start des Containers ohne Ansible

Um den Container manuell zu starten, führt man im /opt/docker/jenkins/-Verzeichnis folgendes Kommando aus:

docker-compose up -d

Automatisierter Start des Containers mit Ansible

Um den Container durch Ansible automatisch zu starten, muss folgender Task ans Ende der main.yml angehängt werden:

- name: Run docker-compose up -d
  shell: docker-compose up -d
  args:
    chdir: /opt/docker/jenkins/ 

Ich führe das Playbook erneut aus:

Ausführen des Ansible Playbooks
Ausführen des Ansible Playbooks

Auf dem Server prüfe ich die Log-Ausgaben des neuen Containers:

cd /opt/docker/jenkins
docker-compose logs -f

Das Log zeigt an, dass der Container erfolgreich erstellt wurde:

Log-Ausgabe des Jenkins-Containers
Log-Ausgabe des Jenkins-Containers

Die Jenkins-Installation steht nun auf Port 8080 des Servers zur Verfügung, und zwar – wie gewünscht – nur auf localhost, sodass dieser nicht offen aus dem Internet erreichbar ist. Im folgenden Schritt stelle ich drei Optionen vor, um von außerhalb auf die Installation zuzugreifen.

Schritt 4: Jenkins-Installation erreichbar machen

Variante a: Zugriff auf den Container-Port über einen SSH-Tunnel

Die einfachste und sicherste Variante auf unseren Server zuzugreifen ist über einen SSH-Tunnel. Serverseitig muss dazu in der /etc/ssh/sshd_config-Datei die Option AllowTcpForwarding yes gesetzt sein.

Je nachdem, ob ihr Windows oder Linux verwendet, öffnet ihr den Tunnel wie folgt:

Windows:

In eurer Putty-Session wählt ihr links Connection/SSH/Tunnels aus und gebt dann rechts einen lokalen Port ein (im Beispiel: 8081) und das Ziel aus Sicht des Servers (im Beispiel: localhost:8080) und klickt auf “Add”:

SSH-Tunnel auf Jenkins-Port 8080 des Servers
SSH-Tunnel auf Jenkins-Port 8080 des Servers

Falls ihr schon eine SSH-Verbindung offen habt, müsst ihr diese nun schließen und eine neue öffnen, um den Tunnel aufzubauen. Über http://localhost:8081 könnt ihr dann auf die Jenkins-Installation zugreifen:

Jenkins Setup Wizard
Jenkins Setup Wizard

Linux:

Von Linux aus stellt ihr über das ssh-Kommando eine Verbindung her und gebt dabei folgenden Parameter mit an:

-L <lokaler Port>:<Host-Name aus Sicht des Servers>:<Port des Docker Containers>

Analog zur oben beschriebenen Putty-Konfiguration wäre das Kommando:

ssh -L 8081:localhost:8080 [email protected]

Hierdurch wird ein Tunnel von meinem lokalen Port 8081 zum Port 8080 des Servers hergestellt (“localhost” aus Sicht des Servers) und meine Jenkins-Installation ist wiederum unter http://localhost:8081 erreichbar.

Variante b: Öffnen des Jenkins-Ports des Servers

Über die docker-compose-Datei haben wir auf dem Server die Adresse 127.0.0.1:8080 auf den Docker-Container gemappt. Wenn wir an dieser Stelle die IP-Adresse 127.0.0.1 weglassen, wird stattdessen auf 0.0.0.0, d. h. auf alle IP-Adressen des Servers, einschließlich der externen gebunden. Da Docker alle gemappten Adressen automatisch auch in der Firewall öffnet, wäre unser Jenkins offen aus dem Internet erreichbar. Auch wenn Jenkins durch eine User-Authentifizierung abgesichert ist, ist dies unter Umständen nicht gewünscht. Außerdem ist die Verbindung unverschlüsselt.

Variante c: Anlegen einer (Sub-)Domain

Die dritte Variante wäre das Anlegen einer (Sub-)Domain, wie z. B. jenkins.happycoders.eu, verbunden mit der Konfiguration eines virtuellen Hosts in haproxy, der den eingehenden Traffic an localhost:8080 (also den Jenkins-Docker-Container) weiterleitet. Dieser Schritt ist aufwändig, insbesondere die Einrichtung eines SSL-Zertifikats, das bei einer solchen Lösung nicht fehlen sollte. Auch bei dieser Variante ist die Jenkins-Installation über das Internet erreichbar, es sei denn ihr konfiguriert die Firewall so, dass die IP-Adresse, an die der virtuelle Host gebunden ist, nur von festgelegten IP-Adressen aus erreichbar ist. Das ist sinnvoll, wenn ihr nur von einem eingeschränkten Kreis von IP-Adressen, z. B. eurem Büro (sofern dies eine feste IP-Adresse hat) auf den Server zugreift.

Die Konfiguration erfolgt analog zur Konfiguration der HappyCoders.eu-Webseite, die ich in meinem Ansible-Tutorial zum Setup von HAProxy und einem HTTPS-Zertifikats von Let’s Encrypt Schritt für Schritt beschrieben habe. Dies hier im Detail zu beschreiben würde den Rahmen dieses Artikels sprengen, denn hier soll es ja in erster Linie um Jenkins gehen – nicht um Ansible, haproxy oder Let’s Encrypt.

Schritt 5: Abschließen der Installation

Zur Fertigstellung der Installation werdet ihr aufgefordert den Inhalt der Datei /var/jenkins_home/secrets/initialAdminPassword in das angezeigte Formular einzutragen. Das Passwort haben wir auch schon in der Log-Ausgabe weiter oben gesehen und können es von dort kopieren.

Alternativ könnt ihr es natürlich auch aus der angegebenen Datei auslesen. Der Dateipfad existiert so allerdings nicht auf dem Host, sondern im Docker-Container. Es gibt zwei Wege den Inhalt der Datei anzuzeigen:

Variante 1: Auslesen der Passwort-Datei mittels docker exec:

Mit docker exec können wir Kommandos auf dem Container ausführen, also auch cat, um den Inhalt einer Datei anzuzeigen:

docker exec -it <Container ID> cat /var/jenkins_home/secrets/initialAdminPassword

Variante 2: Auslesen der Passwort-Datei aus dem gemappten Volume

Da wir in der docker-compose.yml das Verzeichnis /var/jenkins_home/ als Volume definiert haben, ist es auf ein Verzeichnis des Hosts gemappt. Welches das ist, finden wir über docker inspect heraus:

docker inspect -f '{{ .Mounts }}' <Container ID>

Auf meinem Server erscheint folgende Ausgabe:

[{volume jenkins_jenkins_home /var/lib/docker/volumes/jenkins_jenkins_home/_data /var/jenkins_home local rw true }]

Das initiale Admin-Passwort finden wir somit auch über folgendes Kommando (wir müssen hier sudo verwenden, da der reguläre User keinen Zugriff auf das Docker-Volume hat):

sudo cat /var/lib/docker/volumes/jenkins_jenkins_home/_data/secrets/initialAdminPassword

Nach der Eingabe des Admin-Passworts in den Webbrowser müsst ihr euch für eine initiale Plugin-Auswahl entscheiden. Ich wähle hier die empfohlenen Plugins. Als nächstes legt ihr den initialen Admin-User an. Später könnt ihr Jenkins auch mit einem LDAP-Server verbinden. Die Jenkins-URL könnt ihr auf http://localhost:8081/ lassen. Falls ihr später die Art des Zugriffs auf den Server ändert, könnt ihr die URL jederzeit unter Manage Jenkins → Configure System → Jenkins Location anpassen.

Basis-Konfiguration

Installation von Plugins

Nun müssen wir ein paar benötigte Plugins installieren. Dazu klickt ihr in der Navigation links auf Manage Jenkins → Manage Plugins und wählt folgende Plugins zur Installation aus (ihr könnt dazu über die Filter-Funktion nach „authorize“, „git“, „groovy“, „dsl“ und „maven“ suchen):

  • Authorize Project (wird für das Ausführen von aus einem Git-Repository geladenen Groovy-Code benötigt)
  • Git Parameter (ermöglicht die Auswahl eines Branches)
  • Groovy (erlaubt es uns Groovy-Code innerhalb eines Jobs zu verwenden)
  • Job DSL (ermöglicht das Erstellen von Jobs über eine DSL; mehr dazu im zweiten Teil der Artikelserie)
  • Maven Integration (stellt den Job-Typ „Maven“ zur Verfügung)

Setup von Maven

Der Jenkins-Docker-Container enthält noch keine Maven-Installation. Glücklickerweise kann Jenkins Maven eigenständig herunterladen und installieren. Dazu müsst ihr im Hauptmenü Manage Jenkins → Global Tool Configuration auswählen. Dann scrollt ihr zu „Maven“ herunter, klickt auf „Add Maven“, und gebt als Namen „Latest“ ein. Das Häkchen bei „Install automatically“ ist standardmäßig gesetzt und unter Version wird die aktuelle Version angezeigt. Mit einem Klick auf „Save“ speichert ihr die Änderungen.

Installation von Maven auf dem Jenkins-Server
Installation von Maven auf dem Jenkins-Server

Setup von Git

Nun müssen wir noch einen Username für Git-Commits durch Jenkins festlegen. Dies geschieht unter Manage Jenkins → Configure System, im Abschnitt „Git plugin“. Ich trage hier „Jenkins“ als User mit der E-Mail-Adresse „[email protected]“ ein:

Konfiguration des Usernamens für Git-Commits
Konfiguration des Usernamens für Git-Commits

Für den Zugriff auf unser Git-Repository per SSH müssen wir ein Key-Paar erstellen. Jenkins bietet die Möglichkeit im Hauptmenü unter Credentials Key-Paare zu hinterlegen. Allerdings können diese Key-Paare nur für das Clonen von Repositories durch das Git-Plugin verwendet werden. Um Änderungen ins Repository zu pushen (was für den Release-Job nötig sein wird), werden wir das Maven Goal scm:checkin verwenden. Dieses kann nicht auf die in Jenkins hinterlegten Key-Paare zugreifen, sondern verwendet die im Jenkins-Container im Home-Verzeichnis des jenkins-Users liegenden Keys. Auf diese kann wiederum auch das Git-Plugin zugreifen, sodass dieses eine Key-Paar ausreicht. Wir erstellen es, in dem wir im laufenden Jenkins-Docker-Container ssh-keygen aufrufen:

docker exec -it <Container ID> /bin/bash -c "ssh-keygen -t ecdsa -b 521 \
     -C <Kommentar, z. B. '[email protected]<Server Name>'> \
     -N '' \
     -f ~/.ssh/id_ecdsa"

docker exec -it <Container ID> /bin/bash -c "cat ~/.ssh/id_ecdsa.pub"

Auf meinem Server sieht das wie folgt aus:

Key-Paar-Generierung mit ssh-keygen im Jenkins-Docker-Container
Key-Paar-Generierung mit ssh-keygen im Jenkins-Docker-Container

Den öffentlichen Schlüssel hinterlege ich in meinem Git-Repository (auf GitLab ist die URL dafür https://gitlab.com/profile/keys):

Eintragen des öffentlichen SSH-Keys in GitLab
Eintragen des öffentlichen SSH-Keys in GitLab

Manuelle Jenkins-Job-Konfiguration für Maven-Projekte

Ich zeige euch im Folgenden wie ihr für zwei voneinander abhängige Maven-Projekte je einen Build- und einen Release-Job erstellt. Die Beispiel-Maven-Projekte (eine Demo-Applikation und eine von dieser verwendete Demo-Library) liegen in folgendem Git-Monorepo: https://gitlab.com/SvenWoltmann/jenkins-tutorial-demo

Warum ich ein Monorepo verwende, könnt ihr in meinem Artikel über die Vorteile von Monorepos nachlesen. Was ihr anders machen müsst, um einen Job für ein Multirepo zu erstellen, hebe ich an den entsprechenden Stellen hervor.

Jenkins Build-Job

Ein Build-Job läuft üblicherweise nach jedem Commit im Source-Code-Management; er lädt den Quellcode aus dem Source-Code-Management-Tool, baut das Projekt und führt alle Unit- und Integration-Tests aus. Der Build-Job erstellt dabei SNAPSHOT-Artefakte (z. B. JAR-, WAR- oder EAR-Dateien) und lädt diese in ein Artefakt-Repository (ein zentraler Speicherplatz für die erstellten Artefakte, wie z. B. https://mvnrepository.com/) hoch.

Die Build-Jobs für die zwei Projekte im oben verlinkten Git-Repository erstelle ich nun wie folgt:

Als erstes klicke ich links oben auf „New Item“:

Jenkins Build-Job anlegen - Schritt 1
Jenkins Build-Job anlegen – Schritt 1

Auf der folgenden Seite gebe ich einen Namen ein, wähle „Maven project“ aus (diese Option wird durch das Maven Integration-Plugin zur Verfügung gestellt) und klicke auf „OK“:

Jenkins Build-Job anlegen - Schritt 2
Jenkins Build-Job anlegen – Schritt 2

Im „General“-Tab gebe ich eine kurze Beschreibung ein, aktiviere „Discard old builds“ und gebe als „Max # of builds to keep“ 5 an. Builds benötigen teilweise eine Menge Speicherplatz, und mit dieser Einstellung bewirke ich, dass nur die letzten fünf Builds aufgehoben werden.

Jenkins Build-Job anlegen - Schritt 3
Jenkins Build-Job anlegen – Schritt 3

Immer noch auf dem „General“-Tab aktiviere ich „This project is parameterized“, klicke auf „Add Parameter“ und dann auf „Git Parameter“:

Jenkins Build-Job anlegen - Schritt 4
Jenkins Build-Job anlegen – Schritt 4

In dem neu erscheinenden Feld gebe ich als Name „Branch“ ein, dazu eine Beschreibung, als Parameter Type wähle ich ebenfalls „Branch“ aus und als default trage ich „origin/master“ ein. Diese Einstellung ermöglicht es mir einen bestimmten Branch des Projekts zu bauen. Lasse ich den vorangegangenen und diesen Schritt weg, wird immer der Master-Branch gebaut.

Jenkins Build-Job anlegen - Schritt 5
Jenkins Build-Job anlegen – Schritt 5

Ich scrolle weiter zu „Source Code Management“ und wähle „Git“ aus. Als Repository-URL hinterlege ich die Clone-URL meines Repositories, „[email protected]:SvenWoltmann/jenkins-tutorial-demo.git“. Wenn ihr keinen GitLab-Account habt oder es Probleme mit der Authentifizierung gibt, könnt ihr auch die HTTPS-URL „https://gitlab.com/SvenWoltmann/jenkins-tutorial-demo.git“ eintragen, die ein Clonen ohne Authentifizierung ermöglicht.

Unter „Branch Specifier“ gebe ich „$Branch“ an, dies wird zur Build-Zeit durch den in den vorherigen zwei Schritten definierten Branch-Parameter ersetzt.

Da ich nur ein Teilprojekt meines Git-Monorepos auschecken möchte, klicke ich bei „Additional Behaviours“ auf „Add“ und wähle „Sparse Checkout paths“ aus. (Dieser letzte Schritt kann bei einem Multirepo übersprungen werden.)

Jenkins Build-Job anlegen - Schritt 6
Jenkins Build-Job anlegen – Schritt 6

Unter „Sparse Checkout paths / Path“ trage ich den Pfad des zu bauenden Projekts, „library1/“ ein. Ich klicke noch einmal auf „Add“ und auf „Polling ignored commits in certain paths“. (Auch diese Schritte sind bei einem Multirepo nicht nötig.)

Jenkins Build-Job anlegen - Schritt 7
Jenkins Build-Job anlegen – Schritt 7

Unter „Polling ignored commits in certain paths / Included Regions“ trage ich „library1/.*“ ein. Hiermit bewirke ich, dass Jenkins das Projekt nur dann automatisch baut, wenn sich auch etwas an diesem Projekt ändert. Ohne diese Einstellung würde Jenkins das Projekt immer dann neu bauen, wenn sich in einem beliebigen Teilprojekt des Monorepos etwas ändern würde. (Auch dieser Schritt fällt bei einem Multirepo weg.)

Jenkins Build-Job anlegen - Schritt 8
Jenkins Build-Job anlegen – Schritt 8

In der Kategorie „Build Triggers“ müsst ihr der Option „Build whenever a SNAPSHOT dependency is built“ besondere Aufmerksamkeit schenken. Diese Option ist ansich sinnvoll, führt aber bei großen Dependency-Graphen dazu, dass ein kleiner Commit ggf. Hunderte Build-Jobs triggert. Da es in meiner Demo lediglich zwei Projekte gibt, lasse ich die Option aktiviert.

Außerdem aktiviere ich die Option „Poll SCM“ und trage unter „Schedule“ „H/15 * * * *“ ein, was dazu führt, dass Jenkins jede Viertelstunde in GitLab nachschaut, ob es neue Commits für das Projekt gibt und das Projekt neu baut, wenn das der Fall ist.

Jenkins Build-Job anlegen - Schritt 9
Jenkins Build-Job anlegen – Schritt 9

Die Kategorien „Build Environment“ und „Pre Steps“ können wir überspringen. Unter „Build“ muss der „Root POM“-Pfad angepasst werden. Dieser ist standardmäßig „pom.xml“, doch in unserem Monorepo liegen die pom.xml-Dateien in den Projekt-Verzeichnissen. Daher ändere ich den Eintrag auf „library1/pom.xml“. (Bei einem Multirepo mit der pom.xml im Root-Verzeichnis lasst ihr diesen Eintrag unverändert.)

Unter „Goals and options“ trage ich „clean install“ ein, sodass Jenkins das Projekt letztendlich durch einen Aufruf von mvn clean install bauen und im lokalen Maven-Repository ablegen wird.

Jenkins Build-Job anlegen - Schritt 10
Jenkins Build-Job anlegen – Schritt 10

Im Anschluss an den Build könnten wir die erstellte JAR noch in ein Artefakt-Repository, wie z. B. den Nexus Repository Manager hochladen. Dazu würde man unter „Post-build Actions“ auf „Add post-build action“ klicken, „Deploy artifacts to Maven repository“ auswählen, und die Repository-URL eintragen.

Da ich kein Artefakt-Repository installiert habe und mir die JAR im lokalen Maven-Repository des Jenkins-Servers ausreicht, überspringe ich diesen Schritt und klicke zum Abschluss der Konfiguration auf „Save“. Wir landen auf der Seite des soeben angelegten Jobs. Um den Job auszuführen, klicke ich auf „Build with Parameters“:

Jenkins Build-Job ausführen - Schritt 1
Jenkins Build-Job ausführen – Schritt 1

Es erscheint das folgende Formular, in dem wir den Git-Branch auswählen können. Momentan gibt es nur den Master-Branch, also wähle ich diesen aus und klicke zum Fortfahren auf „Build“:

Jenkins Build-Job ausführen - Schritt 2
Jenkins Build-Job ausführen – Schritt 2

Der Job läuft nun, wie man am Fortschrittsbalken in der Build-History links unten sieht:

Jenkins Build-Job ausführen - Schritt 3
Jenkins Build-Job ausführen – Schritt 3

Nach kurzer Zeit ist der Job abgeschlossen. Der Fortschrittsbalken verschwindet und die zuvor graue Kugel wird blau. Falls ihr grün vorzieht (und ihr im Team keine Personen mit Rot-Grün-Sehschwäche habt), hilft euch das Plugin „Green Balls“ weiter.

Jenkins Build-Job ausführen - Schritt 4
Jenkins Build-Job ausführen – Schritt 4

Ein Klick auf die „#1“ führt euch zur Konsolen-Ausgabe des Jobs:

Jenkins Build-Job ausführen - Schritt 5
Jenkins Build-Job ausführen – Schritt 5

Am Ende der Konsolen-Ausgabe sehen wir, dass der Job 7,5 Sekunden benötigt hat und erfolgreich abgeschlossen wurde:

Jenkins Build-Job ausführen - Schritt 6
Jenkins Build-Job ausführen – Schritt 6

Analog könnt ihr den Build-Job für das „application1“-Projekt des Monorepos konfigurieren.

Im folgenden Abschnitt zeige ich euch, wie ihr den Build-Job zu einem Release-Job erweitert.

Jenkins Release-Job

Was unterscheidet einen Release-Job von einem Build-Job? Ein Release-Job wird manuell angestoßen. Er setzt die Versionsnummer des Projekts auf eine Release-Version (also ohne „SNAPSHOT“), baut das Projekt, lädt es ggf. in ein Artefakt-Repository hoch und setzt abschließend die Versionsnummer auf die nächste SNAPSHOT-Version.

Zunächst legen wir den Release-Job als Kopie des Build-Jobs an. Dazu klicken wir erneut links oben auf „New Item“. Als Namen trage ich „Jenkins Tutorial Demo – Library 1 – Release“ ein. Einen Projekt-Typ brauchen wir nicht auszuwählen – stattdessen tragen wir ganz unten bei „Copy from“ den Namen des Jobs, den wir kopieren wollen, ein – also „Jenkins Tutorial Demo – Library 1“. Abschließend klicken wir auf „OK“.

Jenkins Release-Job anlegen - Schritt 1
Jenkins Release-Job anlegen – Schritt 1

Wir landen wieder im Konfigurationsformular, wo ich zunächst die Beschreibung anpasse („Release job“ anstatt „Build job“):

Jenkins Release-Job anlegen - Schritt 2
Jenkins Release-Job anlegen – Schritt 2

Ich scrolle hinunter zu „This project is parameterized“ und entferne den Git Parameter „Branch“ über das Kreuz rechts oben, da ich Releases ausschließlich vom Master-Branch erstellen möchte.

Stattdessen füge ich über „Add Parameter“ zwei „String Parameter“ hinzu:

  • „releaseVersion“: hiermit kann ich die Release-Version setzen. Lasse ich das Feld bei der Ausführung des Job frei, wird standardmäßig die aktuelle SNAPSHOT-Version, ohne den „-SNAPSHOT“-Suffix, verwendet.
  • „nextSnapshotVersion“: hiermit kann ich die Snapshot-Version nach dem Release festlegen. Lasse ich dieses Feld frei, wird die Minor-Stelle der Release-Version um eins erhöht.
Jenkins Release-Job anlegen - Schritt 3
Jenkins Release-Job anlegen – Schritt 3

Im Abschnitt „Source Code Management“ ändern wir den Branch Specifier auf „origin/master“. Den „Branch“-Parameter hatten wir im vorangegangenen Schritt entfernt, da wir Releases nur vom Master-Branch aus bauen wollen.

Jenkins Release-Job anlegen - Schritt 4
Jenkins Release-Job anlegen – Schritt 4

Im selben Abschnitt muss unter „Additional Behaviours“ eine weitere Aktion eingetragen werden, und zwar „Check out to specific local branch“ mit dem Branch-Namen „master“. Andernfalls wird später das Maven Goal scm:checkin mit der Fehlermeldung „Detecting the current branch failed: fatal: ref HEAD is not a symbolic ref“ fehlschlagen.

Jenkins Release-Job anlegen - Schritt 5
Jenkins Release-Job anlegen – Schritt 5

Im Abschnitt „Build Triggers“ deaktiviere ich jegliche Trigger, da ich meinen Release-Job ausschließlich manuell starten will:

Jenkins Release-Job anlegen - Schritt 6
Jenkins Release-Job anlegen – Schritt 6

Im Abschnitt „Pre Steps“ füge ich über den Button „Add pre-build step“ einen Schritt vom Typ „Execute system Groovy script“ hinzu:

Jenkins Release-Job anlegen - Schritt 7
Jenkins Release-Job anlegen – Schritt 7

Mit diesem Script fülle ich die Variablen „releaseVersion“ und „nextSnapshotVersion“, falls diese bei der Ausführung des Jobs leer gelassen wurden:

Jenkins Release-Job anlegen - Schritt 8
Jenkins Release-Job anlegen – Schritt 8

Hier ist der Groovy-Code, ihr könnt ihn gerne kopieren (achtet darauf für eigene Projekte den pomPath anzupassen):

import hudson.model.StringParameterValue
import hudson.model.ParametersAction

def env = build.getEnvironment(listener)
String releaseVersion = env.get('releaseVersion')
String nextSnapshotVersion = env.get('nextSnapshotVersion')

if (!releaseVersion) {
    String pomPath = build.workspace.toString() + '/library1/pom.xml';
    def pom = new XmlSlurper().parse(new File(pomPath))
    releaseVersion = pom.version.toString().replace('-SNAPSHOT', '')
    println "releaseVersion (calculated) = $releaseVersion"
    def param = new StringParameterValue('releaseVersion', releaseVersion)
    build.replaceAction(new ParametersAction(param))
}

if (!nextSnapshotVersion) {
    def tokens = releaseVersion.split('\\.')
    nextSnapshotVersion = tokens[0] + '.' + (Integer.parseInt(tokens[1]) + 1) + '-SNAPSHOT'
    println "nextSnapshotVersion (calculated) = $nextSnapshotVersion"
    def param1 = new StringParameterValue('releaseVersion', releaseVersion)
    def param2 = new StringParameterValue('nextSnapshotVersion', nextSnapshotVersion)
    build.replaceAction(new ParametersAction(param1, param2))
}

Ich füge noch drei weitere Pre Steps hinzu. Mit dem ersten Pre Step setze ich die Versionsnummer in der pom.xml auf die gewünschte Release-Version. Ich füge den Pre Step wie folgt hinzu (releaseVersion ist dabei die als String-Parameter eingetragene bzw. im Groovy-Skript gesetzte Variable):

  • Typ: „Invoke top-level Maven targets“
  • Maven Version: „Latest“
  • Goals:
    versions:set -DnewVersion=${releaseVersion} -DgenerateBackupPoms=false
  • Advanced → POM: „library1/pom.xml“

Der nächste Pre Step setzt alle Snapshot-Dependencies auf Release-Versionen. Falls es von einer Dependency noch keine Release-Version gibt, wird der Build im nächsten Schritt mit einer entsprechenden Fehlermeldung abbrechen. Dadurch stelle ich sicher, dass mein Release nur von anderen Releases abhängt und nicht von Snapshots:

  • Typ: „Invoke top-level Maven targets“
  • Maven Version: „Latest“
  • Goals:
    versions:use-releases -DgenerateBackupPoms=false -DprocessDependencyManagement=true
  • Advanced → POM: „library1/pom.xml“

Der letzte Pre Step stellt sicher, dass die pom.xml keine Referenzen auf Snapshot-Versionen mehr enthält. Dies ist notwendig, da der vorherige Schritt u. a. keine Snapshot-Referenzen auf Parent-Projekte anpasst. Falls wir vergessen haben den Parent zu releasen, wird der Build somit an dieser Stelle abbrechen:

  • Typ: „Execute shell“
  • Command:
    if find library1/ -name 'pom.xml' | xargs grep -n "SNAPSHOT"; then
      echo 'SNAPSHOT versions not allowed in a release'
      exit 1
    fi

Achtung: im folgenden Screenshot sieht man die „POM“-Felder nicht. Diese befinden sich hinter den „Advanced…“-Buttons. Für den Screenshot habe ich die „Advanced“-Felder geschlossen, da diese neben dem „POM“-Feld auch noch eine Reihe andere Eingabefelder enthalten und ziemlich viel Platz in Anspruch nehmen.

Jenkins Release-Job anlegen - Schritt 9
Jenkins Release-Job anlegen – Schritt 9

Am Build-Schritt müssen wir nichts ändern. Im darauf folgenden Abschnitt „Post Steps“ wähle ich ganz oben „Run only if build succeeds“ aus, sodass im Falle eines Build-Fehlers der Release-Job an dieser Stelle abgebrochen wird. Ich füge die folgenden vier „Post Steps“ hinzu, um 1. die Release-Version in Git zu committen, 2. diese zu taggen, 3. die Version auf die nächste Snapshot-Version hochzusetzen und 4. auch diese in Git zu committen.

Alle vier Post Steps sind vom selben Typ, benötigen dieselbe Maven-Version und arbeiten auf derselben pom.xml:

  • Typ: „Invoke top-level Maven targets“
  • Maven Version: „Latest“
  • Advanced → POM: „library1/pom.xml“

Im folgenden liste ich zu jedem Schritt nur noch die jeweiligen Maven-Goals auf:

Erster Post Step – Commit der Release-Version in Git (releaseVersion ist die als String-Parameter eingetragene bzw. im Groovy-Skript gesetzte Variable):

scm:checkin -Dmessage="Release version ${project.artifactId}:${releaseVersion}" -DdeveloperConnectionUrl=scm:git:[email protected]:SvenWoltmann/jenkins-tutorial-demo.git

Zweiter Post Step – Taggen des Releases in Git (project.artifactId wird aus der pom.xml ausgelesen):

scm:tag -Dtag=${project.artifactId}-${releaseVersion} -DdeveloperConnectionUrl=scm:git:[email protected]:SvenWoltmann/jenkins-tutorial-demo.git

Dritter Post Step – Hochsetzen der Version auf die nächste Snapshot-Version (nextSnapshotVersion ist die als String-Parameter eingetragene bzw. im Groovy-Skript gesetzte Variable):

versions:set -DnewVersion=${nextSnapshotVersion} -DgenerateBackupPoms=false

Vierter Post Step – Commit der Snapshot-Version in Git:

scm:checkin -Dmessage="Switch to next snapshot version: ${project.artifactId}:${nextSnapshotVersion}" -DdeveloperConnectionUrl=scm:git:[email protected]:SvenWoltmann/jenkins-tutorial-demo.git

Auch im folgenden Screenshot habe ich die „POM“-Felder der Übersicht halber wieder ausgeblendet:

Jenkins Release-Job anlegen - Schritt 10
Jenkins Release-Job anlegen – Schritt 10

Damit ist auch der Release-Job fertiggestellt, ich speichere ihn mit einem Klick auf „Save“. Ich lande auf der Job-Seite und starte den Job mit „Build with Parameters“:

Jenkins Release-Job ausführen - Schritt 1
Jenkins Release-Job ausführen – Schritt 1

Auf der folgenden Seite kann ich die Release-Version und die nächste Snapshot-Version eintragen. Ich lasse die Felder jedoch leer, vertraue auf meinen in Schritt 8 geschriebenen Groovy-Code und klicke auf „Build“:

Jenkins Release-Job ausführen - Schritt 2
Jenkins Release-Job ausführen – Schritt 2

Während der Release-Job läuft, können wir uns die Ausgaben durch einen Klick auf „#1“ in der Build History anschauen:

Jenkins Release-Job ausführen - Schritt 3
Jenkins Release-Job ausführen – Schritt 3

Der Release-Job läuft in nur 3,2 Sekunden erfolgreich durch (der erste Build-Job hat länger gedauert, weil Maven zunächst noch Dependencies runterladen musste):

Jenkins Release-Job ausführen - Schritt 4
Jenkins Release-Job ausführen – Schritt 4

Um zu überprüfen, ob der Release-Job korrekt gearbeitet hat, schaue ich in die Commit-History meines GitLab-Projekts unter https://gitlab.com/SvenWoltmann/jenkins-tutorial-demo/commits/master und sehe dort die erfolgreich durchgeführten Release-Commits:

Jenkins Release-Job ausführen - Schritt 5
Jenkins Release-Job ausführen – Schritt 5

Somit ist auch der Release-Job abgeschlossen und wir können analog dazu den Release-Job für das „application1“-Projekt konfigurieren.

Fazit und Ausblick

In diesem ersten Teil des Jenkins-Tutorials habe ich euch gezeigt, wie ihr Jenkins über Ansible installiert und initial konfiguriert und wir ihr für Maven-Projekte in einem Git-Monorepo – vorerst manuell – Build- und Release-Jobs erstellt.

Das ist schon ein ziemlich hoher manueller Aufwand. Stellt euch vor, ihr müsstest das für zehn oder 100 Projekte machen. Oder stellt euch vor, ihr wollt in zehn oder 100 Projekten einen einzelnen Schritt im Build- oder Release-Prozess ändern. Nicht nur, dass ihr stundenlang beschäftigt wärt – dieser manuelle Prozess ist zudem überaus fehleranfällig.

Deshalb werde ich euch im zweiten Teil der Artikelserie zeigen, wir ihr die gleichen Jobs mit Hilfe der Jenkins Job DSL als Programmcode schreiben und diesen in Jenkins importierten könnt. Im dritten Teil zeige ich euch, wie ihr einen sogenannten „Seed Job“ schreibt, der für neue Projekte im Monorepo vollautomatisch neue Build- und Release-Jobs generiert, bzw. bei Änderungen des Job-DSL-Codes die bestehenden Jobs automatisch anpasst.

Wenn dir dieser Artikel geholfen hat, freue ich mich, wenn Du ihn über einen der folgenden Share-Buttons teilen würdest.

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.