Zum Inhalt springen

Bei einem komplexen Kundenprojekt stellte sich die Herausforderung, ein vollumfängliches Entwicklungs- und Build-Setup zu etablieren, welches einerseits featurebasierte Entwicklung ermöglicht und andererseits aussagekräftige (Test-) Resultate zum Zustand der Software mit all ihren Komponenten liefert. In den folgenden Abschnitten soll das gewählte Projektsetup sowie das Vorgehen beim Build der Komponenten beispielhaft erläutert werden.

1. Das Projekt der mavenbasierten Entwicklung

Von Anfang an waren die folgenden Eckdaten klar resp. gegeben:

  1. Mavenbasierte Entwicklung
  2. Mehrere JEE-Webapplikation, welche im WebSphere-Application-Container laufen sollen
  3. RCP-Client für Bediener des Systems
  4. MQ-Messaging der Applikationen untereinander
  5. MQ-Messaging sowie http-Kommunikation für Datenlieferanten sowie -abnehmer

Während der Entwicklung der Software hat sich natürlich auch das Setup des Projektes an und für sich weiterentwickelt und verändert. Das beginnt mit der Versionierung des Sourcecode, welche während der laufenden Entwicklung nahtlos von Subversion zu Git migriert wurde, der Strukturierung des Sourcecode resp. der einzelnen Maven-Module bis hin zum Aufbau der ear-Container und der Migration von WebSphere 7 zu WebSphere 8.5, welche ebenfalls bei laufender Entwicklung durchgeführt wurde. Überlegungen, Ideen und Erfahrungen zur Git-Migration sind ein Thema für sich und wohl einen separaten Artikel wert. Im folgenden wird das aktuelle Setup und dessen Gründe beschrieben.

2. Grundsätzliche Projektstruktur mit mehreren Maven Modulen

Das Projekt setzt sich aus fünf eigenständigen Webapplikation sowie einem RCP-Client zusammen. Dabei stellte sich die Frage, wie deren Sourcecode im Repository organisiert wird. An dieser Stelle gibt es mehrere Möglichkeiten, von einem Repository je Maven-Komponente über ein Repository je Applikation bis hin zu einem Repository für das gesamte Projekt. Wichtiger Punkt an dieser Stelle war, dass der Build und insbesondere der Release-Build schnell und möglichst einheitlich vonstatten geht.

Innerhalb des Projektes gibt es mehrere Maven-Module, welche von den Applikationen gemeinsam verwendet werden. Somit muss sichergestellt werden, die korrekte Release-Version dieser Module zu referenzieren. Dieser Punkt macht den Build der Applikation doch recht kompliziert, da es zwangsläufig mehrere Schritte gibt. Im konkreten Fall also zunächst den (Release-)Build der Module, dann das Update der referenzierten Versionen in den anderen Modulen und im Anschluss daran deren Release-Build. Genau dieser Split verkompliziert die Sache ungemein, was vor der Git-Migration schmerzlich festgestellt wurde. Bis dahin wurden die Komponenten einzeln versioniert, was einen unglaublich komplexen Release-Prozess insbesondere für die Lieferung eines Bugfix der Produktivversion nach sich zog. Und gerade dann steht man in der Regel zusätzlich unter Zeitdruck.

Schlussendlich fiel die Entscheidung wie folgt:

  • Nur ein Gesamtrepository

Die Versionierung des gesamten Projektes erfolgt in einem gemeinsamen Git-Repository. Damit kann der konkrete Release-Stand exakt identifiziert und reproduziert werden.

  • Saubere Struktur der Maven-Module

Das Projekt wurde in eine saubere Maven-Struktur gegliedert, wobei das Root-Pom sämtliche Versionsdefinitionen sowie die Referenzen auf die Child-Module enthält. Der nächste Level der Gliederung erfolgt in Backend sowie Frontend, wobei das Backend auf den nächsten Ebenen die Websphere-Applikationen und das Frontend den RCP-Client enthält.

