Aus Linux-Magazin 04/2019

CI/CD-Pipeline für Deliveries verschiedener Betriebssysteme

© Konstantin Shaklein, forplayday; beide 123RF

Für die Mission, die der vorherige Artikel nachzeichnete, aus via Git verwaltetem Quellcode automatisch Deliveries zu veröffentlichen, wählt der folgende Beitrag eine alternative Route. Er koppelt Git, Jenkins, Docker und Github zu einer Buildchain, die sich flexibel ausbauen und modifizieren lässt.

Der Software-Entwickler steht heute vor einer übervollen Werkzeugkiste an Build- und Automatisierungstools, die helfen wollen, die Schritte von einer Änderung am Quellcode in Git bis zur Auslieferung des fertigen Produkts zu automatisieren. Die richtigen Tools auswählen und zu einer Kette schmieden, welche die eigene Arbeitsweise vollständig und zudem effizient abbildet, ist nicht leicht.

Wem eine vorgefertigte Dienste-Lösung wie das eben beschriebene Gitlab nicht reicht, weil deren Anpassungs- und Erweiterungsmöglichkeiten begrenzt sind, kommt mit der folgenden Anleitung zu einem lohnenden Ziel: eine Continuous-Integration-Installation (CI), die Jenkins [1] als Basismodul nutzt.

Damit gelingt es einem Team von Software-Entwicklern, die ihren Quellcode via Git verwalten, die im Projekt immer wiederkehrenden Schritte (Builds, Tests und Auslieferung) intelligent zu automatisieren. Die einzelnen Bestandteile hält das folgende Beispiel bewusst einfach. So bleiben genügend Stellen mit Verbesserungspotenzial je nach konkretem Anwendungsfall.

Mit der Release 2.x zog in Jenkins das Pipelining [2] offiziell als Bestandteil ein. Seither eröffnen sich völlig neue Möglichkeiten, auf welche Art und Weise sich ein Build beziehungsweise eine so genannte Build-Pipeline orchestrieren lässt. Außerdem steht mit Docker [3] ein mächtiges Tool bereit, um Anpassungen sehr schnell umzusetzen, die bei traditionellen Installationen erheblichen Wartungsaufwand bis hin zu Neuinstallationen notwendig machen.

Aufbau des Build-Environment

Im Folgenden soll eine Build-Pipeline entstehen, die Jenkins orchestriert und sich für den Build-Vorgang die Möglichkeiten von Docker zunutze macht. Es geht also vordergründig nicht explizit darum, Docker-Images zu erzeugen.

Jenkins orchestriert den gesamten Build. Um möglichst flexibel hinsichtlich der Aktualisierung von Jenkins selbst zu bleiben, verwendet das vorgestellte Setup das offizielle Jenkins-Docker-Image [4]. Somit bedarf es für den Jenkins-Master lediglich einer Maschine, auf der der Docker-Daemon läuft. Der große Vorteil des Docker-Image ist, dass sich das Jenkins-Homeverzeichnis auf ein Docker-Volume auslagern lässt. Damit gehen keine Settings et cetera verloren, wenn es nötig wird, Jenkins selbst oder das Jenkins-Docker-Image zu aktualisieren.

Bei den Maschinen, die das Beispiel als Slaves für die Linux-Builds benutzt, spielt es keine Rolle, ob es dedizierte Server, VMs oder gar VMs bei einem der Cloudanbieter sind. Damit Jenkins eine Maschine als Slave verwenden kann, muss diese lediglich mit einem SSH-Zugang sowie einer Java-8-Runtime ausgestattet sein. Die Builds selbst machen wiederum von Docker Gebrauch, sodass auf den Slaves zusätzlich Docker installiert sein muss.

Ein Apple-Buildslave ist hinsichtlich der Jenkins-Integration ähnlich einer normalen Linux-Maschine zu konfigurieren, braucht also ebenfalls SSH-Zugang sowie eine installierte Java-8-Runtime. Wer für den Build Docker benötigt, muss dieses dort explizit installieren. Mac-spezifische Builds erfolgen aber in der Regel direkt auf der Maschine, sodass der Admin die dabei erforderliche Software auf dem Gerät einzurichten hat.

