Eine Einführung in Continuous Delivery, Teil 2: Commit Stage

Mit Continuous Delivery erhalten Entwicklungsteams jederzeit einen Überblick über die Qualität der Software. Erfüllt der aktuelle Softwarestand die funktionalen und nichtfunktionalen Anforderungen, kann dieser innerhalb kurzer Zeit in Produktion gehen. Doch wie sehen die konkreten Schritte hin zu einer Continuous Delivery Pipeline aus?

In Pocket speichern vorlesen Druckansicht 6 Kommentare lesen
Lesezeit: 18 Min.
Von
  • Alexander Birk
  • Christoph Lukas
Inhaltsverzeichnis

Mit Continuous Delivery erhalten Entwicklungsteams jederzeit einen Überblick über die Qualität der Software. Erfüllt der aktuelle Softwarestand die funktionalen und nichtfunktionalen Anforderungen, kann dieser innerhalb kurzer Zeit in Produktion gehen. Doch wie sehen die konkreten Schritte hin zu einer Continuous Delivery Pipeline aus?

Eine Continuous Delivery Pipeline besteht aus mehreren miteinander verketteten Teststufen, die mit automatisierten Tests die Qualität des aktuellen Softwarestandes überprüfen. Die erste dieser Stufen ist die sogenannte Commit Stage, in der die Software gebaut und ihre Funktionen mit Unit-Tests überprüft werden.

Mehr Infos

Eine Einführung in Continuous Delivery

In der anschließenden Acceptance Test Stage untersucht man weitergehende Anforderungen an die Software mit Integrations-, Akzeptanz- und Systemtests. Um von den Tests aufgedeckte Regressionen exakt einer einzelnen Änderung zuordnen zu können, löst man die Continuous Devlivery Pipeline direkt nach jeder Änderung am Sourcecode aus und generiert so zeitnah und kontinuierlich Feedback für die Entwickler. Schlägt eine der Teststufen fehl, wird der weitere Durchlauf dieses Softwarestandes durch die Pipeline abgebrochen. Kann der Softwarestand hingegen alle Teststufen erfolgreich passieren, steht einer Installation in der Produktion grundsätzlich nichts mehr im Wege.

In diesem Artikel beschreiben die Autoren den Aufbau der ersten Teststufe, der Commit Stage. Ihr Ziel ist es, eine erste Qualitätskontrolle mit Unit-Tests durchzuführen und im Erfolgsfall die Binärartefakte aller Softwarekomponenten zu einem sogenannten Bundle zusammenzuschnüren. Dieses Bundle wird in den weiteren Teststufen auf einer Testumgebung installiert und für das Ausführen der Tests benutzt. Bei Erfolg kann man exakt das gleiche Bundle, das die automatisierten Tests erfolgreich durchlaufen hat, unverändert auf dem Produktionssystem installieren. Dafür speichert man die in der Commit Stage erzeugten Bundles in einem Repository, um sie von dort aus wiederzuverwenden.

Insgesamt führt die Commit Stage die folgenden Aufgaben aus:

  • Build (kompilieren, falls nötig)
  • Ausführen der Unit-Tests
  • Paketierung und Bündlung der Artefakte
  • Upload des Bundles ins Binärartefakt-Repository
  • Erzeugen von Feedback über mögliche Regressionen für die Entwickler

Dabei liegen diesen Aufgaben zwei wichtige Prinzipien zugrunde:

  • Jeder Softwarestand wird innerhalb der Pipeline nur ein einziges Mal gebaut – nämlich genau hier in der Commit Stage.
  • Jede erfolgreiche Commit Stage produziert ein Software-Bundle – einen installierbaren Release-Kandidaten.

Was ist aber zu tun, um eine solche Commit Stage für eine Continuous Delivery Pipeline aufzubauen? Um ein erstes Gefühl für die dazu notwendigen Schritte zu entwickeln, soll diese Aufgabe für ein einfaches Beispielprojekt in einer vorbereiteten virtuellen Maschine nachvollzogen werden. Dazu ziehen die Autoren ein einfaches Beispielprojekt heran, das zeigt, wie man hierfür die Commit Stage einer Continuous Delivery Pipeline aufbaut.

