Containerbasierte Testautomatisierung

In Zeiten von Containern, Clustern, Build-Pipelines und DevOps ist ein hoher Grad an Automatisierung notwendig, um sich auf das Wesentliche zu konzentrieren: Das Softwareprodukt. Hierfür bietet die Open-Source- Welt eine Reihe von Werkzeugen, um vollautomatische, steuerbare und reproduzierbare Build-, Test- und Deployment-Prozesse zu realisieren.

Testen von Microservices

Ein Tech-Stack mit unterschiedlichsten Technologien, die Fachlichkeit in verschiedene Services verteilt, dazu eine durch Netzwerke und Kommunikationsprotokolle geprägte Gesamtarchitektur – im Vergleich zu monolithischen Systemen wird schnell klar, dass sich durch Microservices prinzipbedingt einiges geändert hat. Dies gilt auch für das Thema Testing. Dabei bleiben die Herausforderungen klassischer Systemarchitekturen bestehen. Der Trendwechsel hin zur Microservice-Architektur bringt sowohl Vorteile als auch Nachteile mit sich. Durch die Aufteilung komplexer Fachlichkeit in kleinere Einheiten verringert sich der funktionale Umfang des einzelnen Services. Die Isolierung der Services über Kommunikationstechnologien und Nachrichten, sowie die kurzen Start- und Deployment-Zeiten vereinfachen das Testen enorm. Die Isolation der Fachlichkeit hält dabei den Umfang der Test-Suite einzelner Komponenten klein und übersichtlich. Unter Verwendung von Container-Technologien lassen sich zudem Ad-hoc-Testumgebungen bereitstellen und wieder entfernen. Die Isolation und Nachrichtenorientierung moderner Systemarchitekturen bringt jedoch im gleichen Zuge neue Herausforderungen mit sich. Der Fokus des Testens verschiebt sich auf Integrationstests, da innerhalb des Gesamtsystems nun deutlich mehr Systemintegrationen zu anderen Services stattfinden müssen. Die verwendeten Kommunikationstechnologien sind dabei genauso unterschiedlich wie die Anforderungen an die jeweiligen Teile des Gesamtsystems. Die korrekte Interaktion zwischen Systemkomponenten und das Testen größerer fachlicher Anforderungen kann nun nicht mehr innerhalb eines Softwareprojekts unter Verwendung von Mocks sichergestellt werden. Stattdessen sind die Endpunkte anderer Services akkurat zu simulieren und End-2-End-Testumgebungen aufzusetzen, um eine aussagekräftige Testabdeckung zu erreichen. Aus der bekannten Test-Pyramide entwickelt sich somit ein Test-Diamant (Abb 1).

 

 

 

Von der Test-Pyramide zum Test-Diamanten. (Abb. 1)

Continuous-Integration und Continuous-Delivery (CI/CD)

