Aus Linux-Magazin 03/2018

Continuous Integration mit Jenkins

© Guesswho, 123RF

Kombinieren Unternehmen Jenkins und Git sinnvoll miteinander und beachten die Admins ein paar Regeln bei der Softwareverwaltung, lässt sich das Bauen von Software vereinfachen und beschleunigen.

Wo Teams von Entwicklern täglich zusammenarbeiten, gilt der Einsatz von Continuous Integration (CI) als eine Selbstverständlichkeit. Als frei verfügbarer Buildserver behauptet sich Jenkins [1] in vielen Projekten als Werkzeug der Wahl, ein Fork des ebenfalls beliebten Buildservers Hudson [2]. In Sachen Funktionalität steht er kommerziellen Konkurrenten wie Bamboo [3] aus dem Hause Atlassian kaum nach. Grund genug, einen Blick auf die Möglichkeiten zu werfen, Jenkins in die eigene Infrastruktur zu integrieren.

Als Ausgangsszenario sollen typische Unternehmen dienen, die mehr als nur ein Softwareprojekt betreuen. Üblicherweise erfordert dabei jedes eigenständige Projekt eine separate Build-Infrastruktur. Das bedeutet allerdings nicht nur, dass die Admins jedes Mal ein entsprechendes Source Control Management Repository (SCM, zum Beispiel Git [4]) sowie Ressourcen auf dem Artefakt-Repository-Server (etwa Nexus [5] oder Artifactory [6]) einrichten müssen. Zusätzlich gilt es, jeweils eine neue Instanz des Buildservers zu erzeugen.

Aufbauhilfe

Als Beispielanwendung arbeitet in diesem Artikel eine unspezifische Java-EE-Webapplikation. Die lässt sich plattformunabhängig entwickeln und gestattet es außerdem, als Gastbetriebssystem für die Jenkins-Installation einen Linux-Server zu verwenden. Als mögliche Kandidaten kommen Fedora und Ubuntu in Frage, um nur zwei exemplarische Vertreter zu nennen.

Die Installation eines Jenkins-CI-Servers stellt keine besondere Hürde dar. So besteht zum Beispiel die Möglichkeit, das zugehörige War-Paket [7] zusammen mit einer Tomcat-Installation auszuliefern oder die alleinstehende Variante mit integriertem Jetty-Servlet-Container zu starten. Eine bestehende Installation zu aktualisieren, verlangt dem Administrator ebenfalls nur einen überschaubaren Aufwand ab.

Die Applikationsdaten der Konfiguration speichert Jenkins in einem gleichnamigen Ordner, der im Home-Verzeichnis des angemeldeten Nutzers landet. Diese Trennung von Applikation und Daten lässt sich später flexibel für die eigenen Zwecke instrumentalisieren.

Wer eine leicht zu duplizierende Jenkins-Installation aufsetzen möchte, könnte eine virtuelle Maschine (VM) nutzen, um auf ihr den Buildserver zu betreiben. Das zieht aber einen recht hohen Wartungsaufwand nach sich. Der elegantere Weg besteht darin, einen Docker-Container [8] einzusetzen. Dabei gibt es die Möglichkeit, einen durch die Community betreuten Docker-Container für Jenkins einzusetzen oder mit einem selbst gebastelten Dockerfile einen auf die eigenen Bedürfnisse zugeschnittenen Jenkins-Container per »docker build« zu generieren.

Wer den Mehraufwand scheut, sollte den durch die Community erstellten Container nutzen. Hilfreich ist, dass Docker Verzeichnisse des Hostsystems in Container einhängen kann. So lässt sich Jenkins’ Datenverzeichnis außerhalb des Containers vorhalten. Wie die Installation verläuft, zeigt Listing 1.

Listing 1

Jenkins-Container unter Ubuntu 17.10 installieren

01 sudo apt-get update
02 sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
03 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
04 sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
05 sudo apt-get update
06 sudo apt-get install docker-ce
07 sudo docker pull jenkins

Der Charme dieser Lösung besteht darin, dass sich Instanzen in schneller Folge innerhalb kürzester Zeit neu erzeugen lassen. Zugleich erlaubt sie es, die Daten mit Leichtigkeit zu sichern. Zerstört ein misslungenes Update eine Jenkins-Instanz, lässt sich das Datenverzeichnis einfach per Restore wiederherstellen und der Dienst selbst in einem frischen Container aufsetzen.