3. Projektstruktur hinsichtlich Maven

Die Modul-Struktur baut sich in Ausschnitten wie folgt auf:

Projekt-Root

+ backend
|  + core
|  |  + coreModuleA
|  |  |  - pom.xml
|  |  + coreModuleB
|  |  |  - pom.xml
|  |  - pom.xml
|  + common
|  |  + commonModuleA
|  |  |  - pom.xml
|  |  + commonModuleB
|  |  |  - pom.xml
|  |  - pom.xml
|  + webservice
|  |  + webserviceModuleA
|  |  |  - pom.xml
|  |  + webserviceModuleB
|  |  |  - pom.xml
|  |  - pom.xml
|  - pom.xml
+ frontend
|  + client
|  |  + clientBundleA
|  |  |  - pom.xml
|  |  + clientBundleB
|  |  |  - pom.xml
|  |  + product
|  |  |  - pom.xml
|  |  - pom.xml
|  + client-tools
|  |  + toolBundleA
|  |  |  - pom.xml
|  |  + toolBundleB
|  |  |  - pom.xml
|  |  - pom.xml
|  - pom.xml
- pom.xml

Unterhalb des Backends gibt es keine Profile sondern lediglich beim Frontend. Damit ist es möglich, das komplette Backend mit einem einzigen Build zu bauen. Das Frontend ist per Default deaktiviert.

Der Build der Frontend-Module verwendet die Tycho-Plugins, mit denen die beiden Welten der Maven- und OSGI- resp. RCP-Builds verbunden werden. Eine wichtige Einschränkung ist dabei, dass innerhalb eines Maven-Reaktors, also eines Maven-Builds nur genau ein RCP-Produkt gebaut werden kann. Genau an dieser Stelle kommen Maven-Profile zum Einsatz, durch welche genau gesteuert werden kann, welches RCP-Produkt gebaut werden soll.

Schlussendlich wurde das folgende Profil-Setup umgesetzt:

frontend/pom.xml:
<modules>
       <module>client-tools</module>
</modules>
<profiles>
       <profile>
               <id>client</id>
               <modules>
                      <module>client</module>
               </modules>
       </profile>
</profiles>

frontend/client-tools/pom.xml
<profiles>
       <profile>
               <id>toolA</id>
               <modules>
                      <module>toolBundleA</module>
               </modules>
       </profile>
       <profile>
               <id>toolB</id>
               <modules>
                      <module>toolBundleB</module>
               </modules>
       </profile>
</profiles>

Alle anderen Module sind direkt in der Modules-Section im jeweiligen Parent-Pom eingebunden. Mit diesem Setup wird einerseits mit einem einfachen 'mvn clean install' im Projekt-Root das komplette Projekt ohne die RCP-Komponenten gebaut und andererseits kann man durch den gleichen Maven-Aufruf direkt im entsprechenden RCP-Modul genau und ausschliesslich dieses Modul bauen.

4. Continuous Integration

Mit der in den vorherigen Abschnitten beschriebenen Struktur wird das Projekt vollumfänglich via Jenkins gebaut und auf verschiedene Entwicklungs- und Test-Systeme deployed. Grundsätzlich sind für den Build der Kernbestandteile zwei Jenkins-Jobs ausreichend: Ein Job für den Build aller Backend-Komponenten sowie ein Job für das Frontend.

Weiterhin wurden für diese zwei Jobs jeweils zwei Ausprägungen angelegt, einmal als "Continuous-Job" und einmal als "Nightly-Job". Der Continuous-Job ist dabei auf Geschwindigkeit getrimmt, bspw. durch die Option "Incremental Build" in den Advanced Maven Settings. Der Nightly-Job macht jedoch explizit immer einen Fullbuild und zusätzlich auch noch die Sonar-Analyse des gesamten Projektes.

4.1. Backend-Build

