Jenkins

Einsortieren:

Jenkins wird zur Automatisierung von Builds und Deployments verwendet.


TLDR;

Die Idee von Jenkins ist prinzipiell gut ... aber ob ich wirklich Jenkins nehmen würde, wenn ich die freie Wahl hätte - ich denke nicht.

Das gute zuerst: Es gibt unzählige Plugins für Jenkins und eine große Community - insofern schießt man sich nicht ins Abseits, wenn man sich dafür entscheidet. Groovy macht die Möglichkeiten THEORETISCH grenzenlos, praktisch setzt das fehlende Tooling feste Grenzen.

ABER:

  • die Web-UI ist fürchterlich - unintuitiv und buggy
  • die Pipline-Sprache existiert als declarative und scripted Variante - schaut man in Foren, um das eigene Problem zu lösen ist es Laien anfangs meist nicht klar welche Variante in der Lösung beschrieben ist. Zudem ist die Developer Experience sehr bescheiden - wie soll man vernünftig an den Pipelines arbeiten, wenn

    • es keine ordentliche Entwicklungsumgebung (Auto-Vervollständigung, Debugging) gibt. Die Pipelines müssen aufgrund der fehlenden Entwicklungsumgebung immer erst mal ins Remote-Git-Repository gebracht werden (commit, push), um dann den Job zu triggern
    • die Fehlermeldungen sehr kryptisch sind

      ich habe mal einen Pipeline-Parameter my-param genannt und wollte ihn per ${params.my-param} referenzieren ... ging nicht - die Fehlermeldung war nichtssagend. Die Doku paßte zu meiner Nutzung und deshalb habe ich den Fehler in dem eingebetteten Kontext gesucht. Zwei Stunden später hat es dann durch eine Umbenennung der Parameter-Variable von my-param zu my_param funktioniert. Ich liebe es.

    • der Workaround über Jenkins Pipeline Syntax Generator funktioniert nicht gut

  • das Pipeline-Konzept und die Jobs passen nicht zueinander .. ich muß einen Job anlegen, um daran eine Pipeline zu referenzieren, die genau diesen Job beschreiben soll. Parameter kann ich aber beispielsweise im Job UND in der Pipeline definieren ... was zieht nun?
    • aus diesem Grund hat man gelegentlich auch das Henne-Ei-Problem wie hier
      • ich definiere einen Job, der die Pipeline (mit Build-Parameters) aus dem GIT-Repo holt
      • beim ersten Start kann ich keine Build-Parameter eingeben, weil Jenkins noch gar nicht weiß, daß es welche gibt
      • beim zweiten Start "kennt" Jenkins dann die Pipeline und bietet mir beim Start Build-Parameter zur Auswahl an
      • ... fühlt sich gar nicht gut ...

Fazit: die Entwicklung von Pipelines verkommt zum nervenaufreibenden Trial-and-Error mit echo Statements, bei dem man nie den Eindruck hat, die Sprache zu beherrschen.


Getting Started

Start über java jar

Jenkins wird einfach per

java -jar jenkins.war --httpPort=8080

gestartet. Selbst mit Docker ist es aufwendiger (zumindest, wenn man Docker erst noch installieren muß).

Installation via Docker

Diese Variante ist die schnellste, wenn man bereits Java 9 oder höher installiert hat, denn

"You will need to explicitly install a Java runtime environment, because Jenkins does not work with Java 9" (Doku)

Docker Jenkins Agents können in dieser Variante evtl. auch verwendet werden ... mit dem Docker-in-Docker Ansatz.

docker run \
    -u root \
    --rm \
    --name jenkins \
    -d \
    -p 8080:8080 \
    -p 50000:50000 \
    -v /tmp/jenkins-data:/var/jenkins_home \
    -v /var/run/docker.sock:/var/run/docker.sock \
    jenkinsci/blueocean

Beim Start wird das admin Password im Log-Output angezeigt - mit diesem Passwort meldet man sich an der Jenkins UI an. Man kann es aber auch über

docker exec -it jenkins cat /var/jenkins_home/secrets/initialAdminPassword

ausgeben.

Anschließend kann man über die UI noch Plugins installieren (optional) und landet in der Classic UI. Die BlueOcean UI ist über eine andere URL verfügbar

