Build-System mit gemeinsamen Workspace in Hudson / Jenkins
Das Aufsetzen einer CI (continuous integration) Umgebung ist heutzutage keine Besonderheit mehr. Sehr häufig kommt dabei der CI-Server Hudson bzw. Jenkins zum Einsatz. In einem aktuellen Projekt habe ich bei der Umsetzung der Umgebung bzw. beim Aufbau des Build-Systems jedoch einige besondere Techniken eingesetzt.
Das Projekt besteht aus mehreren Modulen (eclipse Projekte) zwischen denen Abhängigkeiten bestehen. Bei der Ausführung der Build-Skripte (Ant) ist daher auf eine definierte Reihenfolge zu achten, da einige Module auf die Artefakte von anderen Modulen zurückgreifen. Neben den erstellten Artefakten der einzelnen Module werden weitere Artefakte (= Bibliotheken) verwendet. Die Bibliotheken sind einem Modul zugeordnet, werden aber auch von anderen Modulen referenziert.
Version 1 - Trennung nach Server und Client
In einem ersten Schritt habe ich die Erstellung aller Module der Applikation durch zwei Build-Skripte realisiert. Es gibt ein Skript für den Server und ein Skript für den Client. Da der Client auf die Schnittstellen des Servers zugreift, muss das Skript für den Client nach dem Skript für den Server ausgeführt werden.
Zu Beginn waren diese beiden Skripte relativ übersichtlich und konnte auch von jedem anderen Entwickler im Team schnell verstanden werden. Im Laufe der Zeit hat sich das jedoch geändert. Neben dem reinen Build kamen Funktionen wie das Signieren der Client JARs für den Start über Java WebStart und das Deployment des Server EARs auf verschiedene Application Server hinzu. Erweiterungen, wie z.B. das Erstellen einer Verzeichnisstruktur zur Übergabe der Artefakte an den Betrieb habe ich dann schon in „Satelliten“ Skripte ausgelagert. Diese Skripte verwendeten die Ergebnisse der beiden Haupt-Skripte und erledigten darauf aufbauend weitere Funktionen.
Zum Schluss war die Struktur der Skripte relativ unübersichtlich und nur scher wartbar.
Version 2 - Build pro Modul und kopieren der Artefakte
Für die zweite Version des Build-Systems habe ich für jedes Modul ein eigenes Build-Skript erstellt. Pro Build-Skript lege ich in Hudson / Jenkins einen eigenen Job (Projekt) an.
Die einzelnen Build-Skripte sind wie die eigentlichen Module voneinander abhängig. Jedes Build-Skript legt die von ihm erstellten Artefakte (JARs) in einem zentralen Artefakt-Verzeichnis ab. Andere Build-Skripte können so die Dateien in diesem (Artefakt) Verzeichnis referenzieren und müssen nichts von der internen Struktur der anderen Module wissen. Gemeinsam genutzte Bibliotheken werden im Lib-Verzeichnis eines Moduls abgelegt. Die anderen Module verweisen dann direkt auf das JAR im entsprechenden Verzeichnis eines anderen Moduls.
Standardmäßig verwendet jeder Job in Hudson / Jenkins einen eigenen Workspace. Zu Beginn jedes Jobs wird dieser Workspace mit den Dateien aus einem Quellcode-Verwaltungssystem (cvs, subversion, etc.) befüllt / initialisiert. Somit steht jedem Job ein definierter Workspace zur Verfügung. Problematisch an diesem Verfahren ist jedoch, dass ein Job nicht auf die Ergebnisse / Artefakte eines anderen Jobs zugreifen kann.
Copy Artefact Plugin
Über das Copy Artifact Plugin kann dieses Problem gelöst werden. Die Artefakte eines Jobs können nach dem Ausführen des Build-Skripts von Hudson / Jenkins archiviert werden. Hierzu steht die Post-Build-Action „Artefakte archivieren“ zur Verfügung. Pro Job konfiguriere ich die JAR-Dateien, die vom Build-Skript erstellt worden sind.
Die so exportierten Artefakte werden über einen neuen Build-Schritt, der vom Copy Artifact Plugin zur Verfügung gestellt wird, in den Workspace des abhängigen Jobs kopiert. Werden die Artefakte mehrere Module benötigt, füge ich für jedes Modul einen eigenen Build-Schrit “Copy Artifacts” ein.
Build-Trigger
Um die einzelnen Jobs miteinander zu verbinden und von Hudson / Jenkins in der richtigen Reihenfolge ausführen zu lassen, wird in einer Post-Build-Action eines Jobs die Option „Weitere Projekte bauen“ verwendet. Ist diese Option gesetzt, werden nach einem erfolgreichen Build des aktuellen Jobs, alle im Wert der Option eingetragenen Jobs automatisch gestartet / getriggert. Die Abhängigkeiten / Reihenfolge der Module bilde ich so direkt auf die Vorgänger / Nachfolger Verweise der Jobs in Hudson / Jenkins ab.
Nachteil dieser Lösung war ein erhöhter Bedarf an Festplattenspeicher, der im aktuellen Projekt leider nicht so leicht erweitert werden kann. Auch die zunehmende Anzahl an Copy-Artifact Schritten vor dem eigentlichen Bau eines Jobs führte zu sehr unübersichtlichen Strukturen.
Version 3 - Jobs bauen gemeinsem auf einem Workspace
Damit nicht mehr für jeden Job der gesamte Quellcode ausgecheckt werden muss und die erstellten Artefakte mehrfach zwischen den Jobs kopiert werden, habe ich im jetzt aktuellen Ansatz die Jobs so konfiguriert, dass alle einen gemeinsamen Workspace verwenden.
Ich lege weiterhin für jedes Build-Skript einen eigenen Job in Hudson / Jenkins an. In den erweiterten Projekteinstellungen passe ich allerdings das Verzeichnis des Arbeitsbereichs an. In allen Jobs wird das gleiche Verzeichnis verwendet. Jeder Job findet somit die von ihm benötigten Artefakte im bekannten Artefakt-Verzeichnis – innerhalb des Workspace. Auch die von einem Build-Skript erstellten Artefakte werden dort abgelegt. Die Artefakte von Dritt-Herstellern können wie gehabt direkt aus den Lib-Verzeichnissen der unterschiedlichen Module referenziert werden.
“Root-Job” zur Festlegung der Arbeitsumgebungen
Ich schalte vor den ersten eigentlichen Job einen “Root-Job”. Dieser hat die Aufgabe den Workspace anzulegen bzw. zur Verfügung zu stellen. Auch konfiguriere ich diesen Job so, dass die gewünschte Version des Quellcodes (Head, Branch) ausgecheckt wird.
Folge-Job zur Abarbeitung unterschiedlicher Arbeitsschritte
In allen weiteren Jobs trage ich den Workspace dieses Root-Jobs als Verzeichnis des Arbeitsbereichs ein.
Nachteil dieser ersten Umsetzung ist, dass ich in allen nachfolgenden Jobs fest der Pfad des Root-Jobs eingetragen muss. Ändert sich dieser Verzeichnisname, weil ich z.B. den Namen des Root-Jobs ändere, muss ich somit in allen Jobs den Eintrag des Workspace-Verzeichnisses ändern.
Parameterized Trigger Plugin
In der endgültigen Umsetzung setze ich zwei weitere Features von Hudson / Jenkins ein.
Zum einen können Jobs parametrisiert werden. Beim Starten eines Jobs werden diesem ein oder mehrere Werte / Parameter übergeben. Es handelt sich dabei um Name / Wert Paare. Jeder Parameter wird auf eine Environment-Variable abgebildet. Somit kann innerhalb des Jobs auf diese Parameter in jedem Konfigurationswert zugegriffen werden. Im Build-Skript selbst besteht auch die Möglichkeit auf die Parameter / Environment-Variablen zuzugreifen.
In allen Jobs, die auf den gemeinsamen – und nicht auf einen eigenen Workspace – zugreifen sollen, setze ich als Wert für die Option „Verzeichnis des Arbeitsbereichs anpassen“ den Inhalt der Environment-Variablen WORKSPACE
.
Problematisch ist allerdings, dass die Verwendung von Parametern in einem Job dazu führt, dass vor der eigentlichen Ausführung der Wert der einzelnen Parameter vom Benutzer gesetzt werden muss. Für einen automatisch ablaufenden CI Prozess ist dass natürlich nicht realisierbar.
Hier kommt das zweite Feature, verbunden mit einem weiteren Plugin ins Spiel.
Auch wenn ein Job nicht explizit mit Parametern aufgerufen wird, werden einige Environment-Variablen gesetzt.
Darunter auch der oben aufgeführte WORKSPACE
, die den Pfad des Arbeitsverzeichnisses des Jobs enthält.
Ruft man nun einen Job mit einem Parameter auf, der den Namen einer vordefinierten Environment-Variable hat, wird der von Hudson / Jenkins vorgegebene Wert durch den Wert des Parameters überschrieben.
Mittels des Parameterized Trigger Plugin können bei der Ausführung eines Trigger – beim Aufruf eines abhängigen Jobs nach der Ausführung des übergeordneten Jobs – Parameter an den aufgerufenen Job übergeben werden.
Beim Aufruf des Triggers setze ich den Parameter WORKSPACE
mit dem Wert des aktuellen Arbeitsverzeichnisses des abgeschlossenen Jobs.
Somit erhält der nächste aufgerufene Job den Pfad zum Workspace von seinem Vorgänger übergeben.
Der Workspace wird nur einmal beim ersten Job der Kette festgelegt und an alle weiteren weitergegeben.
Mit diesem Mechanismus besteht jetzt auch die Möglichkeit nur den ersten Root-Job erneut zu erstellen, um z.B. einen anderen Branch zu bauen. Der neue Root-Job ruft die andere Version aus der Quellcode-Verwaltung ab. Durch den Trigger wird dessen Workspace an alle weiteren Jobs weitergegeben und diese bauen somit automatisch den gewünschten Branch. An den nachfolgenden Jobs sind keine Änderungen notwendig.
Neben dem Workspace lassen sich durch diesen Mechanismus – Weitergabe von Parametern über das Parameterized Trigger Plugin natürlich noch weitere Parameter an alle Jobs in der Kette “verteilen”.