en
de

Jenkins Jobs as Code

28 December 2016
| |

Ich setze in Embedded Projekten sehr gerne die CI-Software Jenkins zur Qualitätssicherung ein. Insbesondere den leichten Zugang über das intuitive Web-Portal und der hohe Grad an Konfigurierbarkeit des Tools gefallen mir besonders. Ein großes Manko war bisher jedoch das recht umständliche Job- Management. An der clicky-bunty Java-Script-Anwendung führte quasi kein Weg sinnvoll vorbei. Die Jobs habe ich also manuell erstellt und dann auf dem Build-Server “vergessen”. Backups? Okay, die xml-Dateien aus dem Dateisystem ließen sich durchaus sichern. Aber Multi-Branch-Projekte? Mühsame Handarbeit. Als Infrastructre-As-Code-überzeugtem DevOps-Jünger war mir das immer ein Dorn im Auge.

Mit Groovy-DSLs zu Jobs-as-Code

Seit ein paar Monaten gibt es mit den Plugins Job DSL und Pipeline ein Heilsversprechen aus der Jenkins-Community, dass es diesem Dorn an den Kragen geht. Und nun hat sich sogar eine ruhige Projektphase ergeben, um die CI-Infrastruktur eines unserer langlaufenden Systemprojekte zu überarbeiten. Diese Chance habe ich nun genutzt, um die bisher manuell erstellten Jobs auf die beiden DSLs umzustellen. Im Folgenden werde ich beschreiben, welche Features ich wie eingesetzt haben und welche Fallstricke ich dabei bewältigen musste.

Die bestehenden Jobs folgen bereits einem in Zühlke Systemprojekten mehr oder weniger weit verbreiteten Muster:

  • Firmware Build (hier: GCC-ARM)
  • UnitTests (hier: GoogleTest)
  • statische Code-Analyse (hier: CppCheck)
  • Dokumentationsgenerierung (hier: Doxygen)
  • automatisierte HIL-Tests (hier: NI TestStand)

Die einzelnen Jobs laufen nach jeder Änderung in der Reihenfolge hintereinander durch, bisher ist das per Downstream-Trigger gelöst. Mit den beiden neuen DSLs habe ich nun mehrere Möglichkeiten, diesen Ablauf künftig As-Code abzubilden:

  • 1:1 Übersetzung der Jobs in DSL-Jobs (Job DSL).
  • Einen oder mehrere verkettete Pipeline-Jobs (Pipeline DSL).
  • Kombination aus isolierten, parametrierten DSL-Jobs (Job DSL) und notwendigen Pipelines (Pipeline DSL).

Flexibel und konsistent mit parametrisierten Jobs und Pipelines

Die direkte Übersetzung der Jobs nach Job DSL stellt die einfachste Variante dar. Allerdings bleiben bei dieser Lösung zwei bisherige Baustellen ebenfalls ungelöst. Ich kann zum einen Jobs nicht gezielt und isoliert starten, da der Downstream-Trigger letztendlich immer den (teuren) HIL-Test starten wird. Zum anderen ist eine konsistente Build-Kette nur mit extra Aufwand zu gewährleisten, etwa damit alle Jobs auf der gleichen Git-Revision arbeiten und Artefakte von den korrekten Upstream-Jobs bezogen werden.

Die zweite Variante führt intrinsisch zu einer konsistenten Build-Kette. Einzelne Schritte kann ich hier aber ebenfalls nicht isoliert starten. Sofern ich die komplette Pipeline in einen einzelnen Pipeline-Job migriere, verliere ich sogar die Möglichkeit die Pipeline erst ab “halber Strecke” laufen zu lassen. Das Debuggen der Job-Konfiguration selbst wird dadurch erheblich erschwert.

Daher habe ich mich nun auf die dritte Variante konzentriert. Ich habe entsprechend die bestehenden Jobs zunächst analog zu Variante eins in Job-DSL nachgebildet. Die API-Dokumentation der Job DSL ist bei dieser Aufgabe Gold wert. Damit lässt sich jeder (nicht allzu exotische) Freestyle-Job problemlos als Groovy-Script ablegen. Die Downstream-Trigger lasse ich bei der Migration allerdings aus. Stattdessen füge ich jedem Job entsprechende String- oder Text-Parameter hinzu, insbesondere einen Parameter GIT_COMMIT. Über diese Parameter kann ich die Abhängigkeiten aus der Job-Konfiguration selbst herauslösen, etwa welche Git-Revision oder welcher Upstream-Job verwendet werden soll.

