CDCs für otto.de – Continuous Everything (jetzt mit Cloud™)

Seit einigen Jahren bauen wir otto.de in unabhängigen Teams, die ihre Änderungen kontinuierlich Live stellen, ohne sich dabei untereinander koordinieren zu müssen. Eine weite Palette unterschiedlicher Tests helfen uns dabei, diese Änderungen schnell und ohne Angst vor Fehlern zu deployen. Eine Klasse von Tests hat allerdings in der Industrie noch nicht die Verbreitung gefunden, die ihr zukäme: Consumer Driven Contracts, kurz CDCs. Daher möchte ich unsere kürzlichen Erkenntnisse nutzen, hier darüber zu schreiben.

Für alle Arten von Daten (Produktdaten, Benutzerinformationen, Käufe, Rabattaktionen etc.) gibt es bei uns üblicherweise ein System, das die Hoheit darüber hat. Falls andere Systeme diese Daten ebenfalls benutzen möchten, wird eine Kopie davon im Hintergrund, also asynchron, an einer bereitgestellten Schnittstelle angefragt und automatisch in die eigene Datenbank übertragen. Auf diese Weise können wir lange Request-Kaskaden zwischen den Systemen vermeiden. Das ist gut für unsere Antwortzeit bei Kundenrequests und sehr hilfreich, um die Gesamtarchitektur resilient zu halten.

Die Server kommunizieren bei uns bislang fast ausschließlich über REST-artige HTTP-Schnittstellen. Es gibt also ein System, das Daten bereitstellt (den Server) und ein oder mehrere Systeme, die sich von dort die Daten abrufen sollen (die Clients).

Welches Problem soll gelöst werden?

Der Server stellt einen HTTP-Endpunkt bereit – beispielsweise für aktuelle Produktpreise – und ein Client kann HTTP-Request dagegen absetzen. Weil die Endpunkte im Allgemeinen gegen unberechtigten Zugriff geschützt sind, gehören noch Keystores und Zugangsdaten mit ins Bild, aber im Wesentlichen war es das auch schon:

Cdcs03
Asynchrone Datenversorgung sind häufig nur glorifizierte Http-Requests zwischen Client und Server. Beide Seiten benutzen üblicherweise ihre eigenen Keystores und die Secrets daraus um die Requests zu authentifizieren.

Die Schnittstelle des Servers hat meist irgendeine Art von Dokumentation oder Spezifikation und eine Test-Abdeckung, die vom Server-Team für angemessen gehalten wird. Dennoch kommt es immer wieder vor, dass im Client Abhängigkeiten zu Implementierungsdetails entstehen. Da verlassen sich Teams darauf, dass JSON-Elemente stets in einer bestimmten Reihenfolge auftauchen oder sie können mit neuen Datenfeldern nicht umgehen. Natürlich nehmen sich alle Beteiligten vor, ganz besonders aufmerksam zu sein. Oder sie sind überzeugt, dass diese Art von Fehlern nur anderen passieren. Dennoch kommt es immer wieder vor, dass etwas, das zuvor funktionierte plötzlich nicht mehr funktioniert: ein Bug. Diese Art von Störung ist zudem unauffällig genug, dass sie sich eine ganze Zeit im Life-System befinden kann, bevor es jemandem auffällt.

Ich wünsche mir also, dass wir das zumindest schnell bemerken:

wishlist01.png

Woher wir kommen

Wir haben gute Erfahrungen mit Pipelines gemacht. Ein Test, der nach dem Deployment auf ein Develop-System prüft, ob die Antwort des Servers noch in Domänenobjekte übersetzt werden kann, ist schnell geschrieben. Zwar wird jetzt noch ein weiterer Keystore in der Nähe der Pipeline benötigt, damit auch dieser Request authentifiziert werden kann, aber damit ist mein Wunsch schon erfüllt:

Cdcs11
In der Client-Pipeline prüft ein Test, ob die Antwort des Servers noch den Erwartungen entspricht. Die Pipeline wird rot wenn sich die Schnittstelle ändert und das Team kann reagieren.

Der Test läuft allerdings nur, wenn die Pipeline des Client-Teams angestoßen wird, beispielsweise durch eine Code-Änderung. Falls das Team aber gerade an einem anderen Service arbeitet, könnte vom Server-Team unbemerkt eine inkompatible Änderung Live gebracht werden.

Mein Wunschliste ist also eigentlich noch ein bisschen länger:

wishlist02.png

Wenn der Test des Client-Teams in der Pipeline des Server-Teams ausgeführt wird, dann nennt man das einen CDC-Test. Das Konzept wurde 2006 durch einen Artikel von Martin Fowler populär, ist dann aber anscheinend wieder ein bisschen in Vergessenheit geraten.

Das ist schade, denn dadurch wird effektiv verhindert, dass eine problematische Schnittstellen-Änderung im Server überhaupt live geht:

Cdcs14
Der Test des Client-Teams wird in der Server-Pipeline ausgeführt und kann so verhindern, dass eine inkompatible Server-Änderung überhaupt ins Livesystem deployed wird.

Im Kontext von otto.de wurde zunächst der Test z.B. als Jar-File ins zentrale Artifactory gelegt, von der Server-Pipeline abgeholt und dort ausgeführt. Einige Teams stellten Shell-Skripte zur Verfügung, die z.B. die richtige Version des Jars ermitteln und herunterladen.