Continuous-Integration (CI) bezeichnet eine Arbeitsmethodik, die zum Ziel hat, das gemeinsame Arbeiten an Software zu vereinfachen und Konflikte bei der Integration von zum Beispiel neuen Features zu vermeiden, die durch unterschiedliche Stände in den Arbeitskopien der einzelnen Entwickler verursacht werden. Dies wird bei Continuous-Integration durch einen Workflow realisiert, der kontinuierlich und bestenfalls vollautomatisch durchlaufen wird und dabei sicherstellt, dass die Abweichungen der Softwarestände nicht zu groß werden und keine größeren Konflikte entstehen, die nur unter enormen Zeitaufwand zu lösen sind. Grundlage dieses Workflows ist, dass alle Entwickler einer Software in einem gemeinsamen Repository arbeiten. Services wie GitHub, GitLab und Bitbucket stellen die dafür benötigte Infrastruktur bereit und erlauben neben der reinen Verwaltung von Quellcode auch die Verwaltung von Nutzerberechtigungen, die Anbindungen an Pipeline-Tools und bieten z.T. sogar Projektmanagement-Tools für die Verwaltung von Fragen, Fehlerberichten und neuen Funktionalitäten. Der nächste Schritt in Richtung CI ist das automatische Bauen und Testen der Software. Die Ökosysteme moderner Programmiersprachen bieten hierfür flexible konfigurierbare Build- und Test-Werkzeuge, die über Kommandozeilenbefehle zu steuern sind und sich zum großen Teil durch Plugin-Systeme beliebig erweitern lassen. Stehen Versionsverwaltung und Build-Werkzeuge bereit, kann leicht damit begonnen werden, den Gesamtablauf der Continuous-Integration zu automatisieren. Hierfür empfiehlt sich die Verwendung von spezialisierten CI-Systemen wie Jenkins, Drone-CI, Travis-CI, GitLab-CI oder Amazon-Code-Pipelines, um nur einige aus diesem reichhaltigen Ökosystem aufzulisten. Jedes dieser Tools bietet dabei eine Plattform, mit der es möglich ist, Build- und Test-Pipelines zu erstellen, die sich exakt an den Anforderungen des Software-Builds orientieren. Hierbei ist es wichtig, sich nicht nur auf das automatisierte Bauen und Testen des Hauptzweigs (Master/Trunk) der Software zu beschränken, sondern ebenfalls die Branches mit neuen Features oder Bugfixes diesbezüglich zu automatisieren. Allgemein lässt sich sagen, dass die Kosten für eine Fehlerbehebung sinken, je früher der Fehler erkannt wird. Eine Automatisierung von Branch-Builds, macht Probleme schon während der Entwicklung frühzeitig sichtbar, erlaubt es den Teams zeitnah zu reagieren und reduziert somit aktiv die Entwicklungskosten. Release-Management und die Anbindung weiterer Tools für zum Beispiel statische Codeanalyse lassen sich zudem einfach konfigurieren und in die Pipeline einbinden. Damit sichergestellt ist, dass die aktuelle Pipeline stets zum aktuellen Stand der Software passt, empfiehlt es sich, die Konfiguration mit dem Quellcode zu speichern, zu verwalten und zu versionieren. Dieses als Configuration-as-Code bekannte Prinzip wird von allen gängigen CI-Systemen unterstützt. Dabei ist es unerheblich, ob das CI-System inhouse oder in der Cloud liegen soll. Der Markt bietet ein Produkt für jede Anforderung. Zu guter Letzt ist darauf zu achten, dass Branches nicht langfristig existieren. Je länger ein Branch existiert, desto größer ist die Wahrscheinlichkeit eines Konfliktes bei der Rückführung in den Hauptzweig. Bei diesem Punkt hilft eine disziplinierte Arbeitstechnik mehr als jedes Tool. Wichtig ist, dass die Software in kleinen überschaubaren Inkrementen entwickelt wird und die Resultate nach einem Review direkt in den Produktivcode eingehen. Der nächste Schritt in der Automatisierung ist die Continuous-Delivery (CD). Hierbei wird der Continuous-Integration-Workflow um das Deployment in eine Testumgebung erweitert, in der sich dann z.B. automatisiert Integrationstests durchführen lassen. Ziel von Continuous-Delivery ist es, stets einen Stand der Software vorweisen zu können, der auslieferbar ist. Muss beispielweise ein dringendes Feature ausgeliefert werden, kann mittels CI/CD-Pipelines sichergestellt werden, dass die Reaktionszeit auf die Feature-Anfrage beschleunigt und gleichzeitig die Time-to-Market reduziert wird. Zudem sind dringende, außerplanmäßige Releases ebenfalls kein Problem, da der aktuelle Stand der Software im Hauptzweig stets gebaut, getestet und von technischer Seite abgenommen ist. Die Königsdisziplin der Automatisierung ist das Continuous-Deployment. Hierbei wird der komplette Entwicklungs- und Auslieferungsprozess automatisiert, sodass Änderungen an der Software binnen Minuten oder Stunden den Weg in die Produktion finden. Entgegen der schematischen Darstellung des Lifecylce (Abb. 2) genügt es dabei jedoch nicht, ausschließlich das Deployment in die Produktion zu automatisieren. Die Praxis zeigt, dass es gerade bei Microservice-Architekturen darauf ankommt, zusätzlich eine aussagekräftige Testabdeckung durch Integrations- und Ende-zu-Ende-Tests sicherzustellen und auch diese während der CI/CD-Pipeline automatisiert durchlaufen zu lassen. Hierbei ist es notwendig, die dafür vorgesehenen Umgebungen bereitzustellen und zu konfigurieren. Dies stellt unter Zuhilfenahme modernen Container-Plattformen jedoch kein Problem mehr da.

 

 