In der VCS-Konfiguration des Jenkins-Job wurde hier unter Excluded Regions "frontend/.*" eingetragen, so dass Pushes am Frontend-Code keinen Build des Backend auslösen. Der Build als solches ist ohne weitere Besonderheiten als "mvn clean deploy" im Root-Verzeichnis des Git-Clone konfiguriert.

4.2 Frontend- (Client-)Builds

Der Frontend-Build ist in der Konfiguration komplexer, da Pushes im Common-Teil ebenfalls relevant sind. Demzufolge wurde hier unter Excluded Regions folgendes konfiguriert:

backend/core/.*
backend/webservice/.*

Damit werden Changes unterhalb dieser beiden Folder ignoriert, alles andere löst jedoch einen Build aus, auch Änderungen an den pom-Files. Die einzelnen Build-Steps sind an dieser Stelle etwas umfangreicher, da für einen vollständigen aber alleinigen Build des Frontend die Parent-Poms vorhanden sein müssen. Das würde zwar durch die Auflösung via Maven-Proxy (Nexus) funktionieren, birgt jedoch das Risiko, dass veraltete Versionen angezogen werden, wenn der Backend-Build die jeweiligen Komponenten noch nicht in den Proxy deployed hat.

Aus diesem Grund werden alle relevanten Artefakte durch einzelne Build-Steps explizit gebaut, damit eine vollständige und vor allem aktuelle Maven-Hierarchie vorhanden ist:

Pre-Build-Steps:

1. Root-Pom:

pom-File: pom.xml
Maven-Command: mvn --non-recursive clean install

2. Backend-Pom:

pom-File: backend/pom.xml
Maven-Command: mvn --non-recursive clean install

3. Common-Komponenten:

pom-File: backend/common/pom.xml
Maven-Command: mvn clean install

4. Frontend-Pom:

pom-File: frontend/pom.xml
Maven-Command: mvn --non-recursive clean install

Nach diesen Pre-Build-Steps sind alle Voraussetzungen 
gegeben, um den RCP-Client zu bauen und zu deployen:

Client-Build:

pom-File: frontend/client/pom.xml
Maven-Command: mvn clean deploy

5. Feintuning des Builds

Nach dem bisher beschriebenen Setup funktioniert die CI bereits zuverlässig und kann auch nach dem üblichen Schema mit Continuous- und Nightly-Builds aufgesetzt werden. Bspw. können die Continuous-Builds via Push getriggert werden und die Nightlies bauen einmalig nachts und führen erweiterte Tests sowie die Sonar-Analyse durch.

Hier zeigten sich aber schnell verschiedene Einschränkungen. Das waren insbesondere die Grösse des Projektes, welche sich in der Dauer eines Build-Laufs niederschlägt sowie Build-Abbrüche, obwohl weitere Steps innerhalb des Builds von diesem Abbruch nicht betroffen wären. An dieser Stelle geht es nun an das Feintuning des Builds, um diesen schneller und in gewisser Hinsicht toleranter zu machen.

5.1 WebSphere-Deployment aus Build auslagern

Die einzelnen Applikationen werden innerhalb des Builds auf verschiedene Websphere-Dev-Instanzen deployed. Das dauert aber je Applikation durchaus mehrere Minuten, was den Build extrem in die Länge zieht. Damit haben die Entwickler nicht wirklich "schnell" Feedback.

Die Lösung dieses Problemes ist, das WebSphere-Deployment in ein Maven-Profil zu verlagern und dieses Profil in einem separaten Build-Job auszuführen. Damit wird innerhalb des eigentlichen Builds wirklich nur die gesamte Applikation gebaut und nachgelagerte Deployment-Jobs deployen die Applikationen auf den WebSphere-Instanzen.

Dazu wurde in allen Root-Poms der WebSphere-Applikationen ein Delivery-Modul eingeführt:

