Jenkins Multi-Branch-Jobs as Code

von am 24.02.2017
0
Coding at Zühlke

In meinem letzten Blog-Beitrag habe ich ausgeführt, wie ich die neuen Jenkins DSL-Plugins genutzt habe, um meine Job-Spezifikation in Groovy-Scripte zu migrieren und zusammen mit dem Quellcode im Git-Repo abzulegen. Mit diesem Beitrag möchte ich zeigen, wie ich nun, auf dem letzten Stand aufbauend, das Potential für Multi-Branch-Projekte genutzt habe.

Notwendige Anpassungen für Multi-Branch-Projekte

Bisher war Handarbeit gefragt, um sämtliche Jobs einer Build-Kette über die Jenkins-UI zu duplizieren und an den neuen Branch anzupassen. Ihr könnt Euch sicher den Wartungsspaß vorstellen, wenn Wartungsarbeiten an, für eine handvoll Branches, duplizierten Jobs anstehen. Mit der Umstellung der Job-Spezifikation auf Job DSL liegt nun die Konfiguration komplett unter Revisionskontrolle. Anpassungen an den Jobs für mehrere Branches lassen sich also per Merge auf Textebene erledigen, statt mühsahmer Klickarbeit.

Einige Aspekte gilt es aber noch zu verfeinern, um das ganze Potential der scriptbasierten Job-Spezifikation für Multi-Branch-Projekte langfristig zu sichern:

  • Seed-Jobs für mehrere Branches
  • Branch-Parametrierung der Jobs
  • Eindeutige, branchbasierte Job-Namen

Automatischer Multi-Branch-Seed

Im ersten Blog-Beitrag habe ich einen Seed-Job für einen dedizierten Branch manuell angelegt. Sobald ich mit mehreren Branches arbeite, müsste ich den Seed-Job pro Branch manuell erstellen. Das ist durchaus möglich, aber lässt sich auch automatisiert erledigen. Das Pipeline-Plugin stellt neben dem einfachen Pipeline-Job, wie ich ihn bereits für die Commit-Pipeline verwendet habe, auch ein Multi-Branch-Pipeline-Job zur Verfügung. Letzterer durchsucht alle Branches (per RegEx einschränkbar) des referenzierten Git-Repos. Für jeden Branch mit einem Jenkinsfile auf oberster Verzeichnisebene wird für selbigen automatisch ein Pipeline-Job erzeugt.

Für Projekte mit einem vergleichsweise einfachen Build-Job kann das Jenkinsfile direkt verwendet werden, um die Build-Pipeline zu beschreiben. In meinem Fall verwende ich das Jenkinsfile lediglich als Seed-Job. Die Pipeline bekommt also die Aufgabe den jeweiligen Branch auszuchecken und alle Groovy-Job-Scripte anzuwenden. Die Pipeline DSL stellt dazu das Kommando jobDsl targets: 'jobs/**/*.groovy' bereit.

Git-Konfiguration aus dem Seed-Job übernehmen

Die Git-Referenz in den einzelnen Jobs müssen nun natürlich auf den entsprechenden Branch angepasst werden. Da ich nur äußerst ungerne die Job-Scripte für jeden Branch manuell anpassen möchte, habe ich nach einer Möglichkeit gesucht den Branch per Variable oder Parameter zu injezieren. Leider habe ich keine einfache Möglichkeit gefunden, Parameter aus der Seed-Pipeline an die Job-Scripte zu übergeben. Als Lösung habe ich daher die Seed-Pipeline um eine PrepareEnv-Stage erweitert. In dieser Stage nutze ich das Pipeline-Kommando writeFile file: 'lib/EnvHelper.groovy' um eine Groovy-Bibliotheksfunktion zu generieren, die ich in den Job-Scripten wieder importieren kann. Die Groovy-Klasse EnvHelper bekommt einfach eine Sammlung öffentlicher Eigenschaften, die die benötigten Konfigurationswerte transportieren, in meinem Fall ist das zunächst die Git-Konfiguration mit folgenden Variablen:

  • GIT_URL (= scm.getUserRemoteConfigs()[0].getUrl())
  • GIT_BRANCH (= scm.getBranches()[0])
  • GIT_CREDENTIALS (= scm.getUserRemoteConfigs()[0].getCredentialsId())

Die Werte extrahiere ich direkt aus der Git-Konfiguration des Seed-Jobs, sodass alle durch den Seed-Job erzeugten Jobs mit der selben Git-Konfiguration arbeiten, ohne die Daten manuell mehrfach pflegen zu müssen.

Stolperfalle Groovy-Sandbox