Um den Jenkins-Container der Community zu starten, empfiehlt sich laut [9] das folgende Kommando:

sudo docker run --name jenkins -p 8080:8080 -p 50000:50000 -v /home/User/docker/jenkins/master:/var/jenkins_home jenkins

Es bindet die beiden Ports »8080« und »50000« auf die entsprechenden Ports des Hostsystems, sie sind so über den Browser erreichbar (Abbildung 1). Zugleich bildet die Option »-v« noch das Verzeichnis »/home/user/docker/jenkins/master« auf dem Verzeichnis »/var/jenkins_home/« im Container ab.

Abbildung 1: Jenkins läuft im Container und lässt sich von außen über den Browser einrichten. Dabei holt sich die Software erst einmal Plugins aus dem Netz.

Abbildung 1: Jenkins läuft im Container und lässt sich von außen über den Browser einrichten. Dabei holt sich die Software erst einmal Plugins aus dem Netz.

Das setzt vollständige Pfadangaben voraus, fehlt das Zielverzeichnis im Container, legt Docker es automatisch an. Optional zeigt »pwd« auf das aktuelle Verzeichnis auf dem Host, in dem der Admin den Befehl ausführt. Wer eine Bash braucht, ergänzt die Anweisung noch um die Option »-it«.

Sollen auf einem Hostsystem mehrere Instanzen von Jenkins ihren Dienst verrichten, muss der Admin den Befehl entsprechend modifizieren. Dabei sollte er ein besonderes Augenmerk auf die Portweiterleitung legen. Da die Container ihre Ports an den Host durchreichen, muss er im Docker-Container selbst keine Anpassungen vornehmen, um das Routing kümmert sich das Hostystem.

Teilzeitjob

Mit Jenkins Swarm [10] lässt sich eine verteilte Master-Slave-Infrastruktur für Jenkins erzeugen. Bei Swarm handelt es sich um ein einfaches Java-Archiv, das als Slave-Client fungiert. Die komplette Administration einer solchen Master-Slave-Landschaft erfolgt über das Web-GUI der Master-Installation. Die Slaves fungieren als Agenten, die der Master in Aktion setzt. Abbildung 2 skizziert so eine potenzielle CI-Landschaft.

Abbildung 2: Der Master, die Swarm-Clients und andere Komponenten können in Containern oder VMs laufen.

Abbildung 2: Der Master, die Swarm-Clients und andere Komponenten können in Containern oder VMs laufen.

Über die Kommandozeile verbindet der Admin die Slaves mit dem Master, der im Container läuft:

java -jar swarm-client-3.7.jar -mode normal  -name "Swarm-Client-1" -labels "Node_01"  -description "This slave uses the Swarm- Plugin" -master "http://172.17.0.1:8080"  -username "admin" -password "admin"

Eine vollständige Auflistung aller Verbindungsparameter zeigt auch die Wiki-Seite für Jenkins Swarm-Plugins [10]. Hat der Admin einen Slave erfolgreich ins Rennen geschickt und dieser sich mit seinem Master verbunden, zeigt das GUI des Masters den Slave als aktiv an (Abbildung 3).

Abbildung 3: Die Abbildung zeigt verschiedene Buildprozesse im Master-GUI, darunter zwei, die zum Master gehören, und vier, die zu einem Client zählen.

Abbildung 3: Die Abbildung zeigt verschiedene Buildprozesse im Master-GUI, darunter zwei, die zum Master gehören, und vier, die zu einem Client zählen.

Buildjobs verteilt der Scheduler des Masters bei Bedarf komplett selbstständig. Daneben besteht die Möglichkeit, die Buildjobs explizit einem Slave zuzuweisen. Das wird vor allem dann nötig, wenn nicht alle Knoten eine identische Konfiguration mitbringen. Als Best Practice hat es sich bewährt, den Master ausschließlich mit Administrationsaufgaben zu betrauen und ihm keine Buildjobs zu übertragen. So lässt sich die CI-Umgebung auch bei hoher CPU-Last weiterhin verwalten.

Um einen bestimmten Buildjob an ein definiertes Konto zu delegieren, muss der Admin beim Konfigurieren der Slaves eindeutige Label setzen. Diese zieht die Build-Task dann heran und der Master wählt einen für die Aufgabe geeigneten Slave aus.

