Jenkins ist der weltweit am meisten genutzte Open-Source-Automatisierungsserver mit unzähligen Plugins für eine Vielzahl von Technologien und Einsatzszenarien rund um Continuous Integration und Continuous Delivery [1]. Dieser Artikel stellt verschiedene Möglichkeiten und Werkzeuge vor, mit denen der Jenkins-Server dazu genutzt werden kann, einen weiteren Schritt in Richtung DevOps zu gehen.

Jenkins Build Jobs

Ist ein Jenkins Server eingerichtet und aktiv, können Aufgaben in sogenannten Jenkins Build Jobs definiert werden. Diese Jobs können einzeln über die Oberfläche konfiguriert und manuell, als auch automatisiert, z.B. zeitgesteuert oder bei Änderungen in der Versionsverwaltung, gestartet werden. Da die Jobs im Normalfall nach Aufgaben getrennt sind, pro Job also nur eine Aufgabe ausgeführt wird, werden pro Projekt mehrere Jobs benötigt. Über die Zeit, kann so aus wenigen Jobs relativ schnell eine große und unübersichtliche Menge an Jobs heranwachsen, welche sich nur schwer und mit viel Zeitaufwand pflegen lässt.

Soll nun eine Änderung in mehreren Jobs vorgenommen werden, beispielsweise das Versionierungssystem von Subversion auf Git umzustellen, muss jeder Jenkins Job einzeln abgeändert werden. Da die Jobs vom Jenkins intern als XML Dateien gespeichert werden, ist es durchaus möglich die Änderungen dort vorzunehmen, allerdings ist dies sehr kompliziert und oftmals fehlerbehaftet, da dort keinerlei Validierung der vorgenommenen Änderung stattfindet. Damit stellt die Änderung der XML Dateien keinen sinnvollen Weg dar. Für die Bearbeitung von mehreren Jobs gibt es zudem Plugins aus der Jenkins Community, welche mehr oder weniger gut funktionieren. Das generelle Ziel sollte hier allerdings nicht sein, Werkzeuge zu finden, die bei der aufwändigen Änderungsmethode von Jenkins Jobs unterstützen, sondern eine Methode zu finden, welche es ermöglicht Jenkins Jobs einfach zu erstellen und später auch zu pflegen.

Ein weiterer Punkt, welcher gegen eine Vielzahl von Jenkins Jobs spricht, ist die Dezentralisierung. Verteilt sich ein Prozess auf verschiedene Jobs können diese zwar verkettet und somit automatisch hintereinander aufgerufen werden, allerdings kann dies auch Risiken mit sich bringen. Ein Nutzer kann eine Verkettung nicht direkt sehen, da er dazu in die Konfiguration der einzelnen Jobs schauen müsste. Führt dieser Nutzer zum falschen Zeitpunkt unwissentlich eine ganze Reihe an Jobs, anstatt einen einzelnen Job, aus, kann es schnell zu Probleme kommen. Zudem ist es schwierig ein Reporting über mehrere Jobs zu realisieren um den Fortschritt und die jeweiligen Ergebnisse aller Jobs auf einen Blick sehen zu können. Hier liegen die einzelnen Informationen alle verteilt in jedem einzelnen Jenkins Job.

Jenkins stammt ursprünglich aus einer Zeit, in welcher Programme ausschließlich über die Benutzeroberfläche gesteuert wurden. Damals boten viele Programme keinerlei Optionen, erstellte Neuerungen als Code zu speichern um diesen dann zu versionieren. Auch die Jenkins Build Jobs verlassen sich daher zu viel auf die Benutzeroberfläche, d.h. sie sind dazu designt über die Oberfläche erstellt zu werden. Damit Jenkins Build Jobs versioniert werden können, müssen viele verschiedene XML Dateien in das Versionierungssystem eingespielt werden. Zusätzlich zur Versionierung spielt hier auch Dokumentation eine Rolle. Da die Jenkins Build Jobs über eine Oberfläche erstellt werden, lassen sich alle Änderungen und Konfigurationen nicht einfach dokumentieren, sondern es müssen alle in der UI vorgenommenen Änderungen detailgetreu aufgeschrieben oder Screenshots angefügt werden. Hier zeigt sich auch das Problem der Reproduzierbarkeit. Wird ein kleines Detail vergessen, kann es vorkommen, dass zwei, fast identische, Jenkins Build Jobs komplett unterschiedliche Ergebnisse erzielen.