Sobald man dieses Pipeline-Script allerdings ausführt, stellt einem die Groovy-Sandbox zunächst ein Bein. In der Sandbox dürfen standardmäßig nur sehr eingeschränkte Funktionen der Jenkins-API verwendet werden. Bevor das Script also durchläuft, müssen alle weiteren Funktionsaufrufe manuell in der Jenkins-Konfiguration bestätigt werden. Dazu steht unter Jenkins verwalten der Menüpunkt  In-process Script Approval zur Verfügung. Alle blockierten Funktionsaufrufe werden hier aufgelistet und müssen einzeln bestätigt werden.

Leider erschwert mir meine Jenkins-Version hier die Arbeit. Ich muss das Script für jeden Funktionsaufruf einmal ausführen, um den Funktionsaufruf anschließend bestätigen zu können. Erst dann kann ich mit dem nächsten Aufruf fortfahren. Bei komplexeren Scripts kann das langwierig sein. Im Falle von Pipelines hilft hier die Möglichkeit das Script per Replay-Funktion zunächst auf ein Minimalbeispiel zu reduzieren, statt die komplette Pipeline auszuführen.

Läuft die Seed-Pipeline endlich durch, steht mir der generierte EnvHelper in den Job-Scripten zur Verfügung. Im Abschnitt git { scm { } } kann ich nun die bisher durch die Migration der alten Jobs fest hinterlegten Werte (url, credentials und branch) durch die Variablen im EnvHelper ersetzen. Ein zweiter Branch ist in Git schnell erzeugt und führt wie erwartet dazu, dass der Seed-Job nun für zwei Branches ausgeführt wird. Allerdings überschreibt der zweiter Branch die Jobs des ersten, da sich die Namen der Jobs nicht unterscheiden.

Eindeutige Job-Namen durch branchbasierte Ordnerstruktur

Bei der bisherigen, manuellen Duplizierung der Jobs haben ich typischerweise die Branchnamen als Präfix in die Jobnamen eingebracht, etwa Release_1.Firmware. In Projekten mit sehr vielen Branches führte das unweigerlich zu einer unübersichtlichen Job-Liste. Zu dieser Gelegenheit teste ich das Folders-Plugin. Dieses Plugin stellt sogenannte Folder Jobs bereit, sodass ich nun pro Branch einen Ordner erstellen und die einzelnen Jobs dort einsortieren kann. Dazu habe ich einen weiteres Job-Script angelegt, welches den Branch-Namen als Ordnerstuktur in Jenkins nachbildet:

import EnvHelper
				
				def p = ""
				EnvHelper.GIT_BRANCH.split('/').each {
				  if (p) {
				    p = p + '/' + it
				  } else {
				    p = it
				  }
				  folder(p)
				}

Ich kann somit Branches auch weiter kategorisieren, etwa releases/release_1 erzeugt einen Ordner releases und einen Unterordner release_1. Somit bleibt die oberste Job-Übersicht auf Jenkins auch bei vielen Branches übersichtlich. Damit die einzelnen Jobs nun auch in den korrekten Ordnern landen und nicht wie bisher auf der obersten Ebene, müssen alle Job-Scripte angepasst werden. Da ich den Branch-Namen exakt als Ordnerstruktur nachgebildet habe, genügt es nun, den Branch-Namen als Präfix voranzustellen. Dank des EnvHelpers geschieht dies einfach als job("${EnvHelper.GIT_BRANCH}/SmokeTest"). Der Job-Name muss in diesem Fall bewusst in " stehen, damit Groovy eine Variablenersetzung durchführt.

Job-Scripte sortiert anwenden

Leider beschwert sich Jenkins bei meinem ersten Versuch nun, dass die Ordner für die neuen Jobs nicht existieren. Jenkins scheint die Groovy-Scripte in alphabetischer Reihenfolge auszuwerten, sodass etwa commit.groovy den Commit-Pipeline-Job anzulegen versucht, bevor folders.groovy die Ordner erzeugt hat. Ich habe daher folders.groovy nach aaa_folders.groovy umbenannt, dann funktioniert das. Zunächst hatte ich es mit 00-folder.groovy versucht, den Dateinamen hat Jenkins jedoch als ungültig abgelehnt. Dateinamen dürfen scheinbar nicht mit Ziffern beginnen.

Nach den beschriebenen Erweiterungen bekommt nun jeder Branch automatisch alle spezifizierten Jobs, sobald der Branch publiziert wird. Das führt unweigerlich dazu, dass auch inaktive oder experimentelle Branches diese Jobs bekommen. Der Nachteil hält sich aber insoweit in Grenzen, dass ich beim Erstellen eines Branches unnötige Jobs entweder deaktivieren oder ganz entfernen muss. Dazu kann ich entweder das Groovy-Script entfernen, oder den Job als deactivated() spezifizieren (etwa um die Commit-Pipeline abzuschalten). Dieses bisschen mehr nachdenken müssen, nehme ich im Hinblick auf die Vorteile sehr gerne in Kauf.

Einen Kommentar hinterlassen

Ihre E-Mail-Adresse wird weder veröffentlicht noch weitergegeben. Pflichtfelder sind mit einem * markiert.