Über unsMediaKontaktImpressum
Guido Steinacker 20. Dezember 2016

Continuous Deployment von Microservices

Als wir 2011 mit der Neuentwicklung unseres Online-Shops otto.de starteten, haben wir uns unter anderem Continuous Delivery als Ziel gesetzt. Unsere Vorstellung war, dass wir in jedem Team jeden Morgen mit den Änderungen des Vortags in Produktion gehen könnten – nämlich dann, wenn alle Entwickler anwesend sind und wir auf eventuelle Probleme schnell reagieren können. Dieses Ziel ist längst erreicht: Mittlerweile gehen buchstäblich hunderte Deployments pro Woche in Produktion – und ständig werden es mehr.

So viele "Releases" in einer Woche: Warum und wozu? Wie funktioniert das? Was hat das mit Microservices zu tun? Und vor allem: wie stellt man dabei die Qualität sicher? Fragen dieser Art möchte ich in diesem Artikel beantworten.

Automatisierung

"Hunderte Deployments" hört sich zunächst vollkommen übertrieben und auch etwas absurd an – vor allem, wenn man klassische Release-Prozesse gewohnt ist. Aber warum eigentlich? Es ist doch zumindest vorstellbar, dass jeder Entwickler im Schnitt ein- bis zweimal täglich etwas fertigstellt, was einen, wenn auch kleinen Nutzen stiftet, also deployed werden könnte?

Selbstverständlich geht mit einem solchen Deployment nicht jedes Mal ein fix und fertiges neues Feature live: Auch ein sehr kleiner Bugfix oder eine einfache Konfigurationsänderung ist es Wert, schnell in Produktion genommen zu werden. Wenn der Aufwand dafür in einem vernünftigen Rahmen bleiben soll, sind bereits wenige Deployments pro Tag nur durch einen hohen Automatisierungsgrad zu erreichen. Bei dutzenden oder mehr Deployments muss jede Änderung im VCS (Version Control System), in aller Regel ohne manuelle Eingriffe in Produktion genommen werden können. Das bedeutet dann unter anderem:

  • Niemand muss ein Deployment von Hand anstoßen.
  • In der Regel wird von niemandem eine explizite Freigabe erteilt und es ist keine
    sonstige Absprache notwendig.
  • Es muss keine manuelle Release-Dokumentation geschrieben werden. Die Dokumentation von Releases erfolgt automatisiert über dafür vorgesehene Steps in der Deployment-Pipeline, z. B. aus standardisierten Commit-Messages der Entwickler.
  • Eine manuelle Qualitätssicherung ist in aller Regel nicht erforderlich. Nur in Ausnahmefällen erfolgt eine kurze Sichtprüfung, z. B. wenn automatisierte UI-Tests eine Abweichung melden.
  • Jedes Deployment ist "klein" und daher relativ risikolos.

Das alles klingt recht vielversprechend. Allerdings lässt sich weder Continuous Deployment noch Continuous Delivery "in fünf einfachen Schritten" implementieren – auch wenn man immer wieder Artikel findet, die das suggerieren. Egal wie viele Schritte man benötigt – einfach wird das nicht.

Self-Contained Systems & Microservices

Mit der Neuentwicklung "auf der grünen Wiese", haben wir von vornherein auf eine verteilte Architektur aus sogenannten "Vertikalen" gesetzt. Das Konzept wurde mittlerweile unter dem von Stefan Tilkov geprägten Begriff "Self-Contained Systems" (SCS)[1] populär.

Im Laufe der folgenden Jahre wurden einige SCS relativ groß und unhandlich. Anfang 2015 fingen wir daher an, bestehende SCS in Microservices zu zerlegen oder neue Features von vornherein als Microservices zu entwickeln. Die Details unserer Architektur habe ich bereits in einem früheren Artikel [2] beschrieben, weshalb ich hier nur darauf verweisen möchte.

Mit der Einführung von Microservices ist die Anzahl der Deployments pro Woche drastisch gestiegen und entwickelt sich seither kontinuierlich nach oben.

Auf den ersten Blick ist das nicht ganz selbstverständlich: Warum führen mehr Services zu  mehr Deployments, wenn doch gar nicht "mehr gearbeitet" wird? Eine einfache Erklärung  könnte lauten, dass für viele Änderungen des Codes eben mehrere Microservices angepasst werden müssen. Das ist allerdings nur selten der Fall und wäre ohnehin ein Zeichen dafür,  dass der Service-Schnitt oder die Granularität der Änderungen (zu große Stories) falsch gewählt wurde. Tatsächlich ist es eher so, dass es sehr viel leichter (und auch risikoloser) ist, einen Microservice voll automatisiert in Produktion zu nehmen, als ein mittelgroßes SCS –  oder gar einen schwergewichtigen Monolithen.