Zusammengefasst haben Jenkins Build Jobs mit umfangreicheren Projekten einige Probleme, sei es die reine Anzahl der einzelnen Jobs, die Unübersichtlichkeit und Dezentralisierung von Informationen, ungewollte Risiken und die nicht triviale Versionierungsmöglichkeit. Eben diese Hürden gilt es zu überwinden um weiter in Richtung DevOps zu gehen, und genau dafür gibt es seit Frühjahr 2016 eine offizielle Lösung mit Continuous Delivery Pipelines vom Jenkins Entwicklerteam rund um Kohsuke Kawaguchi.

DevOps und CI/CD

Vielleicht vorher aber nochmal einen Schritt zurück und das große Ganze betrachten und verstehen. Wo soll es überhaupt hingehen? Was ist DevOps? Und wie helfen Continuous Delivery Pipelines dabei, mehr in Richtung DevOps zu kommen?

Abbildung 1: Der DevOps Zyklus [2]

Ganz grob gesagt ist DevOps die Kombination aus Development und Operations. Wird versucht DevOps genauer zu definieren, könnte es in etwa folgendermaßen klingen: „DevOps bezeichnet eine Reihe von Praktiken zur Automatisierung der Prozesse zwischen Softwareentwicklern und IT-Teams, durch die Software schneller und zuverlässiger entwickelt, getestet, freigegeben [3], betrieben und gewartetwerden kann“. Eine genaue Definition ist allerdings schwierig, da DevOps kein konkretes Werkzeug bzw. keine konkrete Methode, sondern eher eine Unternehmenskultur, ist. Zusammengefasst kann gesagt werden, DevOps vereint alle notwendigen Schritte, sowohl die des Entwicklungszyklus:

  • Planen
  • Entwickeln (Code schreiben)
  • Bauen
  • Testen

Als auch die des Betriebszyklus:

  • fertiges Release erstellen
  • in der Kundenumgebung einspielen
  • betreiben
  • überwachen

Aus diesen beiden, ohne DevOps getrennten, Kreisläufen bildet sich somit ein einzelner Zyklus welcher, solange das Produkt lebt, unendlich weiterläuft (siehe Abbildung 1).

Continuous Delivery ist, wie auch DevOps, eines der Schlagworte, die überall genannt werden, aber oftmals gar nicht richtig interpretiert werden können. Was ist z.B. der Unterschied zwischen Continuous Integration, Continuous Delivery und Continuous Deployment? Und wie passt DevOps dazu? Die Unterschiede zeigen sich deutlich, wenn die einzelnen Schritte des Software Lebenszyklus betrachtet werden (Abbildung 2).

Abbildung 2: Abdeckung des Software Lebenszyklus durch Continuous Integration, Continuous Delivery, Continuous Deployment und DevOps

Nun ist auch ersichtlich, dass Continuous Delivery kein Synonym von Continuous Deployment ist, auch wenn beide häufig mit CD abgekürzt werden. Ein Release in eine Staging-Area, also Testumgebung, einzuspielen und danach manuell auszuliefern oder ein Release direkt in das Produktivsystem des Kunden einzuspielen ist ein großer Unterschied.

Jenkins Pipelines

Nachdem nun definiert ist wohin es gehen soll, müssen jetzt noch die technischen Mittel gefunden werden, um diesen Weg zu gehen. Seit 2016 gibt es vom offiziellen Jenkins Entwicklerteam das Pipeline Plugin [4], oder besser die Pipeline Plugin Suite, da das Pipeline Feature auf mehrere Plugins aufgeteilt ist. Diese Plugins erweitern den Jenkins Server um die Funktionalität, Pipelines auszuführen. Pipelines sind Automatisierungsketten, welche, im Falle einer Continuous Delivery Pipeline beispielsweise alle Schritte zum Release einer Software abarbeiten. Nach einer einfachen Installation der Plugins auf dem Jenkins Server kann bereits mit den Pipelines gearbeitet werden, in den meisten Fällen sogar ohne einen Neustart.

