Microservices und Tests in Continuous Delivery Pipelines integrieren

Um schnell von der Idee bis zur öffentlichen Freigabe von Softwareprodukten zu kommen, arbeiten Entwickler heute auch in Unternehmen wie Volkswagen bevorzugt nach den Maximen der Continuous Delivery.

Know-how  –  10 Kommentare
Microservices und Tests in CD-Pipelines integrieren

Immer mehr Software bestimmt den Unternehmensalltag und trägt nicht mehr nur die Office-IT, sondern reicht bis in die Produktionshallen großer Industriekonzerne. Als solcher hat Volkswagen zahlreiche Anwendungen mit unterschiedlichen Aufgaben in den verschiedenen Fachbereichen weltweit im Einsatz: von der Forschung und Entwicklung über die Produktion bis hin zum Kundenservice im After Sales. Dabei kommt pro Fachbereich durchaus eine dreistellige Zahl unterschiedlicher Applikationen zusammen.

Umso wichtiger ist es, Software funktional zu halten und sie schnell und effektiv an neue Marktbedingungen oder Anforderungen der Fachbereiche anzupassen. Deshalb stiegen die Entwickler bei Volkswagen auf agile Softwareentwicklung um und erarbeiteten im Laufe der Zeit Best Practices für eine Continuous Integration and Delivery (CI/CD) Pipeline. Diese startet nun nach jeder Änderung die Pipeline und der Code durchläuft die Etappen Build, Tests und Deployment.

Im Fokus stand bei der Umstellung vor allem die Frage, wie eine permanente Auslieferbarkeit der Software gewährleistet ist. Das wiederum führte zu weiteren Maßnahmen, die den Softwareentwicklungsprozess maßgeblich verbessern sollten.

Eine wesentliche Herausforderung während des Übergangsprozesses bestand zunächst im Umgang mit den Organisationsstrukturen eines Großunternehmens. Während CI/CD aus technischer Sicht seit längerem einsatzfähig war, galt es vor allem die Stakeholder an der Software sowie das Application Management in ein Boot zu holen. Zwar waren agile Methoden wie Scrum in Teilen des Unternehmens etabliert, allerdings bestehen nach wie vor Silos, was schwer mit dem DevOps-Ansatz vereinbar ist. Angestrebt war, von Anfang an die AMS-Experten einzubinden, um keinerlei Ressourcen zu verschwenden. Zielgerichtete Kommunikation mit dem AMS-Team und wichtigen Stakeholdern dienten in der Folge dazu, die Silos aufzubrechen und ein von der Aufbauorganisation unabhängiges Projektteam zu bilden.

Eine weitere Herausforderung des Projekts lag klar in der Testautomatisierung, die einen wesentlichen Bestandteil der CI/CD-Pipeline ausmacht. Die zu entwickelnde Software hatte mit unterschiedlichen, gewachsenen Bestandssystemen zu kooperieren. Unter diesen Voraussetzungen stellt sich das bekannte Problem von Legacy-Systemen ein, dass automatisiertes Testen nicht in Altsystemen funktioniert. Die Mitarbeiter des Projekts mussten dies folglich im Entstehungsprozess der neuen Software berücksichtigen.

Einer der Wege, damit umzugehen, war die Abkehr von Deployment-Monolithen hin zu Microservices, die dem höheren Tempo und dem schlanken Vorgehen der CI/CD-Pipeline entgegenkommen. Monolithen umfassen alle Teilfunktionen einer Anwendung und sind nur durch Replikation auf mehreren Servern skalierbar. Auf die Weise binden sie mehr Ressourcen. Stehen nur kleine Änderungen an der Software an, ist der Aufwand für die Entwickler in solcher einer Architektur verhältnismäßig hoch. Im Vergleich dazu steht ein Microservice für eine Teilfunktion oder einen Service, wodurch Anpassungen schneller und unabhängiger von den restlichen Komponenten umsetzbar sind.

Für den Aufbau der Architektur von Software gibt es die unterschiedlichsten Entwurfsprinzipien wie "Keep it simple stupid" (KISS), "Divide & Conquer" oder "Single-Responsibility". Sie lassen sich im Prinzip auf zwei wichtige Anforderungen herunterbrechen: Einfachheit und Modularität. Und genau das erfüllen Microservices ideal. Durch die Regel, nur einen geschlossenen Funktionsumfang pro Microservice abzubilden, steigt die Gesamteffizienz der Entwicklung. Mehrere kleine agile Teams sind in der Lage, parallel an Services zu arbeiten statt gemeinsam langwierig an einem schwergewichtigen Monolithen. Das Deployment ist insgesamt nicht mehr so stark an die Gesamtanwendung gekoppelt, wenn Microservices als eigene Prozesse, virtuelle Maschinen oder Docker-Container laufen. Hier geht der Trend derzeit klar in Richtung Container, da sie unabhängig von dem darunterliegenden Betriebssystem funktionieren, kaum Overhead erzeugen und mit Tools wie Kubernetes, Rancher oder Swarm vergleichsweise einfach zu orchestrieren sind.

Anders als bei klassischen Modularisierungstechniken, bei denen Funktionen in Klassen, Module oder Bibliotheken gekapselt sind und im selben Prozess laufen, sind Microservices lose miteinander gekoppelt. Jeder Service wird in einem eigenen Prozess ausgeführt, die Kommunikation erfolgt über das Netzwerk via HTTP in der Regel auf Basis von REST und JSON. JSON bietet für den Datenaustausch zwischen Anwendungen ein kompaktes Datenformat mit menschenlesbaren Textformen. Zudem existieren Parser in allen gängigen Programmiersprachen.

Wie Modularisierung mit Microservices funktioniert, veranschaulicht das folgende Beispiel in der Programmiersprache Python:

@app.route('/current_time', methods=['GET'])
def get_current_time():
current_time = datetime.fromtimestamp(time.time())
return jsonify({"current_time" :
str(current_time)})

Der Quelltextauszug zeigt einen Microservice, der die aktuelle Zeit über die HTTP-Route /current_time zur Verfügung stellt. Die Funktion get_current_time() erzeugt für jede HTTP-Anfrage ein JSON-Objekt mit dem Schlüssel current_time und der aktuellen Uhrzeit als Wert. Anfragen an den Zeitservice erfolgen etwa über standardmäßiges HTTP beispielsweise aus dem Browser heraus oder von anderen Services:

GET /current_time HTTP/1.1 Host: 127.0.0.1:5000

Die Antwort des Zeitservice sieht wie folgt aus:

HTTP/1.1 /current_time 200 OK
Content-Type: application/json
Content-Length: 51
...
{
'current_time': '2017-06-19 21:03:17
}

Nach dem HTTP-Header, der Status, Typ des Inhalts und Länge enthält, folgt der eigentliche Inhalt (Content) im JSON-Format.