Ein weiterer Aspekt ist die höhere Produktivität von Entwicklern in einer Microservice-Architektur [3], die sich aus der besseren Wartbarkeit von Microservices im Vergleich zu größeren Monolithen ergibt. Entwickler brauchen einfach weniger Zeit für die Software-Archäologie und können effizienter die notwendigen Anpassungen in Angriff nehmen.

Continuous Deployment

Die meisten Deployments von SCS und Microservices werden bei uns ohne jede manuelle Qualitätssicherung und voll automatisiert, also kontinuierlich durchgeführt. Jedes einzelne "git push" triggered eine Deployment-Pipeline, die bei uns in etwa 20 Minuten durchlaufen ist. Ein kleiner Bugfix: Live. Eine Konfigurationsänderung: Live. Eine kleine Anpassung an
einem Feature: Live. Eine Erweiterung eines halbfertigen Features: Live. – Moment: Ein halbfertiges Feature geht live? Genau – und auf das ‚Wie’ kommen wir später noch zurück.

Wenn wir uns die verschiedenen Schritte anschauen, die eine einzelne Änderung in der Softwareentwicklung durchläuft, sieht das stark vereinfacht wie in Abb. 2 aus.

  • Code wird entwickelt, die Änderungen werden in ein VCS wie z. B. GIT bereitgestellt.
  • Der geänderte Code wird kompiliert und getestet.
  • Anschließend wird ein neues Deployment-Artefakt (in unserem Fall ein Docker-Container) gebaut und in einer Integration-Stage deployed.
  • Hier kann die neue Version der Software dann gegen die technischen und fachlichen Anforderungen getestet werden. Consumer-Driven Contract (CDC)- und Integrationstests werden ausgeführt, um das Zusammenspiel der Software mit der Systemumgebung zu prüfen.
  • Sind alle Tests erfolgreich, erfolgt ein Live-Deployment, möglicherweise gefolgt von Smoke-Tests, die den Erfolg des Deployments absichern.

Egal, wie komplex die tatsächliche Pipeline in der Praxis ausfällt (in meinem Team sind
es beispielsweise über 30 Steps in vier Stages): Das Ziel ist es, jeden Step hinter
"Develop" vollständig zu automatisieren und manuelle Eingriffe zu vermeiden.

Continuous Improvement

Continuous Deployment ist in unserem Fall das Ergebnis von mehreren Jahren, in denen wir unsere Prozesse und Tools kontinuierlich verbessert haben. Das ginge zweifellos schneller – allerdings war unser primäres Ziel ja die Entwicklung des Shops, nicht die Einführung von CD. Dass sich diese Metrik so steil nach oben entwickelt hat, war vor allem eine Folge der Einführung von Microservices und der damit verbundenen Vorteile [3] – verbunden mit einer hohen Autonomie der Teams.

2016 haben wir die Zahl der Deployments nochmals deutlich steigern können und alles deutet darauf hin, dass sich diese Entwicklung noch eine Weile fortsetzen wird. Wenn das passiert, wird es das Ergebnis eines sich kontinuierlich selbst verbessernden Arbeitssystems sein, nicht das Ziel irgendeines Projektes oder Managers: das Ergebnis von eigenverantwortlichen, autonomen Teams, regelmäßigen Retrospektiven und regem Wissensaustausch zwischen Teams. Cross-Functional Teams, die den Freiraum haben, ihre Arbeitsweisen ohne Steuerung und Kontrolle von außen ständig zu verbessern. Teams die sich nicht nur verantwortlich fühlen, sondern die tatsächlich eigenverantwortlich für ihre Software, Tools und Entwicklungsmethoden sind.

Basics

Die Grundlagen von CD [4] sind schnell aufgezählt und dürften allgemein bekannt sein:

  • Der Code für jeden Service wird in einem separaten Repository in einem VCS (z. B. GIT) verwaltet.
  • Eine Pipeline in einem geeigneten CI Server (Jenkins, TeamCity, Go oder etwa LambdaCD [5]) wird z. B. über Git Hooks gestartet, sobald eine Änderung im Repository bereitgestellt wird.
  • Für jede Änderung wird der Code ausgecheckt, kompiliert – und es wird eine Test-Suite ausgeführt. Ein einziger fehlschlagender Test führt zum Fehlschlagen der Pipeline (Zero-Bug Policy).
  • Software wird durch die Pipeline auf die Zielumgebungen deployed. Anfangs wird man dieses Deployment noch über eine manuelle Freigabe steuern, um beispielsweise eine Qualitätssicherung durchführen zu können.