Im Gegensatz zu den Build Jobs wird bei einem Pipeline Job die gesamte Logik in einem Skript, dem sogenannten Jenkinsfile, hinterlegt. Dieser Jenkinsfile sollte auch in das Versionierungssystem eingepflegt werden. Aktuell gibt es zwei Versionen von Jenkins Pipelines. Die erste, initiale, Variante heißt Scripted Pipeline. Hierbei wird der Jenkinsfile, auf welchem die Pipeline aufbaut, noch größtenteils in Groovy, einer auf der Java-Plattform basierenden Programmier- und Skriptsprache, geschrieben. Zusätzlich gibt es die zweite, und neuere, Möglichkeit die Pipelines deklarativ zu erstellen. Die sogenannten Declarative Pipelines basieren auf einer DSL, Domain-Specific-Language, welche auf Groovy aufbaut. Wird eine Pipeline geschrieben, sollte der deklarative Ansatz gewählt werden, da die Scripted Pipeline in die deklarative Variante miteingebunden werden kann, und somit obsolet ist.

Abbildung 3: Erstellung einer neuen Jenkins Pipeline

Da der Jenkins Server die Pipelines als weiteren Job Typ definiert, können Pipelines auf die gleiche Weise wie die normalen Build Jobs angelegt werden. Nach einem Klick auf die Schaltfläche „Element anlegen“ kann nun als Job Typ „Pipeline Job“ ausgewählt werden. Die Oberfläche zur Konfiguration eines Pipeline Jos sieht hierbei ähnlich aus, wie die der Build Jobs. Hier sollte allerdings nicht der Fehler gemacht werden, dass zu viel in der Oberfläche konfiguriert wird. Diese Konfigurationsoptionen stehen ebenfalls im Jenkinsfile zur Verfügung. Wenn mehrere Pipelines auf einem Jenkinsfile aufbauen, muss hier nicht jeder Pipeline Job einzeln konfiguriert werden, sondern jeder Job zieht sich die notwendige Konfiguration aus dem Jenkinsfile. Einzig die Verbindung zum bevorzugten Versionierungssystem muss in jedem Pipeline Job in der Oberfläche gesetzt werden.

Abbildung 4: Beim Konfigurieren einer Jenkins Pipeline die Option „Pipeline script from SCM“ wählen

Da ein Jenkinsfile genutzt wird, findet sich im Pipeline Job selbst keinerlei Konfiguration wieder. Theoretisch besteht auch die Möglichkeit die Konfiguration direkt in der Oberfläche des Pipeline Jobs zu erstellen und die Pipeline nicht in einen Jenkinsfile zu schreiben. Dies wiederspricht allerdings der Idee der Jenkins Pipelines, da diese Pipeline Jobs dann genau wie normale Build Jobs konfiguriert sind und alle Vorteile verloren gehen. Daher sollte ganz unten in der Konfiguration unbedingt die Option, den Jenkinsfile aus der Versionierung zu laden, gewählt werden. Hier stehen alle unterstützten Versionierungssysteme von Jenkins zur Verfügung. Neben Git lassen sich weitere Systeme wie z.B. Subversion und BitBucket über Plugins installieren.

Jenkinsfile

Der Jenkinsfile einer Declarative Pipeline besteht hauptsächlich aus Sektionen und Direktiven. Der Anfang jeder Pipeline ist der pipeline Block. Alles, was in der Pipeline passiert, muss innerhalb dieses Blockes stehen. Sind selbst geschriebene Groovy Funktionen vorhanden, können diese auch außerhalb des Blockes definiert werden. Sinnvoller ist es in diesem Fall aber eine sogenannte Shared Library [5] zu nutzen. Dies ist ein Feature über welches die Standard DSL Sprache um eigene Funktionen erweitert werden kann. Die grobe Struktur innerhalb des pipeline Blockes ist durch sogenannte stages definiert. In jeder stage können verschiedene Befehle innerhalb einer steps Sektion ausgeführt werden. Zudem kann in jeder stage eine post Sektion eingefügt werden. In dieser können verschiedene Befehle aufgrund des Ergebnisses der steps Sektion ausgeführt werden. Diese post Sektion kann ebenfalls am Ende der gesamten stages Sektion definiert werden. Als erstes innerhalb des pipeline Blockes muss mit Hilfe der agent Sektion ein Exekutor, auf welchem die Pipeline, oder einzelne Stages, ausgeführt werden, gewählt werden. Hier bietet Jenkins verschiedene Möglichkeiten. Mit agent any bestimmt Jenkins den Exekutor, mit agent none wird global kein agent festgelegt, dieser muss dann innerhalb der jeweiligen stages definiert werden. Soll ein bestimmter agent gewählt werden kann dies über ein label geschehen. Weitere Möglichkeiten sind in der Jenkins Pipeline Dokumentation beschrieben [6].