Es gibt verschiedene Docker Images:

  • jenkins: Raw Installation
  • jenkinsci/blueocean: Long-Term-Support Jenkins Version mit einigen vorinstallierten Blue-Ocean Plugins ... über http://localhost:8080/blue/ kann man ohne die übliche Plugin-Selektion direkt loslegen
    • in diesem Buch als recommended eingestuft und detailliert beschrieben

Installation via Ubuntu Package Manager

Wie in der Doku beschrieben

wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
sudo echo "deb https://pkg.jenkins.io/debian binary/" >> /etc/apt/sources.list
sudo apt-get update
sudo apt-get install jenkins

Über http://localhost:8080 ist das Jenkins UI erreichbar, über das man nach der Installation noch ein paar Basis-Konfigurationen vornehmen muß. U. a. kann man hier einen ersten User anlegen (zusätzlich zum admin User, dessen initiales Passwort man in /var/lib/jenkins/secrets/initialAdminPassword findet).

Manage Jenkins

sudo service jenkins status
sudo service jenkins stop
sudo service jenkins start

Problem - No Java executable found

Auf meinem System schlug der Start fehl mit

No Java executable found in current PATH

Das Problem ist das Jenkins Startup Script /etc/init.d/jenkins, das folgende Zeile aufweist

PATH=/bin:/usr/bin:/sbin:/usr/sbin

Mein Java befindet sich aber in /usr/lib/jvm/java. Als Workaround habe ich einen Link ln -s /usr/lib/jvm/java/bin/java /usr/sbin/java angelegt - ich wollte das Script nicht verändern, da es evetntuell über ein Paket-Update wieder überschrieben wird.


Konzepte

Konzepte einer Jenkins-Pipeline

Jobs

Jobs stellen den Trigger zur Ausführung von Pipelines zur Verfügung. Jobs werden entweder manuell oder automatisch (z. B. beim Commit in einen Source-Branch - aka "SCM polling trigger") gestartet. Sehr praktisch ist, daß man Jobs auch mit einem API-Token per HTTP-Request triggern kann. Auf diese Weise kann man jederzeit bequem einen Jublauf triggern.

Es gibt verschiedene Job-Typen:

  • Maven Projekt
  • Pipeline
    • ein Pipeline-Job enthält entweder eine Pipeline-Definition oder referenziert ein Jenkinsfile über ein VCS-Repository (SVN, GIT).
  • Multibranch Pipeline
    • häufig bei Build-Jobs verwendet, wenn Feature-Branches verwendet werden ... in dem Fall soll jeder Branch automatisch bei einem Commit gebaut werden
  • ...

Pipeline

Eine Pipeline beschreibt eine Vielzahl von Schritten ... im Extremfall vom VCS checkout, über Bauen der Artefakte, über das Deployent auf verschiedenen Stages bis zum Smoke-Test auf dem Live-Deployment im Stile eines Canary Releases.

Das semantische Konzept existiert in Jenkins schon von Anfang an, aber erst in 2.0 wurde die Pipeline explizit als Beschreibungsform eingeführt.