...
|  + core
|  |  + coreDelivery
|  |  |  - pom.xml
|  |  + coreModuleA
|  |  |  - pom.xml
...
|  + webservice
|  |  + webserviceDelivery
|  |  |  - pom.xml
|  |  + webserviceModuleA
|  |  |  - pom.xml
|  |  + webserviceModuleB
|  |  |  - pom.xml
...

Das Vorgehen innerhalb der Delivery-Module ist bei allen WebSphere-Applikationen exakt gleich. Der Build des Projektes bleibt somit unverändert, hat jedoch mehrere nachgelagerte Jobs für das WebSphere-Deployment der einzelnen Komponenten. Diese Jobs werden ausschliesslich vom Build-Job getriggert, springen also nur dann an, wenn die Applikation vorher neu gebaut wurde.

Hier beispielhaft der Core-Deployment-Job:

pom-File: backend/core/coreDelivery/pom.xml
Maven-Command: mvn clean deploy -Pdeploy2WAS

Nun wird bei einem Change ein Build ausgelöst und die WebSphere-Deployments laufen davon unabhängig erst danach und weitestgehend parallel für die einzelnen WebSphere-Applikationen.

5.2 Test-/Int-/Prod-Deliveries

Für das Deployment auf die Test-, Integrations- und Produktionsumgebungen müssen die Releasenotes, Übergabedokumente, allfällige Scripte für die Datenbank-Updates etc. geliefert werden. Um diesen Schritt möglichst einfach zu gestalten, wurden in den jeweiligen Delivery-Modulen weitere Profile eingeführt, welche via Maven-Assembly-Plugin Zip-Archive erzeugen, in denen die notwendigen Dateien enthalten sind. Hier machte es sich ebenfalls bezahlt, das in allen Delivery-Modulen ein identisches Vorgehen resp. Verhalten implementiert wurde.

6. Release-Build

Da der Build bedingt durch den RCP-Client nicht in einem einzigen Maven-Lauf durchgeführt werden kann, ist ein echter Maven-Release-Build via Maven-Release-Plugin nicht möglich. Aus diesem Grund wurden die notwendigen Schritte als eine Reihe einzelner Jenkins-Jobs aufgesetzt, welche somit die Einzelschritte nachbilden. Werden diese Jobs direkt miteinander verkettet, lässt sich der Release-Build ebenfalls "mit einem Klick" durchführen.

6.1 Aufteilung der Buildschritte

Die einzelnen Steps eines Release-Build sind die folgenden:

  1. Versionseinträge auf Release-Version setzen und pushen
  2. Build von Backend und Frontend
  3. Tag anlegen
  4. Versionseinträge für nächste Dev-Iteration setzen
  5. Site-Build der Release-Version

Der gesamte Release-Build wird auf einem separaten Release-Branch durchgeführt, welcher unmittelbar vorher aus dem Develop erzeugt und im Anschluss daran zurück gemerged wird. Aus diesem Grund wird das Tag auch erst nach dem erfolgreichen Build angelegt.

Die Buildjobs selbst sind fortlaufend nummeriert und zudem mit einem sprechenden Namen versehen, so dass jederzeit von den berechtigten Personen ein Release-Build durchgeführt werden kann:

ReleaseStep01-SetReleaseVersion
ReleaseStep02.1-BuildBackend
ReleaseStep02.2-BuildFrontend
ReleaseStep03-CreateTag
ReleaseStep04-SetDevVersion
ReleaseStep05-CreateSite

Die beiden "richtigen" Buildsteps können parallel ausgeführt werden, weshalb sie unter Schritt zwei zusammengefasst wurden.

6.2 Setzen der Versionen

Für den Release-Build müssen sämtliche Versionseinträge zweimal bearbeitet werden. Zunächst für den eigentlichen Release-Build mit der effektiven Release-Version sowie danach zum setzen der nun folgenden Development-Version.