Pipeline {
    agent {
        label: ‘linux’
    }
    …
    stages {
        …
    }
}

Listing 1: Einen bestimmten Agent über ein Label selektieren

Nach dem der Exekutor definiert wurde, können nun durch weitere Direktiven, noch vor den Stages, einige Optionen, Parameter, Trigger und Umgebungsvariablen gesetzt werden. Nachfolgend sind alle häufig genutzten Sektionen und Direktiven aufgelistet, ausführlichere Informationen zu diesen und weiteren Befehlen finden sich ebenfalls in der Dokumentation der Jenkins Pipelines [6].

Tabelle 1: Auflistung einiger Sektionen und Direktiven

Um die Pipeline nun mit Funktionalität zu versehen, müssen die gewünschten Befehle in die steps Sektion geschrieben werden. Hierfür empfiehlt es sich den, durch die Pipeline Plugins, in den Jenkins integrierten Pipeline Snippet Generator zu nutzen. Dieser findet sich innerhalb eines Pipeline Projektes im Menü auf der linken Bildschirmseite. Hier sind alle verfügbaren Befehle für eine Pipeline enthalten und mit Hilfe der Oberfläche konfiguriert werden. Als Ergebnis liefert der Generator ein Snippet, welches nahtlos in die Pipeline integriert werden kann. Für Plugins, welche Jenkins Pipelines unterstützen, sind die Befehle ebenfalls im Snippet Generator verfügbar. Zusätzlich ist auch ein Direktiven Generator für Declarative Pipelines und viele Referenzen, beispielsweise für einzelne Steps oder globale Variablen, vorhanden.

Abbildung 5: In Jenkins integrierter Snippet & Directive Generator

Zudem gibt es die Möglichkeit den Jenkinsfile validieren zu lassen, bevor die Pipeline ausgeführt wird, indem der Jenkins Pipeline Linter genutzt wird. Dieser wird auf dem Jenkins Server ausgeführt und der Jenkinsfile per http Post-Request übertragen. Je nach Konfiguration müssen hier noch Vorkehrungen getroffen werden, damit der Jenkins Server die Anfrage erfolgreich erhält, mehr dazu findet sich in der Dokumentation [7].

Besonderheiten und Best Practices

Müssen innerhalb der Pipeline, sprich dem Jenkinsfile, Passwörter genutzt werden, sollte unbedingt das Jenkins Credentials Plugin [8] genutzt werden, welches normalerweise standardmäßig mit dem Jenkins installiert wird. Wie bei den Build Jobs ermöglicht das Plugin auch in den Pipelines die Nutzung von IDs, um im Jenkins hinterlegte Benutzerdaten zur Laufzeit abzurufen. Somit stehen keine Benutzerdaten im Klartext in Skripten oder im Jenkinsfile.

withCredentials([usernamePassword(
credentialsId: 'test_credentials',
passwordVariable: 'test_password',
usernameVariable: 'test_user')]) {
    bat '''cd Projektordner
    connect_db %apex-connection% %test_user% %test_password%
    @scripts\\myScript.sql'''
}

Listing 2: Aufruf des Credentials Plugins aus dem Jenkinsfile

Sollen Informationen des aktuellen Builds des Pipeline Jobs abgerufen werden, kann dies mit Hilfe der Eigenschaft currentBuild bewerkstelligt werden. Diese enthält Informationen über die ID bzw. Nummer, den Display Namen, die Beschreibung und das aktuelle Ergebnis, auch zur Laufzeit. Damit ist es möglich, das Ergebnis eines Build Schrittes oder auch der gesamten Pipeline zu prüfen und manuell das Ergebnis zu setzen, sollte der Jenkins Server dies einmal nicht automatisch richtig erkennen. Ist das Ergebnis allerdings einmal fehlerhaft (FAILURE), lässt es sich nur noch auf instabil (UNSTABLE) und nicht mehr auf erfolgreich (SUCCESS) ändern. Bricht ein Benutzer die Pipelineausführung ab gibt es noch den Status ABORTED. Hier sollte also möglichst vor dem Setzen des Ergebnisses geprüft werden.

if (result == "UNSTABLE") {
    currentBuild.result = "SUCCESS"
}

Listing 3: Abrufen und ändern des Build Ergebnisses zur Laufzeit