Groovy war von Beginn an DIE Scriptsprache für Jenkins, um Entwicklern mehr Freiheiten zu geben. Deshalb ist Groovy auch die Basis für die Definition von Pipelines in einem Textfile (z. B. Jenkinsfile). Dieser Code ("Infrastructure-as-Code) wird genauso behandelt wie Source-Code (unter Versionskontrolle). Damit ist das Git-Repository eines Services noch weiter self-contained, denn es enthält auch seinen eigenen Build-Job (ähnlich kann man das bei anderen Build-Tools wie beispielsweise Travis). In Jenkins muß dann nur noch ein Job angelegt werden, in dem die Location des Git-Repositories definiert wird, und angegeben wird, daß das Repository ein Jenkinsfile enthält.

Man unterscheidet zwei Arten von Pipeline Definitionen:

  • Declarative Pipeline
    • durch eine abstrakte DSL (Groovy-basiert) einfacher zu nutzen
    • der Blue Ocean Pipeline Editor hilft bei der Erstellung einer Pipeline
    • dieser Pipeline Typ wurde nach den Scripted Pipelines zur Vereinfachung eingeführt
  • Scriped Pipeline
    • mächtiger als die deklarative Pipeline, da hier richtig programmiert werden kann
      • Exception Handling
      • Kontrollstrukturen (if, ...)
    • es existiert sogar ein eigenes Ökosystem, um die Effizienz durch Reuse zu erhöhen - Nutzung/Erstellung von Shared Libraries

Am besten startet man einen lokaken Jenkins und legt einen Pipeline-Job an. Dann kann man sich Pipeline-Code über "Pipeline Syntax" generieren lassen.

Leider ist das kein schöner Entwicklungszyklus ... eher Trial-and-Error.

Trigger

Typischerweise triggert man einen Build nach einem SCM-Commit - hierzu muß man bei Jenkins Build Triggers - Poll SCM konfigurieren.

Sehr praktisch sind allerdings auch Web-Hooks (Build Triggers - Trigger builds remotely), so daß man Build von außen explizit triggern kann. Auf diese Weise kann auch eine Integration anderen Tools erfolgen.

Außerdem gibt es in einem Jenkins-Job auch Post-Build Actions.

Agent

Jenkins startet Aufgaben (Projekte, Pipelines) und braucht hierzu eine Laufzeitumgebung. Die Laufzeitumgebung nennt man Agent ... es werden verschiedene Typen unterstützt:

  • Docker
  • Jenkins-Knoten

Man kann den Agent an verschiedenen Stellen definieren:

  • Pipeline
  • Stage einer Pipeline
  • ...

Classic UI vs. Blue Ocean

Blue Ocean ist das neuere User-Interface, das parallel zur Classic UI existiert.


Global Tools

Nicht alle Tools werden über Plugins integriert. Tools wie

  • JDK
  • Git
  • Ant
  • Maven
  • Packer
  • Terraform
  • Docker
  • ...

werden über "Jenkins - Global Tool Configuration" konfiguriert.


Benutzer

Es stehen verschiedene Möglichkeiten der Userverwaltung zur Verfügung (konfiguriert in "Jenkins - Configure Global Security"):

  • Jenkins-Userverwaltung
  • Active Directory

Berechtigung können auf verschiedenen Granularitätsstufen vergeben werden

  • Anyone can do anything
  • Logged-in users can do anything
  • Legacy Mode
  • Matrix-based security
  • Project-base Matrix Authorization Strategy

Shared Libraries

Jenkinsfiles werden aufgrund des Ansatzes einen Service als Single-Source-of-Responsibility zu betrachten, der alle Aspekte seines Lifecycles selbst managed, in das Microservice Repository gepackt. Wenn die Pipelines dann allerdings komplexer werden, dann kann die Pflege (Bugfixing, Extensions) aufwendig werden, wenn die Jenkinsfile der Services i. a. sehr ähnlich aussehen.

In diesem Fall kann man Groovy Shared Libraries verwenden, um die Pflege zu zentralisieren. Das Jenkinsfile sieht dann so aus:

#!/usr/bin/env groovy
@Library('my-jenkins-library') _
microservice-pipeline('docker')

Über Parameter können verschiedene Ausprägungen adressiert werden.

In der Jenkins-Server Konfiguration muß die my-jenkins-library unter Manage Jenkins - Configure System - Global Pipeline Libraries eingebunden werden. Hier gibt man beispielsweise ein Git-Repository an und kann eine Default-Version definieren, die je nach Konfiguration im Jenkinsfile überschrieben werden kann:

  • Tag referenzieren: @Library('[email protected]
  • Branches referenzieren: @Library('my-jenkins-library@feature/my-first-scripted-pipeline')

ACHTUNG: das override muß in der Konfiguration hierzu explizit erlaubt werden (Allow default version to be overridden)!!! Ist das nicht erlaubt und es wird eine spezielle Version referenziert, dann schlägt die Ausführung fehlt (kein Failover).


Jenkins CLI

Zur Automatisierung von Aufgaben ist die Jenkins CLI gedacht, die per

java -jar jenkins-cli.jar -s http://localhost:8080/ help

aufgerufen werden kann. Für die meisten Aufufe benötigt man allerdings besondere Berechtigungen. Hierzu muß zum User ein API-Token über das Jenkins User-Management erstellt werden:

Jenkins API-Token

Am besten packt man diesen insgesamt länglichen Aufruf in ein Shell-Skript jenkins-cli.sh (das sich im PATH befindet) und spendiert noch einen alias jenkins=jenkins-cli.sh

#!/bin/bash

_user=pfh
_apiKey=112a010e3c1408a97e38fb5d50fd79d0bf

java -jar ~/programs/jenkins/jenkins-cli.jar -s http://${_user}:${_apiKey}@localhost:8080/ $@

so daß man es dann bequem von überall per jenkins version aufrufen kann.


Plugins

Jenkins zeichnet sich gegenüber anderen Build-Tools dadurch aus, daß es eine Fülle an Plugins gibt.

AnsiColor Plugin

  • so sind die Logfiles leichter zu lesen

Office 365 Connector Plugin

Mit diesem Plugin klappen auch Microsoft Teams Notifications aus Jenkins Pipelines heraus.


Meine erste Pipeline from-scratch

Ich habe bereits eine Jenkins Infrastruktur und dort laufen auch schon Builds basierend auf einer Scripted Shared Library. Das will ich lokal mit einem frisch installierten Jenkins nachbauen.

Mein erster Service, den ich bauen will stellt im GIT-Repository ein Jenkinsfile bereit (da ich meinen Code und meine Deployment-Scripte beisammen und unter Versionskontrolle stellen möchte). Ich erstelle eine erste Pipeline und benötige dazu:

  • Repository URL
  • Repository Credentials: hierzu verwende ich den Jenkins Credentials Provider, der im Dialog angeboten wird. Hierzu hat mir der GIT-Repo-Admin einen User und dessen private Key mitgeteilt (Login-Option: SSH Username with private key)
  • Jenkins Library: in meinem Jenkinsfile verwende ich eine Custom-Pipeline-Bibliothek (zwecks Wiederverwendung in verschiedenen Komponenten), die Jenkins noch nicht kennt. Deshalb muß ich die Bibliothek zunächst unter Global Pipeline Libraries als GIT-Repository bekanntmachen ... hier kann man auch den zu verwendenden Branch konfigurieren (statisch oder dynamisch). Später wird in meinen Builds immer Loading library ... stehen - ACHTUNG: hier wird dann auch der Branch erwähnt, denn man kann verschiedene Branches
  • ich benötige noch Maven, dessen Konfiguration und ein Plugin

    • über Global Tool Configuration (ACHTUNG: dieser Dialog ist sehr verwirrend ... man kann hier die Maven/JDK Installationen des Systems wiederverwenden und muß keine Neuinstallation machen - Install automatically darf nicht ausgewählt sein)!!!) integriere ich die auf meinem System bereits installierte Version, die ich M3 nenne (weil ich Maven 3.5.2 installiert habe):

      Maven Global Tool Config

      in meiner Pipeline werde ich dieses Tool folgendermaßen referenzieren:

      ```groovy stage('Compile') { withMaven(

      // named Maven installation declared in the Jenkins "Global Tool Configuration"
      maven: 'M3'
      

      ) {

      sh "mvn clean compile -U"
      

      } }

      ich präferiere Docker Container, um diese Tools on-the-fly in der richtigen Version bereitzustellen - jetzt arbeite ich erstmal mit den bereits installierten Tools

    • die Konfiguration wird über Global Tool Configuration durchgeführt ... ich verwende meine lokale /home/pfh/.m2/settings.xml
    • das Plugin Pipeline Maven Integration wird über die UI installiert
    • in meiner Pipeline-Library verwende ich das Plugin Pipeline Utility Steps, um die POM zu lesen (readMavenPom) - wird über die UI installiert
  • statt der Wiederverwedung von Maven/JDK von meinem Host-System möchte ich Docker-Container verwenden. Deshalb schreibe ich die Scripted Pipeline folgendermaßen um (siehe Dokumentation:

    docker
      .image('maven:3.3.9-jdk-8-alpine')
      .inside('-v /tmp/maven:/usr/share/maven/ref/ -u root') {
      stage('Compile') {
          sh "mvn clean compile -U"
      }
    }
    

Optimierung 1: JDK und Maven Builds im Docker Agent

Builds in Docker Agents abzubilden hat den Charme, daß die Builds komplett separiert werden. Es kann nicht mehr vorkommen, daß ein Test einen Port öffnen will, der schon von einem parallel laufenden Test geöffnet wurde.

"As a side note, I would suggest attaching agents to that master. It is not recommended to run jobs inside a master." (Automating Jenkins Docker Setup)

Da der User jenkins keine docker Kommandos ohne sudo ausführen kann, füge ich ihn zur Gruppe docker hinzu: usermod -aG docker jenkins. Anschließend muß ich noch den Jenkins-Service restarten, damit die neuen Gruppenzuordnungen auch ziehen.

Die ersten Versuche waren leider wenig erquickend und ich muß gestehen, daß sich die Fehlersuche sehr schwierig gestaltet. Docker selbst ist bei nicht startbaren Docker COntainern ja schon recht schwierig zu analysieren. Dadurch, daß der Docker Container unter dem jenkins user gestartet wurde ist die Fehlersuche noch mal schwieriger. Ich hatte beispielsweise das Problem, daß die gemounteten Volumes (mit Maven settings.xml) nicht sofort zur Verfügung standen ... ich mußte per sleep 5 ein paar Sekunden warten. Solche Fehler sind natürlcih die Hölle. Irgendwann schaffe ich es dann aber doch, meinen Code über den Docker Container zu bauen und zu testen.

So sah meine funktionsfähige Pipeline dann aus:

node {
  docker
    .image('maven:3.3.9-jdk-8-alpine')
    .inside(
        // provide shared Maven Repository => performance
            '-v /tmp/.m2:/tmp/.m2 '

        // provide Maven configuration => put it into customized Docker Image
        + '-v /tmp/maven:/usr/share/maven/ref/ '

        // -u root ... https://github.com/carlossg/docker-maven/issues/63
        + '-u root '

        + '--env MAVEN_OPTS="'

        //                  share Maven Repository => performance
        +                   '-Dmaven.repo.local=/tmp/.m2"') {

        stage('Checkout') {
            checkout scm
        }
        stage('Build') {
            sleep 5
            sh 'mvn clean compile -U'
        }
        stage('Test') {
            // workaround for surefire problem
            // ... https://stackoverflow.com/questions/46670582/docker-maven-failsafe-surefire-starting-fork-fails-with-the-forked-vm-termin
            // ... https://issues.apache.org/jira/browse/SUREFIRE-1422
            sh 'apk add --no-cache procps'

            sh "mvn verify"
        }
  }
}

Automatisierung einer Jenkins Installation - auf die harte Tour

Ich konnte nun zwar die Konfiguration von JDK und Maven über Docker Container loswerden, doch die Installation von

  • Plugins

und die Konfiguration von

  • Admin Password
  • GIT Credentials
  • Shared Libraries
  • Jobs
  • Usern

ist weiterhin manuell durchzuführen.

Jenkins verwaltet diese Konfiguration in XML Dateien (/var/lib/jenkins/credentials.xml), die mir schwierig migrierbar erscheinen (plugin="[email protected]"). Die Pflege dieser Dateien bei Updates scheint mir keine Freude zu sein.

Welche Alternativen sind möglich?

  • Installation per Ansible
  • Dockerisierung eines vorkonfigurierten Jenkins
  • REST API nutzen
  • Groovy Schnittstelle nutzen ... alle -Skripte in ${JENKINS_HOME}/init.groovy.d werden beim Start idempotent ausgeführt.

Optimierung 2: Jenkins als Docker Container

Diese Option hätte den Charme, daß man alles vorkonfigurieren könnte, komplett ohne weitere Infrastruktur auf dem Jenkins-Host (Ansible hat auch Anforderungen). Jeder, der Docker nutzen kann, könnte dann auch einen fertig konfigurierten Jenkins-Server starten.

Option 1:

Im Docker Container läuft ein eigener Docker Host (Parent-Mode).

"The answer is yes, but it is not recommended because it causes many low-level technical problems, which have to do with the way Docker is implemented on the operating system, and which are explained in detail in Jérôme Petazzoni’s post." (Can you run Docker inside a Docker container?)

Option 2:

In dem Fall sind die Docker Container

"The good news is that there is another, recommended, way to use Docker inside a Docker container, with which the two Docker instances are not independent from each other, but which bypasses these problems. With this approach, a container, with Docker installed, does not run its own Docker daemon, but connects to the Docker daemon of the host system. That means, you will have a Docker CLI in the container, as well as on the host system, but they both connect to one and the same Docker daemon. At any time, there is only one Docker daemon running in your machine, the one running on the host system. To achieve this, you can start a Docker container, that has Docker installed, with the following bind mount option: -v /var/run/docker.sock:/var/run/docker.sock" (Can you run Docker inside a Docker container?)

Genau das habe ich mal ausprobiert. Jenkins per

docker run \
    -u root \
    --rm \
    --name jenkins \
    -d \
    -p 9090:8080 \
    -p 50000:50000 \
    -v /tmp/jenkins-data:/var/jenkins_home \
    -v /var/run/docker.sock:/var/run/docker.sock \
    jenkinsci/blueocean

gestartet und dann über Blue Ocean URL in die Classic UI gewechselt (Blue Ocean erlaubt keine Pipelines über die UI zu definieren), um diese Jenkins-Pipeline anzulegen:

pipeline {
    agent none
    stages {
        stage('Example Build') {
            agent {
              docker 'maven:3-alpine'
            }
            steps {
                echo 'Hello, Maven'
                sh 'mvn --version'
            }
        }
    }
}

Job ausgeführt ... funktioniert - per watch docker ps sehe ich während der Jobausführung auch den Docker-Agent :-)

Die Jobs (inkl. History) werden in /var/jenkins_home abgelegt, das in obigem Beispiel auf dem Docker Host (-v /tmp/jenkins-data:/var/jenkins_home) abgelegt wird. Auf diese Weise verliert man nichts, wenn man den Container löschen muß.

Automatisierung einer Jenkins Installation - auf die komfortable Tour

Development Environment

Eine lokale Jenkins Installation hat meine Developer Experience auf jeden fall schon mal verbessert - im Vergleich zu einem Shared-Jenkins-Installation. Doch leider muß ich immer noch immer committen, pushen und dann den Jenkins Job triggern. Um dann festzustellen, daß die Syntax im Jenkinsfile oder einer Shared-Library nicht stimmt. Das ist nicht besonders effizient :-(

Wie geht es besser?

  • Jenkins bietet auf einem gestarteten Jenkins Server ... leider nur Tooling für Declarative Pipelines
  • Blue Ocean Pipeline Editor unterstützt die Editierung von Jenkins Pipelines, doch will ich meine üblichen Editoren und Tools (Visual Studio Code, Git) verwenden, um die Dateien zu editieren.
  • Command-line Pipeline Linter
    • damit bin ich nicht zurecht gekommen
  • Visual Studio Code stellt folgende Plugins bereit:
    • Jenkins Pipeline Linter Connector
      • unterstützt nur deklarative Pipelines
      • muß folgendermaßen konfiguriert werden:
        "jenkins.pipeline.linter.connector.url": "http://localhost:8080/pipeline-model-converter/validate",
        "jenkins.pipeline.linter.connector.user": "pfh",
        "jenkins.pipeline.linter.connector.pass": "112a010e3c1408a97e38fb5d50fd79d0bf"
        
  • Pipeline Unit Testing Framework
    • das sollte man auf jeden Fall verwenden, um die Build-Infrastruktur zu testen!!!

Fazit:

"I hate to be the bearer of bad news, but the documentation is lacking (and often unspecific as to whether or not its examples are for scripted or declarative pipelines), the book barely grazes the subject (if you’ve found one that has better information please share), and piecing together bits of other peoples’ Github examples and Stack Overflow answers is nothing short of a painful slog. Even then, you’re just as likely (if not more!) to find declarative pipeline examples which, of course, have different syntax and abilities." (Hacky Hacker’s Guide To Hacking Together Jenkins Scripted Pipelines and Getting Them To Do Things)

results matching ""

    No results matching ""