Ein Nachteil an dieser Herangehensweise ist, dass Laufzeit-Abhängigkeiten des Tests in der Pipeline vorhanden sein müssen. Bei der Einführung von Java8 hat uns beispielsweise gestört, dass manche Server-Pipelines noch in Java6 liefen. Die Tests anderer Teams wiederum sind auf ein Chrome-Binary oder X11-Libraries angewiesen, die dann vom Server-Team in der Pipeline bereitgestellt werden müssen. Als Reaktion haben einige Teams ihre Jar-Files in Docker-Images verpackt. Das hat das Problem ein wenig reduziert, obwohl das Server-Team natürlich mit dem uralten Docker-Im-Docker Problem kämpfen muss und Versionsänderungen von Docker auch gerne mal inkompatible Änderungen mit sich bringen. Aus dem gleichen Grund war ein Experiment mit Pact auch recht schnell vorbei, als ein Team gerne eine neuere Version einführen wollte, die mit der Version des anderen Teams nicht mehr funktionierte.

Auch ist störend, dass das Server-Team die Zugangsdaten des Client-Tests in der Pipeline zur Verfügung haben muss, weil Credentials natürlich nicht im Code stehen dürfen. Diese Zugangsdaten existieren also parallel zu denen, die das Client-Team und das Server-Team ohnehin in ihrer Umgebung haben müssen, um damit Requests authentifizieren und validieren zu können.

Während sich die einen Wünsche erfüllten, wurde meine Wunschliste also fast genauso schnell länger:

wishlist03.png

Trotz der Unzulänglichkeiten und in völliger Missachtung meiner Wunschliste blieb dies fast sechs Jahre lang der Zustand unserer CDC Tests. Das war zwar nicht richtig, richtig gut, aber immerhin gut genug.

Wo wir keinesfalls hinwollen

Seit wir unser dediziertes Rechenzentrum verlassen haben, um in die Cloud zu migrieren, sind in fast allen Teams die Umgebungen der Systeme netzwerkseitig voneinander und von dem Bereich getrennt, in dem die Pipelines laufen. Das hat natürlich Konsequenzen für die Tests. Der Versuch, den alten Zustand in die neue Welt der Cloud zu übersetzen hat zu diesem Bild geführt:

Cdcs31

Damit die CDC-Tests weiter funktionieren, musste das Server-Team also teilweise auch noch Löcher in ihre Firewalls bohren. Darüber hinaus ist jetzt durch Infrastruktur-As-Code die Netzwerkstrecke zwischen Client und Server potentiell kontinuierlichen Änderungen unterworfen: eine neue Möglichkeit, versehentlich Clients kaputt zu machen.

Das Server-Team muss nun also schon …

  • … die Version des Tests ermitteln, die zu dem Client passt,
  • … den Test (ein potentiell riesiges Artefakt) downloaden,
  • … Laufzeit-Abhängigkeiten bereitstellen,
  • … Passworte im Pipeline-Bereich ablegen,
  • … Löcher in die Firewall bohren.

Dabei ist nicht mal sicher, dass die Kommunikation auch wirklich funktioniert, weil die Netzwerkstrecke sich unerwartet ändern kann.

Nachdem einige Jahre lang Ruhe um das Thema eingekehrt war, wuchs nun die Wunschliste plötzlich in unerträglichem Maße:

wishlist04.png

Das war ein guter Zeitpunkt, die Aufgabenverteilung neu zu überdenken.

Wo wir letztlich gelandet sind

Cdcs45

Das Client-Team stellt nun den Test als Lambda-Funktion, als EC2-Instanz oder als Teil ihres Systems bereit. Ein Api-Gateway ermöglicht es dem Server-Team mit einem HTTP-Request den Test zu starten. Da der Test gleichzeitig mit dem Productioncode ausgeliefert wird, hat er stets die richtige Version. Das Server-Team muss nichts mehr herunterladen und die einzige technische Abhängigkeit ist die Fähigkeit, einen HTTP-Request aus der Pipeline absetzen zu können – was dank Curl oder WGet für kein Team eine Herausforderung darstellt. Die Zugangsdaten für den Test liegen ohnehin im Keystore für den Client und nun endlich sind alle Wünsche auf der Liste in Erfüllung gegangen:

wishlist09.png

Was noch zu tun ist

Mit den neuen Möglichkeiten, auf einfache Weise Kommunikationskanäle aufzubauen, die nicht auf Requests und Responses basieren, sondern die Messaging-Systeme wie SQS, Kinesis oder Kafka benutzen, hat sich das Problemfeld plötzlich derart erweitert, dass wir noch gar keine gute, also allgemeingültige Antwort darauf gefunden haben. Ich wünsche mir neuerdings also, dass wir das auch testen können.

Zwar haben einige Teams schon mit Testnachrichten experimentiert und auch Pact wird wieder mit neuer Zuneigung betrachtet. Andere Teams benutzen die Messaging-Systeme ohnehin in beide Richtungen und können mit wenigen Anpassungen der bisherigen Strategie gute Tests bauen. Bei manchen kommt sogar die Frage auf, ob diese Art von Infrastruktur überhaupt getestet werden sollte.

Ich bin also guter Hoffnung, dass uns neben den wachsenden fachlichen Anforderungen nicht die Gründe ausgehen, intensive technische Diskussionen zu führen. Eigentlich bin ich sogar froh, dass immer neue Wünsche dazu kommen. Denn so wird mir nie langweilig :).