Soll ein Pipeline Job nicht im Standard Jenkins Workspace laufen, kann ein sogenannter Custom Workspace angegeben werden. Hier sollte darauf geachtet werden, dass der Jenkinsfile zuerst aus der Versionierung ausgecheckt wird und dann erst ausgeführt wird. Da der Custom Workspace allerdings erst im Jenkinsfile definiert wird, wird der Jenkinsfile selbst immer in den Standard Workspace ausgecheckt. Hier sollte wirklich nur der Jenkinsfile und nicht etwa das ganze Repository ausgecheckt werden um Duplikate zu verhindern und nicht unnötig Speicherplatz zu verschwenden.

pipeline {
    agent {
        label {
            label ""
            customWorkspace "path/to/workspace"
        }
    }
}

Listing 4: Setzen eines Custom Workspaces innerhalb des Jenkinsfiles

Funktioniert eine Pipeline nicht wie gewünscht gibt es in der Oberfläche des Pipeline Jobs eine Replay Funktion. Mit dieser ist es möglich, den Jenkinsfile temporär in der Oberfläche zu ändern, um dann einen erneuten Durchlauf zu starten. Der Jenkinsfile im Hintergrund bleibt dabei unberührt. Somit ist es einfacher einen Fehler innerhalb des Jenkinsfiles zu finden, ohne diesen jedes Mal committen zu müssen.

Abbildung 6: Replay Funktion in der Oberfläche eines Builds

Oberfläche

Wurde ein Pipeline Job angelegt und mindestens einmal ausgeführt, erscheint in der Oberfläche eine Übersicht über die Ausführungsdauer und das Ergebnis der einzelnen Stages. Zudem lassen sich die Logs der jeweiligen Stagen mit einem Klick auf die Stage abrufen. Ist die Pipeline erfolgreich durchgelaufen erscheinen alle Stages grün. Gab es einen Fehler in einer Stage ist diese und alle nachfolgenden, nicht ausgeführten, Stages, dunkelrot eingefärbt. In diesem Fall färben sich auch die zuvor erfolgreich durchgelaufenen Stages hellrot. Oben in der Übersicht stehen die zuvor vergebenen Namen der Stages und eine Anzeige, wie lange die Ausführungsdauer diese Stage durchschnittlich war. Auf der linken Seite werden die verschiedenen Builds, sprich Ausführungen, aufgelistet mit der jeweiligen Build Nummer, dem Start Datum und der Anzahl an Commits in die Versionierung seit der letzten Ausführung.

Abbildung 7: Standardoberfläche eines Jenkins Pipeline Jobs

Neben der Standardoberfläche gibt es per Plugin [9] auch noch die extra komplett neu entwickelte Oberfläche namens Blue Ocean. Sie soll die User Experience von Jenkins deutlich steigern, indem sie folgende Hauptmerkmale mit sich bringt [10]:

  • Unterstützung von anspruchsvollen Visualisierungen von Continuous Delivery Pipelines
  • Intuitives Erstellen von Pipelines in einem visuellen Prozess durch den Pipeline Editor
  • Anpassungen an die rollenbasierten Anforderungen jedes Teammitglieds
  • Präzision bei der Lokalisierung von Problemen innerhalb einer Pipeline
  • Native Integration von Branches und Pull Requests für GitHub und BitBucket

Wird Blue Ocean genutzt, kann auf einen Blick gesehen werden, wo die Pipeline fehlerhaft war und welche Stages eventuell trotzdem danach noch ausgeführt wurden. Zudem sieht die Oberfläche deutlich „frischer“ aus und ist von Grund auf für Pipelines entwickelt worden.

Abbildung 8: Blue Ocean Oberfläche für einen Fehlschlag und Erfolg des gleichen Jobs

Fazit

Jenkins bietet mit den Pipeline Plugins und der Blue Ocean Oberfläche eine gute Basis für Continuous Delivery Pipelines. Nach einer kurzen Eingewöhnung an den Aufbau des Jenkinsfiles geht die Erstellung von Pipelines, durch den deklarativen Ansatz, einfach von der Hand. Sollte die DSL mal nicht ausreichen, existiert noch immer die Möglichkeit eigene Groovy Funktionen zu schreiben und einzubauen. Mit Blue Ocean bietet der Jenkins auch an der Oberfläche viele Funktionen und eine gute Übersicht, somit lassen sich Pipelines gut verwalten und Probleme leichter beheben.

Quellen und weitere Informationen

Dieser Artikel wurde in der Ausgabe 4/2019 im DOAG RedStack Magazin veröffentlicht.

Jetzt teilen auf:

Jetzt kommentieren