Die Zero-Bug Policy ist eine Grundvoraussetzung für CD: Würde man z. B. ein Prozent
fehlschlagende Tests zulassen, hätte niemand das Vertrauen, dass man mit dem
aktuellen Stand "blind" in Produktion gehen könnte.

Zero-Downtime Deployments

Eine weitere Voraussetzung für CD ist die Fähigkeit, unterbrechungsfrei deployen zu können. Ein Deployment darf dabei keine Auswirkungen auf Kunden haben; insbesondere dürfen Requests, die gerade bearbeitet werden, durch das Deployment nicht abgebrochen werden.

Wir verwenden zwei Varianten, um dieses Ziel zu erreichen: Rolling Deployments und Blue-Green Deployments [6].

Feature Toggles

Continuous Integration "verbietet" bereits die Entwicklung auf Feature-, Bugfix- oder sonstigen Branches im VCS. Ziel ist ja gerade die kontinuierliche Integration von Änderungen. Jede Änderung am Sourcecode eines Services fließt daher in einen einzigen
"Master"-Branch.

In vielen Fällen ist das auch kein Problem: Ein Bugfix lässt sich in der Regel in einem einzelnen Commit unterbringen. Neue, noch unfertige Features werden zunächst einfach nicht in die ‚aktiven’ Teile des Codes eingebunden bzw. nicht zur Anzeige gebracht. Gelegentlich müssen wir jedoch verhindern, dass eine nur halbfertige Änderung kundenwirksam wird: Feature Toggles [9] sind hier das Mittel der Wahl.

Ein Feature Toggle ist ein Schalter, den wir im Source-Code verwenden, um entweder
den einen oder anderen Pfad zu verfolgen:

if (Features.NEW_FANCY_FEATURE.isActive()) {
 useMyFancyNewFeature();
} else {
 doTheOldBoringStuff()
}

Im Idealfall lassen sich Toggles auch über eine UI oder API zur Laufzeit eines Services umschalten.

Feature Toggles sind deutlich mächtiger als Branches, denn:

  • Sie ermöglichen eine Trennung von kundenwirksamen Livegängen von dem
    Deployment einer Applikation.
  • Sie lassen sich verwenden, um Features in Fehler- oder Hochlast-Situationen zu
    deaktivieren (Graceful Degradation of Service)
  • Toggles können auch an einzelne User, IP-Ranges oder ähnliches gebunden
    werden: beispielsweise für ausgewählte Kundengruppen, einzelne Anwender
    oder Tester.
  • A/B-Tests oder auch Gradual Rollouts lassen sich ebenfalls über Toggles
    realisieren.

Das Team sollte dabei nur den Freiraum haben, Feature Toggles schnell und unbürokratisch wieder entfernen zu können, damit der Code wartbar bleibt.

TDD und BDD

Wenn jedes git push potentiell ein Deployment auslöst: Wie sicher kann sich ein Entwickler sein, dass der Code trotz aller Tests nicht die Live-Umgebung in Mitleidenschaft zieht? Das hängt unter anderem von der Testabdeckung und der Qualität der Tests ab.

Test-Driven Development stellt Tests an den Anfang jeder Entwicklung. Zunächst werden Tests geschrieben und erst anschließend wird die eigentliche Anpassung im Code vorgenommen, damit der Test "grün" wird. Auf diese Weise wird nicht nur die Testabdeckung hoch gehalten – es entsteht auch zwangsläufig Code, der sich gut testen lässt. Erst wenn nach einer Änderung alle Tests des (Micro-)Services fehlerfrei durchlaufen, wird ein Push ins VCS durchgeführt.

Entwickelt das Team kontinuierlich Test-First, kann ein Entwickler recht sicher sein, dass eine Änderung keinen größeren Schaden anrichtet. Dank Continuous Deployment, Feature Toggles und einem guten Monitoring weiß er, dass unentdeckte Fehler sehr schnell behoben werden können. Besteht die Test-Suite vorwiegend aus Unit-Tests, können frühere Anforderungen trotzdem leicht verloren gehen, etwa wenn bei einem Refactoring strukturelle Änderungen am Code vorgenommen werden.