Gluelogic Commit-Pipeline

Sobald die Jobs isoliert betrachtet korrekt arbeiten, wende ich mich der übergeordneten Pipeline zu. Für den Moment beschränke ich mich auf eine Commit-Pipeline, die die genannten Jobs nach Änderungen durchlaufen lässt. Nach selbigem Schema wären aber auch Nightly- oder Release-Pipelines denkbar. Die granularen Jobs können hier (auch mehrfach) in zueinander beliebiger Reihenfolge nach Bedarf kombiniert werden.

Für meine Commit-Pipeline erstelle ich ein Pipeline-Script mit den Stages

  • Changelog
  • Firmware
  • UnitTest
  • CppCheck
  • Doxygen
  • SmokeTest

Den Changelog-Stage benötige ich initial, um per Konsolenkommando git rev-parse HEAD die Git-Revision zu ermitteln, die in allen folgenden Stages verwendet werden soll. Diese Behelfslösung ist notwendig, da leider die von Freestyle-Jobs bekannten Git-Umgebungsvariablen in einem Pipeline-Job nicht verfügbar sind.

In den nachfolgenden Stages verwende ich das Pipeline-Kommando build job: '<XYZ>', parameters: [<..>], um die jeweiligen granularen Jobs anzustoßen. Die notwendigen Parameter für die einzelnen Jobs werden einfach in den Array parameters eingetragen, etwa string(name: 'GIT_COMMIT', value: gitCommit), um den String-Parameter mit der fixierten Git-Revision zu belegen.

Eine Besonderheit kommt für die SmokeTest-Stage hinzu, da der HIL-Job ein Artefakt des Firmware-Jobs benötigt. Damit die Konsistenz der Build-Kette gewahrt bleibt, muss der HIL-Job das Artefakt aus dem zuvor gelaufenen Firmware-Job beziehen, und nicht etwa einfach aus dem Latest Stable Build. Der HIL-Job hat zu diesem Zweck einen String-Parameter FIRMWARE_BUILD und bezieht per CopyArtifact die Firmware-Binaries mit dem buildSelector { buildNumber('${FIRMWARE_BUILD}') }, also von dem numerisch referenzierten Build. Bei dem Build-Selector muss man auf die einfachen Hochkommata achten. Schreibt man stattdessen buildNumber("${FIRMWARE_BUILD}"), würde Groovy versuchen ${FIRMWARE_BUILD} mit einer gleichnamigen, lokalen Variable zu ersetzen, welche es vermutlich nicht gibt. Wir wollen aber exakt diesen Text in unserer Job-Konfiguration haben, damit die Variablen-Ersetzung erst später, zur Ausführungszeit der Pipeline, zum Tragen kommt.

Abschließend erstelle ich einen Pipeline-Job from SCM für die Commit-Pipeline, natürlich ebenfalls per Job DSL. Dieser Job ist nun der einzige, der mittels triggers zyklisch das Git-Repo überwacht und somit automatisch ausgeführt wird.

Einstiegspunkt Seed-Job

Damit diese, per Job DSL spezifizierten Jobs auch angelegt werden, wird noch ein Seed-Job benötigt. Also ein spezieller Job, der weiß welche Job-Spezifikationen berücksichtigt werden sollen. Sobald das Job DSL-Plugin installiert ist, gibt es dazu beispielsweise den Build-Step Process Job DSLs. Dort spezifiziert man per Pattern, wo in der Arbeitskopie die Job DSL Groovy-Scripte liegen. Jeder Build dieses Seed-Jobs erzeugt bzw. aktualisiert die spezifizierten Jobs.

Für einen einzelnen Branch im Projekt funktioniert das bis hier gefühlt sehr gut, und deutlich besser als über das Web-UI handgepflegte Jobs. In vielen Projekten, wie auch im vorliegenden, bleibt es aber selten bei einem einzelnen Branch. Bisher haben wir uns nur nach reiflicher Überlegung die Mühe gemacht für Branches ebenfalls eine Build-Kette aufzusetzen. Mit dem hier gezeigten Vorgehen sind jedoch die Voraussetzungen geschaffen auch diesen nächsten Schritt zu automatisieren. Im Rahmen eines weiteren Blog-Beitrages werde ich dann von meinen Erfahrungen zu Multi-Branch-Pipelines berichten.

Comments (0)

×

Updates

Schreiben Sie sich jetzt ein für unsere zwei-wöchentlichen Updates per E-Mail.

This field is required
This field is required
This field is required

I'm interested in:

Select at least one category
You were signed up successfully.