Einfache Java-Anwendung für den Aufbau einer Commit-Stage (Abb. 1)

Als Beispielprojekt kommt eine kleine Java-EE-Webanwendung ("MusicDB") zum Einsatz, die gerade einmal genug Features enthält, um vielleicht einen einfachen Oberflächentest damit auszuführen. Die Software ist als ein einfaches Maven-Projekt angelegt und lässt sich im Tomcat ausführen.

Für die Umsetzung der obigen Aufgaben der Commit Stage setzen Softwareprojekte üblicherweise einen Build- oder CI-Server (Continuous Integration) ein. Im Beispiel kommt dafür Jenkins zum Einsatz, allerdings lassen sich die Aufgaben auch mit anderen Produkten (etwa Bamboo und Teamcity) in ähnlicher Weise umsetzen. Der Einfachheit halber sind alle sonst auf mehrere Rechner verteilten Komponenten der Continuous Delivery Pipeline in der bereitgestellten virtuellen Maschine vereint. Dort findet sich zum einen das Entwicklungssystem mit einem installierten Eclipse, zum anderen dient die gleiche Maschine als zentrales Versionskontrollsystem (VCS) und als Build-Server.

Nach dem Start der VM liegt der Sourcecode in Eclipse bereit. Der Zugriff auf die laufende Anwendung im lokalen Tomcat, auf das VCS und den installierten Jenkins-Server kann einfach über die Links auf der Startseite des Browsers geschehen.

Der Jenkins-Server ist noch komplett leer. Für den Aufbau einer Commit Stage ist im ersten Schritt der aus dem VCS ausgecheckte Sourcecode zu kompilieren, sodass Entwickler im Anschluss die Unit-Tests starten können. Dieser Aufgabe nimmt sich die IDE auf dem System während der Entwicklung kontinuierlich an; jetzt soll sie jedoch der CI-Server mit jeder Änderung am Sourcecode automatisiert ausführen. Um das zu erreichen, muss dieser Schritt jetzt von der Kommandozeile aus und ohne Benutzereingriff möglich sein. Im einfachen Maven-Projekt reicht dazu der Befehl:

mvn clean test