Um die Versionen in allen Pom-Files eines Maven-Projektes zu setzen, steht das maven-version-plugin zur Verfügung. Das funktioniert problemlos und wird ohne Probleme in einem Jenkins-Job durchgeführt. Problematischer wird das bei einem Maven-RCP-Projekt, hier also dem Frontend-Teil, da die Versionen nicht nur in den Pom-Files sondern bspw. auch in den Feature- und Product-Files gesetzt werden müssen.

Glücklicherweise gibt es von Tycho ebenfalls ein Plugin dafür, wobei allerdings die exakte Vorgehensweise resp. Reihenfolge der einzelnen Schritte zu beachten ist. Das tycho-version-plugin löst ebenfalls zunächst den kompletten Dependency-Tree auf, bevor die Versionen der einzelnen Komponenten resp. Dateien aktualisiert werden. Aus diesem Grund ist es wichtig, dass die Dependencies sauber aufgelöst werden können. Das lässt sich am einfachsten sicherstellen, indem vorher ein normaler Build lief und alle Komponenten in den Maven-Proxy deployed hat. Es ist ebenfalls essentiell wichtig, den Update via tycho-version-plugin vor dem Update via maven-version-plugin durchzuführen. Anderenfalls werden vom tycho-version-plugin die gemeinsam verwendeten Komponenten nicht gefunden, da diese dann ja bereits die neue Version haben. Somit hat der Jenkins-Job zum setzen der Versionen den folgenden Aufbau:

  1. Clonen des Repo und Checkout des Release-Branch
  2. Setzen der Versionen des Frontend-Teils via tycho-version-plugin
  3. Setzen der Versionen des Backend-Teils via maven-version-plugin
  4. Push der Changes

Da dieser Ablauf während dem Release zweimal durchgeführt wird, wurde der Job als parametrisierter Job aufgesetzt und dazu zwei Trigger-Jobs angelegt, welche den eigentlichen Version-Update-Job anstossen. Die beiden Trigger-Jobs sind somit die Jobs ReleaseStep01-SetReleaseVersion und ReleaseStep04-SetDevVersion.

6.3 Release-Build

Hier gibt es keinerlei Besonderheiten. Clonen des Repo, Checkout des Release-Branch und Maven-Build resp. Maven-Deploy.

6.4 Tag anlegen

In diesem Job wird via Shell-Step das Git-Tag angelegt. Somit Clonen des Repo, Checkout des Release-Branch, Tag anlegen und pushen

6.5 Maven-Site-Build

Der Build der Maven-Site ist sehr speziell und nicht in einem Schritt durchführbar. Dies insbesondere aus dem Grund, dass der Build selbst mit extrem grossem Java-Heap mit OOM-Errors aussteigt. Vermutlich ist das der Tatsache geschuldet, dass all die verschiedenen Schritte wie das Erzeugen der JavaDocs oder Dokumentation der verschiedenen Datenbank-Schemata via maven-schemaspy-plugin extrem resourcenhungrig sind. Aus diesem Grund wurde der Site-Build in einzelne Schritte aufgeteilt, welche dann im Nachgang zusammengeführt werden. Das Ganze findet innerhalb eines Shell-Script statt, auf dessen Erläuterung hier jedoch nicht weiter eingegangen wird.

7. Ausblick

Weiteres Potential bzgl. des Builds und dessen Dauer ist durchaus vorhanden. Bspw. wird nach obigem Beispiel der Continuous zwar inkrementell gebaut und ist damit je nach Position des auslösenden Commit im Dependency-Tree mitunter sehr schnell, die Deployments werden jedoch für alle Websphere-Applikationen ausgelöst, da es an dieser Stelle nicht feststellbar ist, welche Webapplikation tatsächlich neu gebaut wurde.

Splittet man nun den Build in mehrere separate Jobs auf, welche ihrerseits die Dependencies abbilden, werden ebenfalls nur die relevanten Bestandteile neu gebaut aber auch nur die neu gebauten Komponenten in den Websphere deployed. Damit entfällt die Deployment-Downtime für die Applikationen vollständig, welche keine Changes enthalten und damit auch nicht erneut deployed werden müssen.