Wie immer bedarf Windows einer extra Behandlung. In Kurzform sieht die Umsetzung so aus, dass sich die Windows-Slaves via JNLP verbinden und die jeweils benötigte Toolchain auf der Windows-Maschine installiert sein muss. Details zur Anbindung solcher Slaves sind beispielsweise unter [5] zu finden.

Builder-Images

Um das Resultat für verschiedene Linux-Zielsysteme zu bauen, bietet sich eine schlaue Verwendung von Docker an. Die Idee ist, jeweils ein Docker-Image des entsprechenden Zielsystems zu verwenden und in einer Instanz dieses Images zu bauen. Im Anschluss an den Build exportiert das System das Resultat aus der Instanz auf geeignete Art und Weise. Als Delivery entsteht also kein Docker-Image, sondern ein Zielsystem-spezifisches Binary.

Das Ziel des vorgestellten Setup ist eine Cmake-basierte Hello-World-Applikation. Das zugehörige Dockerfile für einen Debian-Builder dokumentiert Listing 1. Das hiermit entstehende Binary dient natürlich nur als Beispiel, da es als Stand-alone-Binary nicht an ein konkretes Linux-System gebunden ist. Daher funktioniert in diesem Fall das für Fedora gebaute Binary genauso problemlos unter Debian – was in der Praxis selten ist.

Listing 1

Dockerfile Debian-Builder

01 FROM debian:stretch
02 MAINTAINER Yves Schumann <yves@eisfair.org>
03
04 ENV WORK_DIR=/data/work \
05     DEBIAN_FRONTEND=noninteractive \
06     LC_ALL=en_US.UTF-8
07
08 # Mount point for development workspace
09 RUN mkdir -p ${WORK_DIR}
10 VOLUME ${WORK_DIR}
11
12 COPY Debian/stretch-backports.list /etc/apt/sources.list.d/
13 COPY Debian/testing.list /etc/apt/sources.list.d/
14
15 RUN apt-get update -y \
16  && apt-get upgrade -y
17
18 RUN apt-get install -y \
19     autoconf \
20     automake \
21     build-essential \
22     ca-certificates \
23     cmake \
24     curl \
25     g++-7 \
26     git \
27     less \
28     locales \
29     make \
30     openssh-client \
31     pkg-config \
32     wget
33
34 # Set locale to UTF8
35 RUN echo "${LC_ALL} UTF-8" > /etc/locale.gen \
36  && locale-gen ${LC_ALL} \
37  && dpkg-reconfigure locales \
38  && /usr/sbin/update-locale LANG=${LC_ALL}

Uploader-Image

Nachdem der Build abgeschlossen ist, befindet sich das Delivery noch innerhalb des laufenden Containers. Wer das Resultat auf Github veröffentlichen will, braucht einen geeigneten Mechanismus, um diesen Upload-Schritt zu automatisieren. Hier bietet sich das Github-Projekt Aktau/Github-release [6] an, eine Go-Applikation, die hilft beliebige Artefakte auf Github zu veröffentlichen.

Um das Tool in den Buildprozess zu integrieren, spielen abermals Docker und dessen breite Basis fertiger Images die Hauptrollen. Für das Uploader-Image kommt das auf Alpine Linux basierende »golang«-Image zum Einsatz. Dort hinein installiert Zeile 7 des Dockerfile in Listing 2 das »github-release«-Tool.

Listing 2

Dockerfile github-release-Tool

01 FROM golang:1.11.5-alpine3.8
02 MAINTAINER Yves Schumann <yves@eisfair.org>
03
04 # Install git, show Go version and install github-release
05 RUN apk add git \
06  && go version \
07  && go get github.com/aktau/github-release
08
09 # Create mountpoint for file to upload
10 RUN mkdir /filesToUpload
11
12 # Just show the help to github-release if container is simply started
13 CMD github-release --help

Jenkins-Master einrichten

Am einfachsten ist der Jenkins-Master aufzusetzen, der auf dem offiziellen Docker-Image fußt. Dazu führt der Admin auf dem Host, der den Jenkins-Master beherbergen soll, das Kommando

docker run --name jenkins -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 jenkins/jenkins:latest

aus. Unmittelbar danach sollte er sich mit »docker logs -f jenkins« das Log ausgeben lassen, um das initiale Admin-Passwort zu erhalten:

[...]
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
12345678901234567890123456789012
This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
[...]

Wie zu sehen ist, liegt das Passwort zudem im Container unter dem angegebenen Pfad zur Ansicht bereit:

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

Die Datei bleibt bestehen, bis das initiale Setup beendet ist. Genau dies verfolgt der Admin nun weiter, wenn er die Jenkins-URL im Browser aufruft. Am einfachsten gelingt es mit »http://Host-IP:8080/« direkt auf den Jenkins-Master, da das vorgestellt Setup vereinfacht ist und weder ein HTTPS-Setup noch eine Port-Konfiguration umfasst.

Die folgenden Schritte richten den Default-Admin ein und installieren die empfohlenen Plugins. Danach ist die Standard-Oberfläche von Jenkins erreichbar und die grundsätzliche Installation des Jenkins-Master abgeschlossen.

Als Vorbereitung hinterlegt der Admin die Credentials, die Jenkins für den Zugriff auf Github sowie Dockerhub benötigt. Die Credentials sind vom Typ Username/Passwort und eröffnen die Zugänge zum jeweiligen API. Später werden die Zugriffe auf Github auch via SSH erfolgen, weshalb der Admin einerseits den entsprechende Private-Key in einem eigenen Credential-Eintrag in der Form »SSH Username with private key« hinterlegen und andererseits das SSH-Agent-Plugin installieren muss.

Bei einer Github-Organisation empfiehlt es sich, einen separaten CI-Account einzurichten und dort den zugehörigen Public-Key zu hinterlegen. Bei einem persönlichen Konto muss dessen Inhaber den Public-Key zum eigenen Profil hinzufügen.

Build-Slaves einrichten

Um Linux- oder Mac-Builder anzubinden, muss der Admin einen Build-Node mit der Launch Method »Launch Slave Agents via SSH« konfigurieren. Hierfür braucht er auf den Slaves einen Account mit SSH-Zugang und eingerichtetem Key-Login. Auf dem Master hinterlegt er in der Credentials-Konfiguration den zu verwendenden Private-Key.

Der Account auf dem Slave braucht im Normalfall keine Admin-Rechte, muss aber Docker-Kommandos ausführen dürfen. Wichtig ist außerdem, dass die Linux-Slaves mit dem korrekten Label versehen sind – hier: »docker«. Das stellt sicher, dass die puren Docker-Builds explizit auf den Linux-Slaves starten.

Einen Windows-Slave bindet der Admin am einfachsten mit der Launch Method »Launch agent via Java Web Start« ein. Details dazu beschreibt [7].

Github und Jenkins koppeln

Ein wichtiger Punkt der Build-Automatisierung ist das Verbinden von Jenkins mit den Repositories auf Github. An dieser Stelle bietet sich an, einen so genannten Organization Scanner zu benutzen (Abbildung 1). Für einen ersten Wurf reichen die Default-Einstellungen des Organization Scanner. Der Admin braucht lediglich die Zugriffs-Credentials sowie unter »Owner« die Github-Organisation beziehungsweise den eigenen Github-Benutzernamen zu hinterlegen.

Der große Vorteil eines Scanners ist, dass es reicht, in ihm das Github-Projekt oder den Github-User zu konfigurieren. Der Organization Scanner findet automatisch die Git-Repositories dieser Organisation respektive des Benutzers und analysiert deren Branches auf das Vorhandenseins eines Jenkinsfile. Ist eine Datei »Jenkinsfile« tatsächlich vorhanden, legt der Scanner automatisch einen passenden Buildjob an.