Hierbei kompiliert Maven zuerst den gesamten Quelltext und führt im Anschluss die Unit-Tests aus. Die Übertragung der Aufgabe in einen Job im Jenkins-Server (http://localhost:9080) ist recht schnell erledigt. Nach dem Anlegen eines neuen Maven-Projekt-Jobs (als Name bitte "1-commit-stage" wählen) trägt man zuerst unter "Source-Code-Management" das Git-Repository der MusicDB ein (git1@cddemo:cd-workshop-heise.git) und konfiguriert im Anschluss den eigentlichen Maven Build. Hier muss man den Pfad zur pom-Datei zu musicDB/pom.xml ändern und die gewünschten Maven Goals (clean test) eintragen. Nach dem Speichern der Job-Konfiguration kann man den Job manuell starten und so die Aufgabe "Build und Unit-Tests" ausführen. Dieser einfache Maven-Job stellt jetzt die erste Umsetzung der Commit Stage dar.

Konfiguration des einfachen Maven Build Job (Abb. 2)

Da man die Continuous Delivery Pipeline für jede Änderung an der Software starten will und die Commit Stage die erste Teststufe ist, sollte jede ins VCS übermittelte Änderung des Sourcecodes diese auslösen können. Das realisiert man durch ein sogenanntes Hook-Skript im Versionskontrollsystem. Ein entsprechendes findet sich in der Datei /srv/git-repo/cd-workshop-heise.git/hooks/post-receive. Damit das Skript den Commit Stage Job im Jenkins-Server starten kann, muss er ein Auslösen des Builds über die Jenkins API erlauben. Dazu setzen Entwickler in der Job-Konfiguration bei "Build-Auslöser" ein Haken bei "Build von außerhalb starten" und hinterlegen das folgende Token zur Absicherung: chahRee3Ko3k.

Start des Jenkins-Jobs bei jedem Commit (Abb. 3)

Damit jetzt zusätzlich genau der Softwarestand gebaut und getestet wird, der soeben ins zentrales Repository übertragen wurde, übergibt man vom Git-Repository die aktuelle Commit-ID an den Jenkins-Job. Dazu wird dem Job ein Build-Parameter mit dem Namen SHA hinzugefügt, den das Hook-Skript beim Auslösen des Jobs übergibt.

Übergabe der Commit-ID an den Jenkins-Job (Abb. 4)

Trägt man den Parameter in der erweiterten Konfiguration des Repository in der Job-Konfiguration ein, baut der 1-commit-stage-Job jetzt immer den gewünschten Softwarestand und nicht mehr einfach den HEAD des Master-Branches.

Checkout der gewünschten Source-Version im Jenkins-Job (Abb. 5)

Pusht man jetzt geänderten Sourcecode aus dem installierten Eclipse in das zentrale Git-Repo, startet Jenkins automatisch den 1-commit-stage Job und baut sowie testet genau den Softwarestand zum letzten Commit. Hiermit wurde ein erstes Zwischenziel erreicht: Mit jeder Änderung der Software im zentralen Repository wird automatisch ein Lauf der Commit Stage der einfachen Continuous Delivery Pipeline angestoßen. Dort werden die Software kompiliert und die Unit-Tests gestartet.

Nach dem erfolgreichen Abschluss der Unit-Tests sollen jetzt die erzeugten Artefakte in einem zentralen Binär-Repository für die spätere Wiederverwendung gespeichert werden. Dazu schnürt man die Kompilate des Build-Prozesses zu einem einfachen Paket zusammen. Bei Java-Software werden die Kompilate üblicherweise zu JAR- oder WAR-Files zusammengepackt. Das lässt sich im Build-Job im CI-Server durch ein einfaches Ersetzen des Maven Goal "test" durch "package" erreichen. Im Beispielprojekt landet so im Unterverzeichnis target eine musicDB-0.0.1-SNAPSHOT.war-Datei, die man direkt in einen Tomcat deployen kann.

Allerdings stößt man hier auf ein Problem: In der Continuous Delivery Pipeline soll dieses Artefakt auch in allen folgenden Teststufen wiederverwendet werden. Gleichzeitig muss aber sichergestellt sein, dass sich die dort gefundenen Testergebnisse genau einem Softwarestand zuordnen lassen, damit immer klar erkennbar ist, welche Änderung am Sourcecode eine Regression hervorgerufen hat. Wie aber lässt sich diese Rückverfolgbarkeit erreichen, wenn die Commit Stage mit jedem Lauf immer das gleichnamige Artefakt musicDB-0.0.1-SNAPSHOT.war erzeugt?

Da man beim Bau der Software mit einer Continuous Delivery Pipeline jeden Build der Software als einen potenziellen Release-Kandidaten auffasst, bekommt jetzt jeder Build nicht mehr eine Snapshot-Bezeichnung, sondern eine eindeutige Versionskennung, die sich dann auch im Dateinamen des Artefakts wiederfindet. Man geht also zugunsten der Rückverfolgbarkeit bewusst weg von den in der Java-Welt häufig üblichen Snapshot-Builds hin zu eindeutig versionierten Softwareständen.

In dieser auf den ersten Blick recht trivial erscheinenden Änderung steckt eine grundsätzlich andere Herangehensweise an den Entwicklungsprozess: Es gibt nicht mehr den eher unwichtigen Development Branch, in den gerne auch mal fehlerhafter oder unfertiger Code eingecheckt wird, und die wichtigen Release Branches, in denen jede Änderung nur mit großer Sorgfalt vorgenommen wird. Auch im Hauptenwicklungszweig fassen Entwickler jetzt jeden Commit als potenzielles Release auf. Technisch realisiert man diese Änderung zum Beispiel durch die Angabe der benutzten Versionskennung beim Aufruf von Maven:

mvn package -Dversion=20140325092556-d4dafcd-2341 

Durch die Angabe eines ensprechenden Property Tag in der pom.xml benutzt Maven diese Angabe für die Kennzeichnung der Version, die Angabe resultiert in einem Artefakt mit dem Namen:

musicDB-20140325092556-d4dafcd-2341.war

Durch Anfügen eines Zeitstempels ist sichergestellt, dass zeitlich aufeinanderfolgende Builds auch aufsteigende Versionsnummern tragen. Die angefügte Commit ID erlaubt schon aus dem Dateinamen des Artefakts den Rückschluss auf den zugrunde liegenden Commit. Die am Ende angefügte Build ID lässt zusätzlich einen Rückschluss auf den entsprechenden Lauf des Build-Jobs zu.

Grundsätzlich kommen hier verschiedene Möglichkeiten für das Format der Versionierung in Frage. Entscheidend an dieser Stelle ist aber, dass man das Binärartefakt zu einem Softwarestand nur ein einziges Mal erzeugt und sich dieses immer (auch wenn es auf dem Produktionssystem läuft) ohne Probleme einem exakten Softwarestand zuordnen lässt.

Um diese Versionierung nun auch im 1-commit-stage-Job im Jenkins zu integrieren, wird das EnvInject-Plug-in benutzt, um über ein kleines Groovy-Skript eine passende Versionsnummer bereitzustellen (Haken bei "Prepare an environment for the run" setzen):

def date = new Date()
def datestring = date.format("yyyyMMddHHmmss")
def version = datestring + "-" + SHA + "-" + BUILD_NUMBER;
return [VERSION: version]

Eindeutige Versionsnummer per EnvInject-Plug-in erzeugen (Abb. 6)

Damit steht im gesamten restlichen Lauf die Umgebungsvariable VERSION zur Verfügung, die dann dem Maven-Aufruf in den erweiterten Einstellungen übergeben wird:

Übergabe der Versionsnummer an Maven (Abb. 7)

Das so erzeugte und versionierte Artefakt ist grundsätzlich schon für die Ablage in einem Binär-Repository und die Wiederverwendung in späteren Teststufen geeignet.

Im Gegensatz zum einfachen Beispiel hier werden in "echten" Softwareprojekten zumeist mehrere Artefakte zu einem Softwarestand entstehen. So sei angenommen, dass es ein wie oben versioniertes WAR-File für eine Frontend- und ein Backend-Komponente gibt. Beide werden nach obigem Schema versioniert. Will man in folgenden Teststufen jetzt jedoch einen kompletten aus Front- und Backend-Komponente bestehenden Softwarestand auf eine Testumgebung installieren, muss man beim Deployment mit mehreren Artefakten hantieren und womöglich zusätzliche Metainformationen zu diesem Softwarestand bereithalten.

Um diesen Prozess zu vereinfachen, bietet es sich an, alle zum Softwarestand gehörenden Artefakte zu einem Bundle-File zusammenzupacken. Als ein solches Bundle-File kann wieder eine einfache .zip- oder tar.gz-Datei zum Einsatz kommen. Dadurch ist für das spätere Deployment nur noch ein einzige Datei aus dem Repository zu entnehmen, die automatisch einen in sich konsistenten und zusammen getesteten Softwarestand aus Front- und Backend-Komponente enthält. Zusätzlich bietet dieses Bundle-File die Möglichkeit, Metainformationen zum Softwarestand ebenfalls innerhalb des Bundles abzulegen und so beim Deployment immer zur Verfügung zu haben.

Im Beispielprojekt werden nach erfolgreichem Abschluss der Unit-Tests jetzt also alle erzeugten Artefakte der beteiligten Softwarekomponenten (hier eben nur ein einziges) zu einem Bundle-File zusammengepackt. Dazu wird ein einfaches Bash-Skript aufgerufen, das dieses Bundle erstellt:

musicDB/src/main/scripts/bundle_artifacts.sh

Dessen Dateiname trägt dabei die gleiche Versionskennung wie die einzelnen Artefakte:

musicdb-bundle_20140325092556-d4dafcd-2341.tar.gz

Für die MusicDB ist jetzt das Skript zum Erstellen des Software-Bundles in den Commit Stage Job im Jenkins-Server zu integrieren. Dazu konfigurieren Entwickler einen sogenannten Post-Build-Schritt vom Typ "Shell ausführen", der nur ausgeführt werden soll, wenn der eigentliche Build erfolgreich war. In das Textfeld tragen sie den Aufruf des bundle_artifacts.sh-Skripts ein:

bash musicDB/src/main/scripts/bundle_artifacts.sh $VERSION

Paketierung der Artefakte im Anschluss an den Build (Abb. 8)

Bei der Umsetzung von Packaging und Bundling liegt nach Erfahrung der Autoren eine erste Hürde bei der Einführung von Continuous Delivery: Viele Entwickler sind bisher nur den Umgang mit Snapshot-Artefakten auf der einen sowie "echten" Releases auf der anderen Seite gewohnt und sträuben sich gegen die Denkweise, dass jetzt plötzlich jeder Build eine Art Release sein und einen eindeutigen Bezeichner bekommen soll. Aber genau das ist ein zentraler Punkt von Continuous Delivery: Jeder Build ist ein potenzieller Release-Kandidat, der auch auf dem Produktionssystem landen kann und daher genau so zu behandeln ist.

Als Repository für die Binärartefakte kommen in reinen Java-Projekten häufig Produkte wie Artifactory oder Nexus zum Einsatz. Diese Repositories sind für die Ablage und Verteilung von Java-Artefakten konzipiert und in den meisten Softwareprojekten sowieso schon im Einsatz. Für die Ablage des Software-Bundles sind sie allerdings nicht immer geeignet: Erzeugt man für jeden Softwarestand ein neues Artefakt und legt es mit eindeutiger Kennzeichnung ins Repository, lassen sich dort zu gleicher Group-ID je nach Größe des Entwicklerteams durchaus über 100 verschiedene Versionen pro Tag deponieren. Hier müsste man also veraltete Artefakte in regelmäßigen Abständen aufräumen. Diese Aufgabe ist in diesen Repositories zwar durchaus vorgesehen, allerdings werden dabei lediglich ältere Versionen nach Erstellungsdatum als überflüssig betrachtet. Was aber, wenn man gerne die Artefakte nach anderen Kriterien wie "Erfolg der Teststufen" aufheben möchte?

Die Autoren haben daher in mehreren Projekten einen etwas anderen Ansatz verfolgt: Schließt man die in der Commit Stage erzeugten Artefakte zu einem eindeutig bezeichneten Software-Bundle zusammen, kann man diese Archive einfach in einer simplen Verzeichnisstruktur auf einer Repository-VM ablegen.

Einfache Verzeichnisstruktur als Binär-Repository (Abb. 9)

Auf dem CI-Server erzeugte Software-Bundles lassen sich dann einfach per SCP (Secure Copy) in dieser Verzeichnisstruktur im Repository kopieren. Für den Download der Bundles kann ebenfalls SCP zum Einsatz kommen, oder Entwickler stellen die Verzeichnisstruktur über einen Webserver zur Verfügung und können dann den Download der Bundles über HTTP realisieren. Für die MusicDB fügt man dem Shell-Post-Build-Schritt im Commit Stage Job einen weiteren Skript-Aufruf hinzu, der das eben erzeugte Bundle jetzt in das einfache
Verzeichnis-Repository kopiert:

bash musicDB/src/main/scripts/upload_bundle.sh $VERSION

Damit liegen nun alle von der Commit Stage erzeugten Artefakte zusammengepackt in einem Bundle im Binär-Repository und lassen sich von dort in den folgenden Teststufen wiederverwenden.

Damit wären die technischen Aufgaben der Commit Stage implementiert. Eine der wichtigsten Aufgabe einer Continuous Delivery Pipeline ist jedoch, aus allen Teststufen kontinuierlich Feedback über den aktuellen Qualitätsstand für die Entwickler zu generieren. Hierfür lassen sich unterschiedliche Kanäle nutzen. Die einfachste Möglichkeit, aus dem CI-Server eine Rückmeldung zu generieren, wäre der Versand einer E-Mail an die Entwickler bei einem fehlgeschlagenen Build. Das lässt sich im Jenkins Job einfach konfigurieren (s. Abb. 10).

E-Mail-Feedback an die Entwickler (Abb. 10)

Da die Entwickler im besten Fall sporadisch den Eingang neuer E-Mails überwachen, kann es passieren, dass das Feedback über einen defekten Build so längere Zeit unbeachtet bleibt. Ist jedoch die Commit Stage schon fehlerhaft, werden die übrigen Teststufen gar nicht mehr ausgeführt und können so auch kein weiteres Feedback für die Entwickler generieren. Wird der Fehler in der Commit Stage erst nach einigen Stunden behoben und so das Feedback der übrigen Teststufen wieder eingeschaltet, kommt es häufig vor, dass jetzt hier Testfehlschläge nachfolgender Änderungen auftauchen, die sich nun nicht mehr genau einer einzigen Änderung zuordnen lassen.

Gerade für die Commit Stage ist es daher besonders wichtig, Fehlschlägen sofort im gesamten Team auf den Grund zu gehen. Helfen kann dabei, das Feedback auch auf anderen Kanälen an die Teammitglieder zu übermitteln. Beispielsweise können automatische Nachrichten in Teamchats, Wall-Displays, die auf rot wechseln, oder sogenannten Extreme Feedback Devices unmittelbare Rückmeldung geben. Oder versandte Nachrichten in Teamchats oder Wall-Displays, die auf rot wechseln, geben unmittelbare Rückmeldung.

Damit das Feedback die Entwickler nach Möglichkeit noch erreicht, bevor sich diese in die nächste Aufgabe vertiefen, ist es wichtig, die Durchlaufzeiten von Änderungen in der Commit Stage im Auge zu behalten. Die gesamte Zeit für Kompilieren, Paketieren und Unit-Tests sollte nach Möglichkeit einen Wert von zehn Minuten nicht überschreiten. Um diese Grenze auch für komplexere Projekte einhalten zu können, ist der eigentliche Build von voneinander unabhängigen Modulen in der Regel zu parallelisieren und auf mehrere Build-Server zu verteilen.

Mit wenigen technischen Anpassungen ließ sich der einfache Maven Build Job zu einer vollständigen Commit Stage einer Continuous Delivery Pipeline erweitert werden. Der Job generiert nun eindeutig versionierte und rückverfolgbare Artefakte und die daraus erzeugten Bundles sind gut geeignet für ein Deployment auf Test- und Produktionsumgebungen. Da die Commit Stage für jede Änderung am Sourcecode gestartet wird, können die Entwickler gefundene Testfehlschläge exakt einer Änderung zuordnen.

Die folgenden Artikel werden nun den Aufbau weiterer Teststufen unter Wiederverwendung der Bundles sowie den Umgang mit Testumgebungen behandeln, um den Überblick über den Aufbau einer Continuous Delivery Pipeline zu vervollständigen.

Alexander Birk und Christoph Lukas
arbeiten seit 15 Jahren als freiberufliche Entwickler, Coaches und Berater unter dem Namen pingworks. Sie unterstützen Unternehmen und Teams bei der Anwendung von agilen Entwicklungsmethoden und der Umsetzung von Continuous Delivery.
(ane)