Behaviour-Driven Design (BDD) ist eine Erweiterung des TDD, bei dem die fachlichen Anforderungen an einen Service als (Black-Box) Acceptance-Tests in Code gegossen werden. Services, die zusätzlich zu Unit- und Component-Tests mit Acceptance-Tests abgesichert werden, sind deutlich robuster gegen fehlerhafte Änderungen.

Kleine Commits

Arbeitet man einigermaßen regelmäßig "Test First", ergeben sich automatisch sehr kleine Commits: ein neuer Test, eine Änderung, damit der Test durchläuft, ein Commit. Während im Hintergrund die komplette Test-Suite läuft, schreibt man bereits den nächsten Test.

Je kleiner die Commits, desto leichter ist es, einen möglichen Fehler zu identifizieren und desto schneller ist dieser auch wieder behoben. Ganz nebenbei entsteht dabei eine umfangreiche Test-Suite, auf die man sich wirklich verlassen kann.

Ein Service, der mehr Test-Code als "richtigen" Code hat, ist durchaus normal: auch ein Verhältnis 2:1 ist gar nicht ungewöhnlich. Test-Code erfordert mindestens die gleichen hohen Qualitätsstandards, die man auch im Produktiv-Code voraussetzt. Insbesondere die Akzeptanz- und Integrationstests sind wichtig, da sie die Aspekte prüfen, die auch nach dem nächsten Refactoring noch relevant sind.

Consumer-Driven Contracts

Microservice-Architekturen sind per Definition lose gekoppelte, verteilte Systeme. Damit lassen sie sich natürlich auch unabhängig voneinander deployen. Angenommen, ein Service A verwendet die API eines Service B. Wie stellen wir sicher, dass mit einem Deployment von B der Service A nicht in Mitleidenschaft gezogen wird?

Hier hilft das Konzept der Consumer-Driven Contract-Tests (CDC-Tests): Die Erwartungshaltung von A an die API von B wird in Form von automatisierten CDCs getestet. Die Tests werden nun in der Deployment-Pipeline von B mit ausgeführt, so dass B nur dann deployed werden kann, wenn alle Erwartungen von A erfüllt sind. Zusätzlich zu den CDCs können natürlich auch Integrationstests entwickelt werden: Diese testen aber eher die grundsätzliche Integration und nicht so sehr die Details der API.

Insgesamt haben wir mit CDC-Tests sehr gute Erfahrungen gemacht und vermeiden damit recht zuverlässig, dass mit einem Deployment die Kommunikation zwischen Services gestört wird.

Selenium, JLineup und Galen

Einige Aspekte einer Anwendung lassen sich durch klassische automatisierte Tests nur schwer prüfen: Ist nach einer Änderung im CSS die Darstellung im Browser wie erwartet? Hat eine Änderung im Frontend Features eines anderen Services beeinträchtigt?

Vor allem um Fehler dieser Art zu vermeiden, haben wir in der Vergangenheit vor einem Live-Deployment eine manuelle Qualitätssicherung durchgeführt. Mittlerweile konnten wir jedoch auch diesen Schritt automatisieren:

  • Mit Selenium [11] testen wir, ob eine Seite, die von verschiedenen Microservices dargestellt wird, insgesamt noch bedienbar ist.
  • Galen [12] wird in einigen Teams eingesetzt, um das Layout einer Seite zu prüfen.
  • Lineup [13] ist eine Eigenentwicklung, die wir während des Deployments für einen pixelgenauen Screenshot-Vergleich nutzen: Verändert das Deployment das Frontend nicht, läuft die Pipeline automatisch durch. Nur bei Änderungen wird eine manuelle Freigabe angefordert.

Auf diese Weise vermeiden wir in den allermeisten Fällen eine manuelle Sichtprüfung ohne das Risiko einzugehen, dass nach einem Deployment Seiten fehlerhaft dargestellt werden – oder schlicht nicht mehr bedienbar sind.

Deployment Gates

Es wird immer Situationen geben, in denen man aus irgendeinem Grund gerade nicht deployen möchte: Infrastrukturprobleme, extreme Last oder einfach nur, weil gerade kein Entwickler im Büro ist.

Da es in einer Microservice-Architektur mit potentiell hunderten Pipelines nicht praktikabel wäre, jede Pipeline einzeln zu deaktivieren, verwenden wir sogenannte Deployment-Gates, die über unseren Gatekeeper [10] zentral gesteuert werden können.

Die Pipelines prüfen vor dem Deployment auf eine Umgebung, ob das Gate geöffnet ist. Bei geschlossenem Gate stoppt die Pipeline und wartet, bis entweder eine manuelle Freigabe erfolgt oder bis sich das Gate wieder öffnet.

