×

Next Level Continuous Integration – mit Jenkins, Docker und Kubernetes

Docker war eines der ganz großen Themen der letzten und insbesondere des letzten Jahres. Für die einen Grund genug, sich mit dem Klein-Klein der Umsetzung der Container-Vitalisierung und eventuellen Alternativen zu beschäftigen. Anderen reichte dies bereits, um den Tod von Docker zu prophezeien. Dabei ist es an der Zeit, sich Gedanken zu machen, was man mit Docker alles anstellen kann, das über das Ausliefern einer Anwendung im Container hinausgeht.

Kubernetes als Container-Orchestrierung

Richtig Spaß macht der Einsatz von Docker erst mit der passenden Container-Orchestrierung. Hier hat sich ganz klar das von Google entwickelte, und mittlerweile an die Open-Source-Gemeinde übergebene, Kubernetes durchgesetzt. Kubernetes erlaubt es, horizontal skalierbare und in Docker Containern verfügbar gemachte Anwendungen allein durch das Einspielen eines so genannten Playbooks aufzusetzen. Dabei wird implizit für jede Anwendung ein Load Balancer ins Feld gesetzt und die Anwendung statisch oder dynamisch hochskaliert. Das Playbook ist dabei eine in Json-oder Yaml-Notation geschriebene und gut leserliche Setup-Beschreibung. Besonders wirkungsvoll wird diese Umgebung, wenn sie zusammen mit einer Microservice-Architektur zum Einsatz kommt, aber auch abseits davon bieten sich interessante Möglichkeiten.

Kubernetes im Zusammenspiel mit Jenkins

Eine dieser Möglichkeiten für Continuous Integration ist das Zusammenspiel des Jenkins Kubernetes Plugins und des Jenkins Pipeline Plugins. Dabei erlaubt es das Pipeline Plugin, wie der Name schon vermuten lässt, Build Pipelines zu definieren, während sich das Kubernetes Plugin darum kümmert, dynamisch Buildnodes in einem Kubernetes Cluster zu spawnen. Diese werden für exakt einen einzigen Build genutzt.

Jeder der bereits etwas komplexere Jenkins Build Jobs angelegt hat, bei denen auch als Teil des Builds automatisierte Integrationstests ausgeführt werden, wird das Problem kennen: für ein solches Build muss eine entsprechende Umgebung auf dem Build Node geschaffen werden und am Ende des Builds auch wieder aufgeräumt werden. Das kann von der Notwendigkeit, Development Kits und Frameworks in bestimmten Versionen vorliegen zu haben, bis zum Aufsetzen einer Testinfrastruktur gehen. Es ist mit Jenkins X auch bereits eine vollständige Integration von Kubernetes angekündigt.

Der große Vorteil, den die beiden vorrangehend erwähnten Plugins liefern, ist, dass sie den Nutzer in die Lage versetzen, für jeden Build-Schritt die genaue Infrastruktur in Form von Docker Containern zu definieren, die automatisiert gespawnt und wieder aufgeräumt werden. Vereinfacht wird das auch dadurch, dass mithilfe der Jenkinsfiles die Build Chain im Repository der Anwendung definiert wird, wo sie auch thematisch hingehört.

Beispiel zum Aufbau einer Jenkins Pipeline

Als einfaches Beispiel gehen wir von einer, wie auch immer gearteten, Java Web Application aus, die außerdem ihre eigene Angular Webseite mit ausliefert. Um das Java Backend zu bauen, benötigt man lediglich einen Docker Container, welcher Java und Maven beinhaltet; – diesen gibt es fix und fertig im Docker Hub, er muss nur noch um eigene settings.xml ergänzt werden. Für das Angular Frontend wird hingegen noch node.js und yarn benötigt. Da eine Angular Anwendung für ihren Kompiliervorgang immer etwas länger braucht, bietet es sich an, das Kompilieren des Front- und Backend zu parallelisieren.

—Beginn Beispiel—

def label = UUID.randomUUID().toString()
podTemplate(label: label, containers: [
     containerTemplate(name: 'frontend',
        image: 'my-docker-registry/maven-nodejs:3.5.2-jdk8',
        ttyEnabled: true,
        command: 'cat'),
     containerTemplate(name: 'backend',
        image: 'maven:3.5.2-jdk8',
        ttyEnabled: true,
        command: 'cat')
     ],
     volumes: [secretVolume(secretName: 'maven', mountPath: '${MAVEN_CONFIG}')]

) {
  node(label) {
   try {
     stage ('Clone') {
       def scmVars = checkout scm
       branchName = scmVars.GIT_BRANCH

       echo 'Pulled...' + branchName
    }
       parallel (
          backend : {
            container('backend') {
              stage('Compile backend') {
                sh 'mvn -B clean compile -U -Pbackend'
              }
            }
          },
          forntend : {
            container('frontend') {
              stage('Compile ui') {
                sh 'mvn compile -Pfrontend'
              }
            }
          }
      )
      currentBuild.result = 'SUCCESS'
    } catch (err) {
       currentBuild.result = 'FAILED'
       throw err
    }
  }
}

—Ende Beispiel—

Mit dem Beispiel wird deutlich, welche Möglichkeiten sich dadurch ergeben. Auch das Hochfahren eines Message Bus, einer Datenbank, oder Ähnliches für Low Level Integration Tests, welche in nachgelagerten Pipeline Stages ausgeführt werden, ist überhaupt kein Problem. Auch muss man sich nicht um das Aufräumen der Umgebung kümmern, da diese nach Beendigung des Jenkins Jobs wieder vollständig verschwindet. So findet der nächste Job wieder eine frisch gespawnte Umgebung vor. Ebenso gehören Versionskonflikte der Development Kits zwischen eigentlich unabhängigen Builds der Vergangenheit an, da jeder Build genau die Versionen spawnt, die er benötigt.

Im Wesentlichen bringt dieses Setup dem Entwickler zwei Vorteile: Zum einen kann er anspruchsvollere Build Chains mit geringerem Aufwand aufsetzen, was sowohl die Qualität der Software erhöht, als auch Arbeit spart. Und zum anderen kann man seine Build-Umgebung selbst definieren, ohne sich Gedanken darum zu machen, ob man damit andere Builds zerschießt oder sich mit der IT herumzuärgern.

Links:

https://www.docker.com/

https://kubernetes.io/

https://jenkins.io/

https://wiki.jenkins.io/display/JENKINS/Kubernetes+Plugin

https://wiki.jenkins.io/display/JENKINS/Pipeline+Plugin

Von Matthias Schupp | 3.04.2018
Matthias Schupp

Softwareentwicklung