Damit braucht der Admin für weitere Git-Repositories selbst nicht mehr Hand am CI-System anzulegen, da die Buildjobs vollautomatisch entstehen. Wird ein Branch nach einem erfolgreichen Pull-Request gelöscht, verschwindet natürlich auch der zugehörige Buildjob wieder.

Abbildung 1: Der Startpunkt, um in Jenkins einen Github Organization Scanner anzulegen.

Abbildung 1: Der Startpunkt, um in Jenkins einen Github Organization Scanner anzulegen.

Nun die Build-Images bauen

Im ersten Schritt baut der Admin die Images, die der eigentliche Build des Delivery braucht. Es liegt nahe, das ebenfalls via Jenkins vollautomatisch zu erledigen. Dazu versioniert man die Dockerfiles in eigenen Git-Repositories. Zudem bekommt jedes der Repositories ein so genanntes Jenkinsfile ins Rootverzeichnis, das den Build des Image definiert. Das Git-Repository für den Uploader ist unter [8] zu finden, das Repository für die Builder unter [9] und die damit korrespondierenden Docker-Repositories unter [10] bis [12].

Dank des im vorherigen Schritt eingerichteten Organization Scanner findet das System diese Repositories automatisch und richtet entsprechende Buildjobs ein. Der Scanner legt außerdem die Hooks auf Github an, sodass die Builds bei einem Push getriggert werden. Damit entfällt die Notwendigkeit, die Repositories periodisch auf Änderungen zu pullen. Dazu muss der verwendete Account natürlich geeignete Repository-Permissions besitzen sowie die Jenkins-Instanz von Github ausgehend erreichbar sein.

Sind die Jobs erfolgreich durchgelaufen, stehen ab jetzt die für den eigentlichen Build der Deliveries benötigten Images auf Dockerhub bereit.

Build der Projekt-Deliveries

Nun ist das Setup so weit gediehen, dass das CI-System für seine eigentlichen Aufgaben bereit ist: den wiederkehrenden automatischen Build der Projekt-Deliveries mit allen notwendigen Details sowie deren Upload zu Github. Das Hauptrepository des Projekts hat im Rootverzeichnis ein Jenkinsfile, das den Build definiert. Der Ablauf eines Build aus Sicht von Jenkins ist somit identisch zum Build der bereits beschriebenen Hilfs-Repositories für die Builder-Images sowie den Github-Uploader. Der Build der Linux-Binaries erfolgt in Docker-Instanzen, die auf den vorbereiteten Builder- und Uploader-Images basieren. Der so genannte Multistage-Build der Debian-Variante ist in Listing 3 zu sehen.

Der spannende Teil ist nun jedoch, das Projekt-Jenkinsfile möglichst effizient aufzubauen. In der Praxis fallen dem Admin nämlich Funktionalitäten auf, die er in fast jedem Jenkinsfile aufs Neue definieren müsste. Es ist wesentlich effizienter, diese in separate Pipeline-Funktionen auszulagern und in Jenkins global zur Verfügung zu stellen. Somit braucht er den Code nur an einer Stelle zu pflegen, und alle Pipelines profitieren davon.

Listing 3

Multistage-Dockerfile des Applikationsbuild

01 ### At first perform source build ###
02 FROM starwarsfan/cmake-builder-debian:1.0 as build
03 MAINTAINER Yves Schumann <yves@eisfair.org>
04
05 COPY . /demo-app
06
07 RUN mkdir /demo-app/build \
08  && cd /demo-app/build \
09  && cmake .. \
10  && cmake --build .
11
12 RUN echo "Build is finished now, just execute result right here to show something on the log:" \
13  && /demo-app/build/hello-world
14
15 ### Now upload binaries to GitHub ###
16 FROM starwarsfan/github-release-uploader:1.0
17 MAINTAINER Yves Schumann <yves@eisfair.org>
18
19 ARG GITHUB_TOKEN=1234567
20 ARG RELEASE=latest
21 ARG REPOSITORY=cmake-buildtest
22 ARG GIT_COMMIT=unknown
23 ARG REPLACE_EXISTING_ARCHIVE=''
24 ENV ARCHIVE=cmake-buildtest-${RELEASE}-${GIT_COMMIT}-Debian.tgz
25
26 RUN mkdir -p /filesToUpload/usr/local/bin
27
28 # Maybe copy more from the builder...
29 COPY --from=build /demo-app/build/hello-world /filesToUpload/usr/local/bin/hello-world
30
31 RUN cd /filesToUpload \
32  && tar czf ${ARCHIVE} . \
33  && github-release upload \
34         --user starwarsfan \
35         --security-token "${GITHUB_TOKEN}" \
36         --repo "${REPOSITORY}" \
37         --tag "${RELEASE}" \
38         --name "${ARCHIVE}" \
39         --file "/filesToUpload/${ARCHIVE}" \
40         ${REPLACE_EXISTING_ARCHIVE} \
41  && export GITHUB_TOKEN=---

Hilfsfunktionen zum Release-Handling

Das »github-release«-Tool als Docker-Image leistet beim Handling der Releases auf Github wie gezeigt wertvolle Dienste, die viel vereinfachen. Gleichwohl lassen sich die Funktionen noch weiter kapseln. Beispielhaft ist das im Ansatz im Repository »jenkins-pipeline-helper« [13] zu sehen. Dort sind die Funktionen von »github-release« in Pipeline-Funktionen gekapselt und lassen sich innerhalb der Pipeline direkt mit Namen aufrufen. Im Jenkinsfile, um eine Github-Release anzulegen, spiegelt sich das dann wie in Listing 4 wider. Als Gimmick können die Release-Funktionen zwei verschiedene Varianten der Release-Beschreibung verarbeiten.

Wer den Parameter »description« mit Text befüllt, dessen Jenkins wird diesen Text als Release-Beschreibung verwenden. So bekommen die Develop-Builds lediglich die Buildnummer übergeben:

RELEASE_DESCRIPTION = "Build ${BUILD_NUMBER}"

Übergibt man aber einen Dateinamen, kommt deren Inhalt zum Einsatz. Das ist im Beispiel bei den Builds vom Master-Branch der Fall:

RELEASE_DESCRIPTION = "${WORKSPACE}/Releasenotes.md"

Konkret bewirkt die Zeile, dass die Toolchain bei jedem Release-Build auch die Release Notes auf Github automatisch veröffentlicht.

Listing 4

Jenkinsfile legt Github-Release an

01 script {
02         createRelease(
03                user: "${GITHUB_ACCOUNT}",
04                repository: "${GITHUB_REPOSITORY}",
05                tag: "${GIT_TAG_TO_USE}",
06                name: "${RELEASE_NAME}",
07                description: "${RELEASE_DESCRIPTION}",
08                preRelease: "${PRERELEASE}"
09         )
10 }

Strukturierung der Build-Pipeline

Das im Folgenden besprochene »Jenkinsfile« definiert den gesamten Build. Das Repository »cmake-buildtest« [14] enthält den Quellcode sowie das Jenkinsfile für eine simple Hello-World-Beispielapplikation.

Die gesamte Pipeline wird zunächst an einen Agent mit dem Label »docker« gebunden. Die Idee dahinter ist, den Zweig der Pipeline, der am längsten läuft, ohne weitere Agent-Labels auf diesem Agent abzubilden. Alle parallelisierbaren Schritte erhalten dann ein separates Agent-Label, sodass für diese Steps parallel zur bereits laufenden Instanz eine neue Builder-Instanz startet (siehe Kasten “Achtung, Deadlock-Gefahr!”) und das System dort die entsprechenden Build-Steps ausführt.

Die Gliederung der Pipeline an und für sich richtet sich in der nächsten Stufe nach dem aktuellen Branch. Somit enthält die erste Stage ein »when«-Statement, um sie nur dann auszuführen, wenn es sich nicht um den Develop- oder Master-Branch handelt. Diese so genannten Feature-Branch Builds (Abbildung 2) sind in der Regel stark reduziert. Das Beispiel hier referenziert ein spezielles Dockerfile, das die Demo-Applikation lediglich baut und zu Anschauungszwecken startet. Die meisten Anwendungsfälle prozessieren hier nur ihre Unit-Tests.

Die Idee der weiteren Stages ist, dass diese nur für Master- und Develop-Builds gelten. Um den Unterschieden eines Build vom Master-Branch gerecht zu werden, gibt es in der zweiten Stage, »Prepare master branch build«, lediglich einige Neudefinitionen von Variablen. Ab hier kommen nun auch die im vorherigen Abschnitt beschriebenen Hilfsfunktionen zum Einsatz – so in der Stage »Git tag handling«, um die Release auf Github vorzubereiten (Abbildung 3).

In der letzten Stage, »Build steps«, erfolgt nun der finale Build der Applikation inklusive Github-Deployment. Der Aufruf des Build ist abermals trivial, da die eigentliche Build-Logik im zugehörigen Dockerfile steckt. Von dort aus erfolgt auch das Github-Deployment (Abbildung 4).

Abbildung 2: Die Build-Pipeline eines Feature-Branch (Ansicht in Github).

Abbildung 2: Die Build-Pipeline eines Feature-Branch (Ansicht in Github).


Abbildung 3: Die Build-Pipeline des Develop-Branch. Gut sichtbar ist die Plattform-abh&auml;ngige Verzweigung.

Abbildung 3: Die Build-Pipeline des Develop-Branch. Gut sichtbar ist die Plattform-abhängige Verzweigung.


Abbildung 4: Die Build-Pipeline des Master-Branch f&uuml;r den produktiven Betrieb.

Abbildung 4: Die Build-Pipeline des Master-Branch für den produktiven Betrieb.

Achtung, Deadlock-Gefahr!

Wer parallele Buildsteps zulässt, muss besonderes Augenmerk darauf legen, in keinen Deadlock zu laufen. Wenn beispielsweise zwei Builder mit jeweils einem Buildslot vorhanden sind und zwei separate Jobs starten, belegen diese beiden Jobs beide Slots. Starten beide Jobs nun parallele Buildsteps, bleiben sie in der Jobqueue wartend stecken, da nie mehr ein Buildslot frei wird.

Also muss der Admin entweder sicherstellen, dass genügend Buildslaves zur Verfügung stehen, oder einen Job-Timeout verwenden, um die Jobs gegebenenfalls abzubrechen.

Ziel erreicht

Dieser Artikel zeichnet in einem vollständigen Beispiel-Setup, wie jeder Admin oder Entwickler eine Continuous-Integration-Pipeline mit Jenkins aufbauen kann (Abbildung 5). Er hält die einzelnen Bestandteile bewusst einfach.

Die Domain-specific Language (DSL) der Jenkins-Pipeline [2] erweist sich dabei als sehr mächtiges Werkzeug, um alle Bedürfnisse einer modernen Git- und Container-basierten Software-Entwicklung abzudecken, die auf Continuous Integration sowie Continuous Delivery und Deployment ausgerichtet ist.

Abbildung 5: Die Release-Area des Beispiel-Repository auf Github.

Abbildung 5: Die Release-Area des Beispiel-Repository auf Github.

Der Autor

Yves Schumann arbeitet als Lead Software Engineer bei der ti&m AG in Zürich. Als Certified Cloudbees Jenkins Engineer beschäftigt er sich dort hauptsächlich mit der Build- und Deployment-Automatisierung und dem ganzen Aufgabenbereich hinsichtlich Quellcode-Versionierung sowie mit dem für den Software-Entwickler relevanten Tooling.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 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