Schematische Darstellung eines Continuous-Deployment-Lifecylce.  (Abb. 2)

Testautomatisierung auf Container-Plattformen

Auf Basis von Docker und Kubernetes lassen sich heutzutage binnen Minuten komplexe Infrastrukturen realisieren. Das gilt nicht nur für Sourcecode-Verwaltung, CI-Systeme, Container-Registries und Routings, sondern auch für ganze Softwaresysteme sowie Entwicklungs-, Test- und Integrationsumgebungen. Im folgenden Beispiel wird unter Verwendung von RedHat-OpenShift und Jenkins gezeigt, wie eine CI/CD Pipeline für eine Todo-App im Container umgesetzt werden kann. Bei RedHat-OpenShift handelt es sich um eine Platform-as-a-Service (PaaS) Lösung (vgl. Abb. 3) aus dem Cloud-Computing-Umfeld, die ihren Fokus auf die Lösung infrastruktureller Problemstellungen legt. So sind beispielsweise die Verwaltung von Builds, Deployments, Softwarekonfigurationen und Routings, aber auch die Skalierungen von Services sowie die Bereitstellung und Konfiguration von Netzwerken im Funktionsumfang enthalten. Softwareprojekte erhalten innerhalb von OpenShift eine oder mehrere eigene Umgebungen, in dem die zugehörigen Artefakte hinterlegt werden. Dies lenkt den Fokus weg von infrastrukturellen Problemstellungen, hin zur eigentlichen Softwareentwicklung (Dev) und den dazugehörigen Betriebsthemen (Ops).

 

Übersicht verschiedener as-a-Service-Ansätze. (Abb. 3)

 

Innerhalb von OpenShift gibt es verschiedenste Möglichkeiten, CI/CD-Systeme zu verwenden. Eine native Unterstützung erfährt Jenkins, der über ein Build-Konfiguration-Template (Listing 1) angelegt werden kann und anschließend komplett konfiguriert zur Verfügung steht. Seine Pipeline bezieht das System in Form eines Jenkins-File aus einem Git-Repository im Configuration-as-Code-Ansatz und ist somit ausschließlich für die Abwicklung der Builds für das konfigurierte Projekt zuständig. Das Jenkins-File aus dem Repository wird zudem vor jedem Build-Prozess neu geladen und ist daher immer aktuell.

 

Die bereitgestellte Jenkins-Instanz ist darüber hinaus so konfiguriert und mit den notwendigen Berechtigungen versehen, um das Scheduling der Worker-Nodes durchzuführen und eine Steuerung des Clusters bzw. des Projekt-Namespaces aus der Jenkins-Pipeline heraus zu ermöglichen. Es können zudem auch andere Projekt-Namespaces gesteuert werden. Dies bedarf jedoch einer expliziten Freigabe über das Kommandozeilenwerkzeug mittels oc policy add-role-to-user edit system:serviceaccount:todo-app-dev:jenkins -n todo-app-int.

Hier wird dem Service-Account des Jenkins aus dem Namespace todo-app-dev die Berechtigung edit auf den Namespace todo-app-int gewährt. Dies befähigt Jenkins, ebenfalls diverse Aktionen wie zum Beispiel Deployments innerhalb der Integrationsumgebung durchzuführen. Um diese Berechtigung vergeben zu können, muss der Nutzer, der diese Konfiguration vornimmt, über ausreichende Berechtigungen innerhalb des Zielprojektes verfügen.