Jobvermittler

Jenkins’ Job-DSL-Plugin [11] liefert eine elegante Möglichkeit, komplexe Buildjobs mit Groovy [12] zu beschreiben. So ist der Admin in der Lage, alle Buildjobs unter ein Konfigurationsmanagement zu stellen. Auch kann er den Buildserver so über automatisierte Quellcode-Checkouts mit Aufgaben befüllen.

Arbeit spart der Admin, wenn er alle für das Unternehmen relevanten Buildjobs der einzelnen Projekte als Job-DSL-Dateien aufbereitet und das Sourcecode-Repository entsprechend organisiert. Dabei empfiehlt es sich sehr, für jeden Buildjob ein eigenes File anzulegen. So erhöht er die Lesbarkeit der einzelnen Jobbeschreibungen, schöpft aber auch die Stärken des Konfigurationsmanagements effizienter aus.

Im zweiten Schritt nutzt er einen automatisierten Buildjob, um die Jenkins-Instanz mit den Buildjobs zu befüllen. Den dazu notwendigen Seed-Job legt er als »JobType free-style«-Softwareprojekt an. In der anschließenden Konfigurationsübersicht des neu erstellten Buildjobs wählt er im Abschnitt »Build« für den Build-Schritt den Eintrag »Process Job DSL« aus einem Dropdown-Menü. Ins daraufhin erscheinende Eingabefeld trägt er die Zeilen aus Listing 2 ein. Speichert der Admin die Konfiguration und führt den Job zum ersten Mal per Hand aus, holt Jenkins alle in Git vorhandenen Jobbeschreibungen in den eigenen Projektbereich. Anschließend installiert es die vorgefundenen Jobs.

Listing 2

Job-Konfiguration

01 job('seed-job') {
02   scm {
03      git('git://github.com/my-project.git')
04   }
05   steps {
06      dsl {
07        external('*.groovy')
08        removeAction('DISABLE')
09     }
10   }
11   triggers {
12     scm('@hourly')
13   }
14 }

Die wenigen Zeilen des Skripts erklären sich weitgehend selbst, notwendige Anpassungen fallen einem routinierten Admin schnell auf. Die Beispiel-Konfiguration sieht allerdings nicht vor, aus Git entfernte Jobs vom Buildserver zu löschen. Diese Einstellung hat sich bewährt, um mögliche Fehlerquellen zu reduzieren. Der Trigger am Ende steht auf “stündlich”, um Git auf mögliche Änderungen hin zu überwachen.

In der hier gezeigten Konfiguration erscheinen die erstellten Buildjobs ungeordnet in der Jenkins-Übersichtsseite. Um die Übersichtlichkeit zu verbessern, kann der Admin die Jobs nach Projekten in einer Listenansicht organisieren. Dies zeigt Listing 3.

Listing 3

listView

01 listView('ProjektA') {
02   jobs {
03     name('Build-TRUNK')
04     name('Build-Release-1.5)
05   }
06   description('Software Projekt A')
07   columns {
08     status()
09     weather()
10     name()
11     lastSuccess()
12     lastFailure()
13     lastDuration()
14     buildButton()
15   }
16 }

Diese einfachen Beispiele demonstrieren auch gleich die Arbeitsweise des Job-DSL-Plugins. Das vollständige API der Job-DSL findet sich auf Github [13], ist sehr übersichtlich und mit vielen Beispielen kommentiert (Abbildung 4). Es deckt die wichtigsten Quellcode-Verwaltungssysteme ab, darunter Git, Subversion und sogar das recht angestaubte CVS. Aber auch verschiedenste Buildsysteme wie zum Beispiel Ant, Maven oder Gradle sind integriert.

Abbildung 4: Das API für Jenkins' Job-DSL-Plugin ist im Internet ausführlich dokumentiert.

Abbildung 4: Das API für Jenkins’ Job-DSL-Plugin ist im Internet ausführlich dokumentiert.

Shellskripte oder in Interpreter-Sprachen wie Ruby und Phyton verfasste Skripte erweitern den Funktionsumfang der DSL beliebig. Es ist aber ratsam, es mit den Anpassungen nicht ohne Not zu übertreiben, da diese die Komplexität erhöhen. Zugleich gilt es auch, schwer nachvollziehbare Build-Definitionen aus Gründen der Wartbarkeit zu vermeiden.

Großbaustelle

Große Projekte weisen oft die schmerzhafte Eigenschaft auf, dass vollständige Builds mehrere Stunden dauern. Das liegt nicht selten an überfrachteten Build-Plänen. Diese fristen ihr Dasein als schwer überschaubare Monolithen, dank denen Commits erst nach längerem Warten adäquates Feedback erzeugen.

Zu den weiteren unerwünschten Seiteneffekten gehören überdurchschnittlich stark beanspruchte Hardware-Ressourcen wegen parallel stattfindender Builds. Um solche Situationen in der eigenen Infrastruktur zu vermeiden, folgen nun einige Anmerkungen, die es Admins erlauben, effiziente Build-Pläne zu gestalten. Das gestattet einen Ressourcen-schonenden Umgang mit Builds, der sich auch positiv auf das Budget auswirkt.

Wer die einzelnen Schritte beim Bauen von Software-Artefakten betrachtet, identifiziert schnell Tasks, die nicht permanent im Einsatz sein müssen, etwa das Erstellen der API-Dokumentation. Solche Aufgaben sollte der Admin nicht direkt mit dem Erzeugen von Binärdateien in einen Topf werfen. So erzeugen Entwickler als API-Dokumentation für Java-Projekte meist Javadoc [14] als eigenständiges Jar-File.

Weil aber für eine CI-Umgebung das Kompilieren der Artefakte Priorität besitzt, genügt es, die API-Dokumentation in einem separaten Build einmal täglich außerhalb der Hauptlastzeit zu generieren. Neben dem Aufbau der Dokumentation gehören Sourcecode-Analysen oder das Optimieren oder Verschleiern von Binär-Artefakten zu solchen Teilaufgaben, die sich eigenständig und außerhalb des Hauptbuild erledigen lassen.

Als weitere Ursache für langwierige Builds erweist sich oft der Checkout-Prozess aus dem SCM. So enthalten voluminöse Repositories oft viele Dateien, die für das Erzeugen der Artefakte nicht unbedingt relevant sind, aber zum Flaschenhals werden. Typische Vertreter solcher Repositories wären Office-Dokumente-Sammlungen samt allen Versionen der Status-Meeting-Präsentationen.

Etwas seltener fehlen in solchen Projekten binäre Repositories wie Artifactory oder Nexus. In diesem Fall landen Zusatzbibliotheken unter Umständen als Binär-Artefakte ebenfalls im SCM. Auch das Vermischen verschiedenster Projekte in einem gemeinsamen Repository wirkt sich negativ auf Buildzeiten einer CI-Umgebung aus.

Ganz egal wie viel oder wenig Dateien der Buildprozess jedoch am Ende aus einem Repository zieht – der Checkout dauert bei Gigabyte-großen Repositories einfach auffallend lange. Dann kann es dem Admin helfen, die Struktur des SCM neu zu organisieren. Wer also beim Aufstellen von Build-Plänen ein wenig Sorgfalt walten lässt, darf auf bessere Ergebnisse hoffen.

Lessons learned

Die hier vorgestellten Ideen verknüpfen etablierte Technologien zu einem leistungsstarken Team. Doch bei aller Freude gibt es natürlich auch Einschränkungen. So beschränkt sich die Umsetzung der hier beschriebenen Infrastruktur unter Linux auf Plattformen, deren Compiler unter Linux laufen. Beispielsweise gibt es für Dotnet erhebliche Einschränkungen, da das Mono-Projekt [15] nicht den vollen Sprachumfang neuester Dotnet-Versionen unterstützt.

Dennoch vereinfachen es die genannten Automatisierungen maßgeblich, eigene Infrastruktur zu erzeugen, und warten für viele Szenarien leicht verständliche Lösungen auf ihren Einsatz.

Der Autor

Marco Schulz hat Diplominformatik studiert. Sein Fokus liegt auf Software-Architekturen, der Automatisierung des Software-Entwicklungsprozesses und dem Software-Konfigurationsmanagement. Seit über fünfzehn Jahren entwickelt er Webapplikationen, derzeit arbeitet er als freier Consultant mailto:marco.schulz@outlook.com.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben