Pull-Requests mit Docker deployen

Keine Kommentare

Die Git-Repositorys in meinem aktuellen Projekt sind auf Bitbucket Cloud gehostet. Änderungen am Code werden nur über Pull-Requests in die Code-Basis eingepflegt. Jenkins baut die Pull-Requests und vergibt entsprechend sein Approval, wenn der Build grün ist. Außerdem wird noch von mindestens einem Teammitglied ein Code-Review durchgeführt. Ein Code-Review erfordert zwar grundsätzlich nicht, dass der Reviewer den Code baut und die Anwendung lokal deployt und testet, aber manchmal möchte man sich das Ergebnis der Änderungen doch anschauen, gerade wenn z. B. CSS/HTML angepasst wurde. Ein visueller Test kann hier helfen, die Änderungen nachzuvollziehen und zu verifizieren.

Um es dem Reviewer möglichst einfach zu machen, sollte ein Weg gefunden werden, einen Pull-Request per Knopfdruck testbar zu machen. Die Anwendung wurde erst kürzlich auf Spring Boot migriert. Momentan bauen wir noch eine War-Datei, die auf einem Tomcat deployt wird. Die War-Datei lässt sich aber auch direkt mit dem embeddeten Tomcat ausführen und damit sehr einfach in ein Docker-Image verpacken.

Als Datenbank kommt MongoDB zum Einsatz. Für die lokale Entwicklung bauen wir dafür ebenfalls ein Docker-Image. Das Besondere daran ist, dass dieses Image bereits Daten enthält. Die Natur unserer Anwendung macht es nötig, dass immer aktuelle Daten in der Datenbank sind. Deshalb gibt es einen Job, der nächtlich Testdaten für die Integrationsinstanz der MongoDB generiert. Jenkins baut nun ebenfalls nächtlich ein Docker-Image für die MongoDB. Dabei wird ein Dump von der Integrationsinstanz gezogen, der dann ins Docker-Image importiert wird. Das Image wird in die Docker-Registry gepusht. Morgens kann man sich dann als Entwickler ein frisches Image pullen.

Unser Jenkins läuft ebenfalls unter Docker, ein Container für den Master und einer für den Slave, der alles baut. Der Docker-Socket des Hosts ist dabei in den Slave gemountet. Wenn nun Jenkins einen Pull-Request baut, erstellt er gleich auch ein Docker-Image davon, das mit der Nummer des Pull-Requests getaggt wird. Über das Build-Pipeline-Plugin wird es nun ermöglicht, einen Pull-Request und dazu eine frische MongoDB-Instanz auf dem Jenkins-Host zu deployen.

PR Pipeline

Der Job zum Bauen des Pull-Requests hat einen manuellen Downstream-Job (Teil des Build-Pipeline-Plugins), der die Anwendung samt Datenbank deployt.

postbuild-1

Ein Pythonskript erzeugt dazu per Docker ein Netzwerk und startet die Container.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import socket
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from subprocess import Popen, STDOUT, PIPE
 
 
def docker(*arguments):
    cmdline = ['docker']
    cmdline.extend(arguments)
 
    print()
    print(' '.join(arg for arg in cmdline))
 
    proc = Popen(cmdline, stdout=PIPE, stderr=STDOUT)
    output = []
    while True:
        line = proc.stdout.readline()
        if not line:
            break
 
        line = line.rstrip()
        print('docker> %s' % line)
        output.append(line)
 
    proc.wait()
    return output
 
 
def start_myproject():
    print('Starting MyProject: %s' % version)
 
    docker('network', 'create', '-d', 'bridge', network_name)
 
    docker('run', '-d', '--name', mongo, '--network', network_name, '--network-alias', mongo, '-P', 'myrepo/mongo')
 
    docker('run', '-d', '--name', myproject, '--network', network_name, '-P', 'myrepo/myproject:%s' % version,
           '--spring.profiles.active=dev', '--database.hosts=%s' % mongo)
 
    docker('port', mongo)
    docker('port', myproject)
 
 
def destroy_myproject():
    print('Destroying MyProject: %s' % version)
 
    docker('stop', myproject)
    docker('rm', myproject)
 
    docker('stop', mongo)
    docker('rm', mongo)
 
    docker('network', 'rm', network_name)
 
 
args_parser = ArgumentParser(description='Manages MyProject/Docker dev deployments')
sub_parsers = args_parser.add_subparsers()
 
start_parser = sub_parsers.add_parser('start', help='Start MyProject and Mongo')
start_parser.add_argument('--version', '-v', required=True, help='The MyProject version to start')
start_parser.set_defaults(func=start_myproject)
 
destroy_parser = sub_parsers.add_parser('destroy', help='Destroy MyProject and Mongo')
destroy_parser.add_argument('--version', '-v', required=True, help='The MyProject version to destroy')
destroy_parser.set_defaults(func=destroy_myproject)
 
args = args_parser.parse_args()
 
version = args.version
network_name = version
mongo = 'mongo_%s' % version
myproject = 'myproject_%s' % version
 
args.func()

shell-start

Der Port für die Anwendung wird von Docker vergeben. Die URL findet man dann im Log des Jobs, an das man über den Build-Pipeline-View schnell rankommt. Der Job erhält als Parameter die Nummer des Pull-Requests und weiß dadurch, welches Image zu starten ist.

Der Job zum Deployen hat wiederum einen manuellen Downstream-Job, mit dem sich das Deployment wieder zerstören lässt.

postbuild-2

shell-destroy

Damit man sich die Logs der Anwendung anschauen oder auch mal eine Shell auf dem Container öffnen kann, ohne sich per SSH auf den Jenkins-Host zu verbinden, haben wir noch Shipyard installiert. Damit bekommt man einen Überblick über alle laufenden Docker-Container und kann ggf. aufräumen, falls mal ein Deployment vergessen wurde.

Wieso kein Pipeline-Job?

Leider ist das Bitbucket Pull Request Builder Plugin nicht mit den neuen Pipeline-Jobs des Jenkins kompatibel. Es gibt dazu ein Ticket, in dem auf das Bitbucket Branch Source Plugin verwiesen wird, das in unserem Fall aber nicht in Betracht kommt, da es mit Webhooks arbeitet. Für den master-Build verwenden wir allerdings bereits einen Pipeline-Job.

 

Reinhard Nägele

Reinhard ist Senior IT Consultant am Münchner Standort der codecentric. Er blickt auf rund 18 Jahre Erfahrung in der Java-Entwicklung zurück. In seinen Projekten setzt er sich sehr dafür ein, die Automatisierung voranzutreiben und hat in den letzten Jahren umfangreiches Wissen in Infrastrukturfragen rund um Maven, Jenkins, Git, Docker aufgebaut und dieses auch in Schulungen weitergegeben. Aktuell beschäftigt sich Reinhard mit Cluster-Plattformen wie Kubernetes und DCOS und ist begeistert von den neuen Möglichkeiten, die sich hier auftun. Reinhard trägt gerne zu Open-Source-Projekten bei. Er ist Mitglied im Maintainer-Team für Kubernetes Helm Charts.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Weitere Inhalte zu Continuous Integration

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.