Um nun eine CI/CD Pipeline bereitstellen zu können, benötigt die Jenkins-Instanz noch das Jenkins-File. Für dieses Beispiel wurde die Pipeline in drei Stages aufgeteilt. Die erste Stage befasst sich mit dem Bauen der Software inklusive Unit-Tests sowie einem Deployment in einer Umgebung für Entwicklertests. Die zweite Stage nimmt ein Deployment der Applikation in einer Integrationsumgebung vor und führt die dazugehörigen Integrationstests aus. Die letzte Stage realisiert das Deployment in die Produktivumgebung. Innerhalb der Development-Stage (Listing 2) wird mittels einer Jenkins-Worker-Node ein Checkout des Quellcodes vorgenommen. Die Information, aus welchem Repository der Sourcecode bezogen werden soll, erhält der Jenkins mit seiner Build-Konfiguration. Anschließend wird die Todo-App innerhalb des Projekt-Namespaces todo-app-dev gebaut und deployed. Hierzu werden zwei Helfermethoden verwendet (Listing 3 und 4), die wiederkehrende oder unübersichtliche Operationen innerhalb der Pipeline abstrahieren. Das Resultat des Builds ist ein Docker-Container, der automatisch in der OpenShift-eigenen Docker-Registry hinterlegt wird und somit zur weiteren Verwendung innerhalb des Clusters zur Verfügung steht. Sind diese Operationen erfolgreich, wird anschließend das Tag des Containers aus der Dev-Umgebung in die Int-Umgebung übertragen. Dies macht die Todo-App für ein Deployment innerhalb der Integrationsumgebung verfügbar.

 

 

 

Die Integration-Stage (Listing 5) ist im Vergleich zur Development-Stage strukturell anders aufgebaut. Hier wird über ein PodTemplate eine spezielle Worker-Instanz mit vorinstalliertem Maven gestartet. Nach dem Checkout des Projekts und dem Deployment können nun Integrationstests, die im Repository hinterlegt sind, mittels Maven gestartet und im Zusammenspiel mit der Todo-Applikation ausgeführt werden. Das Resultat der Integrationstests wird anschließend an Jenkins und OpenShift weitergeleitet, sodass ein fehlgeschlagener Integrationstest einen Abbruch der Pipeline zur Folge hat. Sind die Tests erfolgreich, erfolgt eine Übertragung des Container-Tags in die Produktivumgebung.

Abschließend wird das Deployment in die Produktivumgebung (Listing 6) vorgenommen.

Das komplette ausführbare Beispiel, inklusive Todo-App, den Integration Tests, umgesetzt mit dem Integration Testing Framework Citrus, sowie einer Anleitung zum Aufsetzen der notwendigen lokalen Infrastruktur befindet sich auf GitHub[1].

 

Fazit:

Die Testautomatisierung im Microservice- und Container-Umfeld stellt uns vor neue Herausforderungen, die es zu bewältigen gilt. Unter Verwendung moderner Technologien, den richtigen Methodiken und einem geeigneten Setup sind diese Herausforderungen jedoch durchaus zu meistern. Mithilfe von Containern, Clustern oder PaaS-Systemen lassen sich ohne großen Aufwand verschiedenste Umgebungen erstellen und über CI-Systeme steuern. Über Mechanismen wie Configuration-as-Code stellt man zudem sicher, dass die Infrastruktur und ihre Konfiguration immer zum aktuellen Stand der Software passen.

 

Sven Hettwer ist Senior Software Engineer mit Fokus auf Testautomatisierungs- und CI/CD-Lösungen sowie Maintainer des Open Source Frameworks Citrus beim Münchner IT-Dienstleister Consol am Standord Düsseldorf.

 

Links/Quellen/Verweise:

[1] https://bit.ly/2TASHyy

Victoria Krautter


Leave a Reply