Über eine UI lassen sich Gates konfigurieren, öffnen und schließen. Die Integration in die Pipelines erfolgt über eine API des Gatekeepers. Es gibt auch die Möglichkeit "Office-Hours" zu konfigurieren: Ein Entwickler, der außerhalb der üblichen Zeiten Code eincheckt, würde damit kein automatisches Deployment auslösen. Über einen manuellen Trigger lassen sich Gates jedoch überwinden.

Dokumentation

Deployments müssen häufig dokumentiert werden. Vermutlich, weil Deployments traditionell eine "große Sache" waren und eine kontinuierliche Entwicklung auch heute eher ungewöhnlich ist... Wie auch immer, häufig gibt es die Anforderung, dass jedes Deployment dokumentiert werden muss.

In unserem Fall wird in der Deployment-Pipeline bei jedem Live-Deployment ein Service darüber informiert, welcher Service wann mit welchen Änderungen aktualisiert wurde. Die Änderungen werden über standardisierte Commit-Messages automatisiert dokumentiert (Service A wird im Rahmen von Story B durch Entwickler C deployed und ändert Feature D wie folgt...). In einem nächsten Schritt werden wir auch das Umschalten von Feature Toggles auf ähnliche Weise automatisiert dokumentieren.

Monitoring

Tests können grundsätzlich nur die Anwesenheit von Fehlern beweisen, nie deren Abwesenheit. Egal, wie umfangreich eine Test-Suite aufgebaut ist, es werden immer wieder Bugs in Produktion gehen.

Damit das Gesamtsystem trotzdem eine hohe Verfügbarkeit hat, ist ein gutes Live-Monitoring wichtig. Verändern sich nach einem Deployment Kennzahlen in der Nutzung von wichtigen Features? Bricht die Anzahl der Bestellungen plötzlich ein? Erhöht sich die Anzahl der Error-Logs? Sinken die Seitenaufrufe pro Minute?

Jedes Team hat einen oder mehrere Monitore mit Live-Dashboards der Services, die vom Team verantwortet werden. Die Teams – und nicht nur eine Operations-Abteilung – sind verantwortlich dafür, diese Metriken zu erfassen und zu überwachen. Da Deployments ausschließlich während der üblichen Arbeitszeiten erfolgen, lassen sich durch Software-Änderungen verursachte Probleme auch i. d. R. beheben, während die Entwickler noch
vor Ort sind.

Tests und Monitoring sind teilweise komplementär: je besser das Monitoring incl. schneller Korrektur (z. B. über automatische Rollbacks), desto eher kann man bei den Tests Abstriche machen.

tl;dr

Continuous Deployment ist keine theoretische Spinnerei, sondern tägliche Praxis bei OTTO und löst alltägliche Probleme der Entwicklung. CD senkt Kosten, insbesondere die "Cost of Delay", also durch eine unnötig verzögerte Bereitstellung von Änderungen verursachten Kosten. CD ist dabei kein Feature eines Tools, sondern das Ergebnis eines kontinuierlichen Verbesserungsprozesses.

Microservices unterstützen CD nicht nur, umgekehrt hilft CD auch bei der Umsetzung einer Microservice-Architektur.

Autonome, eigenverantwortliche Teams sind mehr als hilfreich, um sowohl die Komplexität von Microservices als auch die notwendige kontinuierliche Verbesserung der Entwicklungsprozesse in den Griff zu bekommen. Insbesondere benötigen die Teams den Freiraum, um für CD benötigte Tools zu integrieren – oder zu entwickeln.

Quellen
  1. Self-Contained Systems (SCS)
  2. Informatik Aktuell - Guido Steinacker: Von Monolithen und Microservices
  3. Guido Steinacker: Why Microservices?
  4. J. Humble, D. Farley, 2010: Continuous Delivery : Reliable Software Releases Through Build, Test, and Deployment Automation; Addison-Wesley Signature
  5. Lambda CD
  6. Guido Steinacker: Continuous Delivery mit Blue-Green Deployments
  7. U. Friedrichsen: The Broken Promise of Reuse
  8. Otto GitHub
  9. Guido Steinacker: Continuous Delivery mit Feature Toggles
  10. Otto Gatekeeper
  11. Selenium
  12. Galen
  13. Lineup

Autor

Guido Steinacker

Guido Steinacker ist Diplom-Informatiker. 2010 entwickelte er im Rahmen von Prototypen die Grundlagen für die neue Shop-Architektur von otto.de und ist als Executive Software-Architekt im Bereich E-Commerce tätig.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben