KAPITEL 21

Infrastruktur sicher ändern

Durch das gesamte Buch zieht sich das Motiv, Änderungen häufig und schnell vorzunehmen. Wie schon ziemlich zu Beginn in »Einwand ›Wir müssen zwischen Geschwindigkeit und Qualität entscheiden‹« auf Seite 37 erwähnt, geht es nicht darum, Systeme durch die Geschwindigkeit instabil zu machen. Geschwindigkeit ermöglicht Stabilität und umgekehrt. Das Mantra ist nicht »move fast and break things«, sondern »move fast and improve things«.

Aber Stabilität und Qualität entstehen nicht daraus, einfach nur die Geschwindigkeit zu erhöhen. Die im ersten Kapitel zitierten Forschungsergebnisse zeigen: Versuchen Sie, nur auf Geschwindigkeit oder nur auf Qualität zu optimieren, erreichen Sie beides nicht. Der Schlüssel liegt darin, auf beides zu optimieren. Konzentrieren Sie sich darauf, Änderungen häufig, schnell und sicher vornehmen zu können, Fehler schnell zu entdecken und diese zu beheben.

Alles, was in diesem Buch empfohlen wird – vom Verwenden von Code zum Bauen konsistenter Infrastruktur über das Einbinden von Tests als festen Bestandteil der Arbeitsweise bis hin zum Aufteilen von Systemen in kleinere Einheiten –, ermöglicht schnelle, häufige und sichere Änderungen.

Aber wenn Sie häufig Änderungen an der Infrastruktur vornehmen, führt das zu Herausforderungen, wenn Services nicht unterbrochen werden dürfen. Dieses Kapitel schaut sich diese Herausforderungen an und stellt Techniken vor, wie Sie sie angehen können. Die Mentalität hinter diesen Techniken ist, Änderungen nicht als Bedrohung für Stabilität und Kontinuität anzusehen, sondern die dynamische Natur moderner Infrastruktur auszunutzen. Verwenden Sie die Prinzipien, Praktiken und Techniken aus diesem Buch, um Unterbrechungen durch Änderungen zu minimieren.

Reduzieren Sie den Umfang von Änderungen

Agile, XP, Lean und ähnliche Ansätze optimieren die Geschwindigkeit und Zuverlässigkeit der Auslieferung, indem sie Änderungen in kleinen Schritten umsetzen. Es ist einfacher, eine kleine Änderung zu planen, zu implementieren, zu testen und zu debuggen als eine große, daher versuchen wir, die Batchgröße zu reduzieren.1 Natürlich müssen wir häufig größere Änderungen an unseren Systemen vornehmen, aber dazu können wir Dinge in einen kleinen Satz von Änderungen herunterbrechen, die sich nach und nach ausliefern lassen.

Als Beispiel hat das ShopSpinner-Team seine Infrastruktur zunächst mit einem einzelnen Infrastruktur-Stack gebaut. Dieser Stack enthielt das Webserver-Cluster und einen Anwendungsserver. Mit der Zeit haben die Teammitglieder mehr Anwendungsserver hinzugefügt und einige in Clustern zusammengestellt. Dann stellten sie fest, dass das Betreiben des Webserver-Clusters und aller Anwendungsserver in einem einzelnen VLAN eine schlechte Design-Entscheidung war, daher haben sie ihr Netzwerk-Design verbessert und diese Elemente auf verschiedene VLANs aufgeteilt. Zudem nahmen sie sich die Ratschläge aus diesem Buch zu Herzen und teilten ihre Infrastruktur in mehrere Stacks auf, um es einfacher zu machen, sie individuell ändern zu können.

Die ursprüngliche Implementierung war ein einzelner Stack mit einem einzelnen VLAN (siehe Abbildung 21-1).

image

Abbildung 21-1: Ausgangs-Implementierung des Beispiels – ein Stack, ein VLAN

Das Team plant, seinen Stack in mehrere Stacks aufzuteilen. Dazu gehören der shared-networking-stack und der application-infrastructure-stack, denen wir schon in den Beispielen in früheren Kapiteln begegnet sind. Zu dem Plan gehört auch noch ein web-cluster-stack, um das Container-Cluster für die Frontend-Webserver zu managen, und ein application-database-stack für die Datenbank-Instanz für jede Anwendung (Abbildung 21-2).

image

Abbildung 21-2: Plan für das Aufteilen in mehrere Stacks

Zudem wird das Team sein eines VLAN in mehrere VLANs aufteilen. Die Anwendungsserver werden aus Redundanzgründen auf diese VLANs verteilt werden (siehe Abbildung 21-3).

In Kapitel 17 sind die Designentscheidungen und ein paar Implementierungs-Patterns für das Aufteilen dieser Beispiel-Stacks beschrieben. Jetzt schauen wir uns Wege an, wie wir in einem Produktivsystem von einer Implementierung zur anderen kommen können.

image

Abbildung 21-3: Plan für das Erstellen mehrerer VLANs

Kleine Änderungen

Das größte Chaos, das ich in Code angerichtet habe, entstand immer dann, wenn ich zu viel lokal erstellte, bevor ich es in das Repository schob. Es ist aber auch zu verlockend, sich darauf zu konzentrieren, erst einmal die Arbeit für ein Element vollständig abzuschließen. Viel schwerer ist es, eine kleine Änderung umzusetzen, die Sie Ihrem Ziel nur ein kleines bisschen näherbringt. Wollen Sie große Änderungen als Abfolge kleiner Änderungen implementieren, brauchen Sie eine andere Mentalität und andere Gewohnheiten.

Zum Glück hat uns die Welt der Softwareentwicklung schon den Weg geebnet. Ich habe in diesem Buch viele Techniken beschrieben, die das Bauen von Systemen in kleinen Häppchen unterstützen – unter anderem TDD, CI und CD. Progressives Testen und Ausliefern von Code-Änderungen über eine Pipeline (beschrieben in Kapitel 8 und immer wieder im Buch erwähnt) ist eine Möglichkeit. Sie sollten dazu in der Lage sein, eine kleine Änderung an Ihrem Code vorzunehmen, sie weiter zu transportieren, Feedback zu ihrer Funktionalität zu erhalten und in die Produktivumgebung zu bringen.

Teams, die diese Techniken effektiv einsetzen, transportieren Änderungen sehr häufig. Eine einzelne Entwicklerin oder ein einzelner Entwickler kann Änderungen stündlich (oder so) ins System einspeisen, und jede Änderung wird mit der Haupt-Codebasis integriert und in einem vollständig integrierten System auf Produktivitätstauglichkeit getestet.

Die Leute verwenden verschiedene Begriffe und Techniken für das Umsetzen einer größeren Änderung als Abfolge kleinerer Änderungen:

Inkrementell

Eine inkrementelle Änderung ist eine, die einen Teil einer geplanten Implementierung hinzufügt. Sie könnten das als Beispiel beschriebene ShopSpinner-System inkrementell bauen, indem Sie immer nur einen Stack gleichzeitig implementieren. Als Erstes erstellen Sie den gemeinsam genutzten Networking-Stack. Dann fügen Sie den Web-Cluster-Stack hinzu. Und schließlich bauen Sie den Anwendungs-Infrastruktur-Stack.

Iterativ

Eine iterative Änderung sorgt für eine progressive Verbesserung des Systems. Beginnen Sie mit dem Aufbau des ShopSpinner-Systems, indem Sie eine grundlegende Version aller drei Stacks bauen. Dann sorgen Sie für eine Abfolge von Änderungen, wobei jede die Funktionalität der Stacks erweitert.

Walking Skeleton

Ein Walking Skeleton (https://oreil.ly/1I7bC) ist eine grundlegende Implementierung der wichtigsten Teile eines neuen Systems, die Sie implementieren, um das allgemeine Design und die Struktur zu überprüfen.1 Die Leute erstellen oft ein Walking Skeleton für ein Infrastruktur-Projekt parallel zu einer entsprechenden ersten Implementierung von Anwendungen, die darauf laufen werden, sodass die Teams sehen können, wie Ausliefern, Deployen und Operations funktionieren werden. Die initiale Implementierung und Auswahl von Tools und Services für das Skeleton sind oft nicht die, die auch langfristig zum Einsatz kommen sollen. So können Sie beispielsweise eine umfangreiche Monitoring-Lösung einplanen, Ihr Walking Skeleton aber mit einfacheren Services umsetzen, die von Ihrem Cloud-Provider angeboten werden.

Refaktorieren

Refaktorieren (https://refactoring.com) beinhaltet das Ändern des Designs eines Systems oder einer Komponente, ohne das entsprechende Verhalten zu ändern. Das geschieht häufig, um die Grundlage für Änderungen zu legen, die dann wiederum das Verhalten ändern.2 Durch das Refaktorieren wird eventuell der Code klarer, sodass er sich einfacher ändern lässt, oder er wird so umorganisiert, dass er besser zu den geplanten Änderungen passt.

Refaktorieren – ein Beispiel

Das ShopSpinner-Team entscheidet sich dazu, seinen aktuellen Stack in mehrere Stacks und Stack-Instanzen aufzuteilen. Zur geplanten Implementierung gehört eine Stack-Instanz für das Container-Cluster, das die Webserver hostet, und eine andere für die gemeinsam genutzte Networking-Infrastruktur. Das Team wird auch jeweils ein Stack-Paar für jeden Service haben – einen für den Anwendungsserver und das zugehörige Networking und einen für die Datenbank-Instanz für diesen Service (siehe Abbildung 21-4).

image

Abbildung 21-4: Plan zum Aufteilen eines Stacks

Die Teammitglieder wollen zudem ihr Container-Cluster-Produkt ersetzen und statt eines Kubernetes-Clusters, das sie selbst auf virtuelle Maschinen deployen, nun Containers as a Service nutzen, das von ihrem Cloud-Anbieter bereitgestellt wird (siehe »Lösungen für Anwendungs-Cluster« auf Seite 270).

Das Team entscheidet sich dazu, seine aufgeteilte Architektur inkrementell zu implementieren. Der erste Schritt besteht darin, das Container-Cluster in seinen eigenen Stack auszulagern und dann das Container-Produkt im Stack zu ersetzen (Abbildung 21-5).

Dieser Plan ist ein Beispiel für ein Refaktorieren, um eine Änderung zu ermöglichen. Die Container-Cluster-Lösung lässt sich einfacher austauschen, wenn sie sich isoliert in ihrem eigenen Stack befindet, als wenn sie Teil eines größeren Stacks mit anderer Infrastruktur ist. Lagern die Teammitglieder das Cluster in seinen eigenen Stack aus, können sie dessen Integrationspunkte für den Rest des Systems definieren. Sie können Tests und andere Validierungen schreiben, die die Separation und die Integration sauber halten. Damit kann das Team davon ausgehen, dass es die Inhalte des Cluster-Stacks sicher austauschen kann.

image

Abbildung 21-5: Plan zum Auslagern und Ersetzen des Container-Clusters

image

Neu bauen

Statt Ihr bestehendes Produktivsystem inkrementell zu ändern, könnten Sie die neue Version Ihres Systems auch getrennt bauen und die Anwenderinnen und Anwender dorthin umziehen, wenn Sie fertig sind. Vielleicht ist es einfacher, die neue Version zu bauen, wenn Sie Ihr Systemdesign und die Implementierung massiv verändern. Aber auch dann ist es nützlich, Ihre Arbeit so schnell wie möglich produktiv zu bekommen. Es ist weniger riskant, immer nur einen Teils Ihres Systems auszulagern und neu zu bauen, als alles auf einmal zu bauen. Zudem lassen sich so Verbesserungen schneller testen und ausliefern. Selbst ein grundlegender Rebuild kann also inkrementell erfolgen.

Unvollständige Änderungen in die Produktivumgebung übernehmen

Wie können Sie eine größere Änderung an Ihr Produktivsystem als Folge kleinerer, inkrementeller Änderungen ausliefern und den Service dabei lauffähig halten? Manche dieser kleineren Änderungen sind für sich allein eventuell gar nicht sinnvoll. Es mag nicht praktikabel sein, bestehende Funktionalität zu entfernen, bevor nicht der gesamte Satz an Änderungen vollständig ist.

In »Refaktorieren – ein Beispiel« auf Seite 406 waren zwei inkrementelle Schritte zu sehen. Erst wurde ein Container-Cluster aus einem Stack in seinen eigenen Stack ausgelagert, dann wurde die Cluster-Lösung im neuen Stack ausgetauscht. Jeder dieser Schritte ist groß, daher würde er eventuell als Abfolge kleinerer Code-Pushes umgesetzt werden.

Aber viele der kleineren Änderungen allein würden das Cluster unbenutzbar machen. Also müssen Sie Wege finden, diese kleinen Schritte umzusetzen und dabei den bestehenden Code und die bestehende Funktionalität verfügbar zu halten. Abhängig von Ihrer Situation gibt es dafür verschiedene Techniken.

Parallele Instanzen

Der zweite Schritt des Beispiels zum Ersetzen des Clusters beginnt damit, dass die ursprüngliche Container-Lösung ihren eigenen Stack hat, und er endet mit der neuen Container-Lösung (siehe Abbildung 21-6:).

image

Abbildung 21-6: Die Cluster-Lösung ersetzen

Die bestehende Lösung ist eine als Paket verfügbare Kubernetes-Distribution namens KubeCan.1 Das Team wechselt zu FKS, einem gemanagten Cluster-Service, der von ihrer Cloud-Plattform angeboten wird.2 In »Lösungen für Anwendungs-Cluster« auf Seite 270 finden Sie mehr zu Clusters as a Service und als Pakete angebotene Cluster-Distributionen.

Es ist nicht praktikabel, das KubeCan-Cluster in kleinen Schritten in ein FKS-Cluster umzuwandeln. Aber das Team kann die beiden Cluster parallel betreiben. Es gibt ein paar Möglichkeiten, die beiden Container-Stacks mit einer einzelnen Instanz des ursprünglichen Stacks parallel laufen zu lassen.

Eine Option ist, einen Parameter für den Haupt-Stack zu haben, um auswählen zu können, mit welchem Cluster-Stack er sich integrieren soll (siehe Abbildung 21-7).

So ist einer der Stacks aktiv und kümmert sich um die Live-Workload. Der zweite Stack ist inaktiv, aber vorhanden. Das Team kann den zweiten Stack in einer vollständig lauffähigen Umgebung testen, damit üben, Auslieferungs-Pipeline und Testsuite entwickeln und in jeder Umgebung mit anderen Teilen der Infrastruktur integrieren.

image

Abbildung 21-7: Ein Stack ist aktiv, einer inaktiv.

image

Warum die alte Container-Lösung überhaupt auslagern?

Da wir ja am Ende einen eigenen Stack mit der neuen Container-Lösung haben, hätten wir durchaus das Auslagern der alten Lösung in ihren eigenen Stack überspringen können. Dann würden wir nur den neuen Stack mit der neuen Lösung von Grund auf bauen.

Aber indem wir die alte Lösung auslagern, ist es leichter, sicherzustellen, dass unsere neue Lösung zum Verhalten der alten Lösung passt. Der extrahierte Stack definiert klar, wie das Cluster mit anderer Infrastruktur integriert. Indem wir den ausgelagerten Stack in der Produktivumgebung einsetzen, garantieren wir, dass die Integrationspunkte korrekt sind.

Mit automatisierten Tests und einer neuen Pipeline für den extrahierten Stack stellen wir sicher, dass wir es sofort bemerken, wenn eine unserer Änderungen etwas kaputt macht.

Belassen wir die alte Cluster-Lösung im ursprünglichen Stack und bauen die neue getrennt davon, wird der Austausch disruptiv sein. Wir werden erst am Ende feststellen, ob wir ein inkompatibles Design geschaffen oder eine entsprechende Implementierungsentscheidung getroffen haben. Es würde Zeit kosten, den neuen Stack mit anderen Teilen der Infrastruktur zu integrieren, ihn zu testen, zu debuggen und Probleme zu beheben.

Eine weitere Option ist, beide Stacks mit dem Haupt-Stack zu integrieren (siehe Abbildung 21-8).

image

Abbildung 21-8: Jede Cluster-Implementierung nutzt ihre eigene Stack-Instanz.

Auf diese Art und Weise können Sie Ihre Workload auf die verschiedenen Cluster-Stacks lenken. Dabei lässt sie sich unterschiedlich aufteilen:

Workload-Prozentsatz

Leiten Sie jeweils einen Teil der Workload auf jeden Stack. Normalerweise kümmert sich der alte Stack zunächst um einen Großteil der Last, und der neue Stack übernimmt nur einen kleinen Prozentsatz, um zu ermitteln, wie gut er funktioniert. Läuft es mit dem neuen Stack gut, können Sie die Last mit der Zeit hochregeln. Nachdem der neue Stack erfolgreich 100 Prozent der Last managt und alle dazu bereit sind, können Sie den alten Stack abbauen. Diese Option geht davon aus, dass der neue Stack alle Fähigkeiten des alten Stacks besitzt und dass es keine Probleme gibt, wenn Daten oder Messages auf die Stacks verteilt werden.

Service-Migration

Migrieren Sie die Services einen nach dem anderen auf das neue Cluster. Die Workloads im Haupt-Stack, wie zum Beispiel Netzwerkverbindungen oder Messages, werden jeweils auf die Stack-Instanz geleitet, auf der der entsprechende Service läuft. Diese Option ist besonders dann nützlich, wenn Sie Service-Anwendungen anpassen müssen, um sie auf den neuen Stack umziehen lassen zu können. Oft ist eine komplexere Integration erforderlich – vielleicht sogar zwischen den alten und den neuen Cluster-Stacks. Diese Komplexität kann erforderlich sein, wenn Sie ein komplexes Service-Portfolio migrieren.1

Benutzer-Aufteilung

In manchen Fällen werden Gruppen von Benutzerinnen und Benutzern auf verschiedene Stack-Implementierungen verteilt. Testende und interne Anwenderinnen und Anwender sind oft die erste Gruppe. Sie können explorative Tests ausführen und das neue System unter die Lupe nehmen, bevor man das Risiko eingeht, es auf »echte« Kunden und Kundinnen loszulassen. In manchen Fällen können in einer zweiten Gruppe Kundinnen und Kunden sein, die sich dazu bereiterklären, Alpha-Tests durchzuführen oder eine Service-Preview zu bekommen. Das kann sinnvoller sein, wenn der Service, der auf dem neuen Stack läuft, so anders ist, dass dies von außen bemerkt werden wird.

Das parallele Betreiben neuer und alter Teile des Systems ist eine Form des Branch by Abstraction (https://oreil.ly/N_ej8). Verschieben Sie progressiv Teile einer Workload auf die neuen Elemente eines Systems, ist das ein Canary Release (https://oreil.ly/FTS0b). Dark Launching (https://oreil.ly/PZmch) beschreibt die Vorgehensweise, neue System-Features in die Produktivumgebung zu transportieren, sie aber dort noch nicht der produktiven Workload auszusetzen, sodass Sie sie in Ruhe testen können.

Abwärtskompatible Transformationen

Auch wenn manche Änderungen das Bauen und Betreiben einer neuen Komponente parallel zur alten erfordern, bis alles erledigt ist, können Sie viele Änderungen in einer Komponente vornehmen, ohne Benutzer oder Kundinnen zu beeinträchtigen.

Selbst wenn Sie das ändern oder ergänzen, was Sie nach außen anbieten, können Sie häufig neue Integrationspunkte anbieten und die bestehenden unverändert lassen. Kundinnen und Kunden können dann abhängig von ihren eigenen Anforderungen auf die neuen Integrationspunkte wechseln.

So plant das ShopSpinner-Team beispielsweise, seinen shared-networking-stack von einem einzelnen VLAN auf drei VLANs umzubauen (Abbildung 21-9).

image

Abbildung 21-9: Änderung von einem einzelnen VLAN zu drei VLANs

Consumer-Stacks, unter anderem application-infrastructure-stack, integrieren mit dem einen VLAN, das vom Networking-Stack gemangt wird, über eine der Discovery-Methoden aus »Abhängigkeiten zwischen Stacks erkennen« auf Seite 329. Der Code des shared-networking-stack exportiert die VLAN-Kennung für seine Consumer-Stacks, damit diese es finden können:

vlans:

- main_vlan

address_range: 10.2.0.0/8

export:

- main_vlan: main_vlan.id

Die neue Version von shared-networking-stack erstellt drei VLANs und exportiert ihre Kennungen unter neuen Namen. Sie exportiert zudem eine der VLAN-Kennungen über den alten Namen:

vlans:

- appserver_vlan_A

address_range: 10.1.0.0/16

- appserver_vlan_B

address_range: 10.2.0.0/16

- appserver_vlan_C

address_range: 10.3.0.0/16

export:

- appserver_vlan_A: appserver_vlan_A.id

- appserver_vlan_B: appserver_vlan_B.id

- appserver_vlan_C: appserver_vlan_C.id

# Deprecated

- main_vlan: appserver_vlan_A.id

Durch das Behalten der alten Kennungen funktioniert der modifizierte Networking-Stack weiterhin für den Consumer-Infrastruktur-Code. Der Consumer-Code sollte so angepasst werden, dass er die neuen Kennungen nutzt, und sobald alle Abhängigkeiten zur alten Kennung Geschichte sind, kann diese aus dem Networking-Stack-Code entfernt werden.

Feature Toggles

Nimmt man eine Änderung an einer Komponente vor, muss man häufig die bestehende Implementierung nutzen, bis man mit der Änderung fertig ist. Manche erstellen einen Code-Branch in der Versionsverwaltung, arbeiten in diesem Branch an der neuen Änderung und nutzen den alten Branch in der Produktivumgebung. Die Probleme dabei sind aber:

Es ist effektiver, an Änderungen an der Haupt-Codebasis ohne Branching zu arbeiten. Sie können Feature Toggles nutzen, um die Code-Implementierung für verschiedene Umgebungen zu wechseln. Schalten Sie in manchen Umgebungen auf den neuen Code, um Ihre Arbeit zu testen, nutzten Sie aber den bestehenden Code für Umgebungen auf dem Weg zur Produktiv-Instanz. Verwenden Sie einen Stack-Konfigurationsparameter (wie in Kapitel 7 beschrieben), um festzulegen, welcher Teil des Codes für eine bestimmte Instanz angewendet werden soll.

Ist das ShopSpinner-Team damit fertig, seinen shared-networking-stack um VLANs zu ergänzen, muss es den application-infrastructure-stack so anpassen, dass er die neuen VLANs nutzt. Die Teammitglieder stellen fest, dass das doch keine so einfache Änderung ist, wie sie zu Beginn dachten.

Der Anwendungs-Stack definiert anwendungsspezifische Netzwerk-Routen, Load Balancer VIPs und Firewall-Regeln. Die sind komplexer, wenn Anwendungsserver in unterschiedlichen VLANs gehostet werden, als wenn sie in einem einzelnen VLAN laufen.

Es wird die Teammitglieder ein paar Tage kosten, den Code für diese Änderung zu implementieren und zu testen. Das ist nicht lang genug, um das Einrichten eines eigenen Stacks zu rechtfertigen (wie in »Parallele Instanzen« auf Seite 408 beschrieben). Aber man hat vor, inkrementelle Änderungen in das Repository zu pushen, sobald sie funktionieren, daher kann das Team auf diesem Weg kontinuierliches Feedback aus den Tests erhalten – auch aus den Systemintegrations-Tests.

Das Team entscheidet sich dazu, den application-infrastructure-stack um einen Konfigurationsparameter zu erweitern, der die unterschiedlichen Teile des Stack-Codes wählt und damit festlegt, ob ein einzelnes VLAN oder mehrere VLANs genutzt werden sollen.

Dieses Snippet des Quellcodes für den Stack nutzt drei Variablen – appserver_A_vlan, appserver_B_vlan und appserver_C_vlan –, um festzulegen, welches VLAN welchem Anwendungsserver zugewiesen werden soll. Der Wert für jede dieser Variablen wird abhängig vom Wert des Feature-Toggle-Parameters toggle_use_multiple_vlans gesetzt:

input_parameters:

name: toggle_use_multiple_vlans

default: false

variables:

- name: appserver_A_vlan

value:

$IF(${toggle_use_multiple_vlans} appserver_vlan_A ELSE main_vlan)

- name: appserver_B_vlan

value:

$IF(${toggle_use_multiple_vlans} appserver_vlan_B ELSE main_vlan)

- name: appserver_C_vlan

value:

$IF(${toggle_use_multiple_vlans} appserver_vlan_C ELSE main_vlan)

virtual_machine:

name: appserver-${SERVICE}-A

memory: 4GB

address_block: ${appserver_A_vlan}

virtual_machine:

name: appserver-${SERVICE}-B

memory: 4GB

address_block: ${appserver_B_vlan}

virtual_machine:

name: appserver-${SERVICE}-C

memory: 4GB

address_block: ${appserver_C_vlan}

Wird toggle_use_multiple_vlans auf false gesetzt, werden die ganzen appserver_X_vlan-Parameter so gesetzt, dass die alte VLAN-Kennung main_vlan genutzt wird. Ist der Toggle true, wird jede der Variablen auf eine der neuen VLAN-Kennungen gesetzt.

Der gleiche Toggle-Parameter wird in anderen Teilen des Stack-Codes genutzt, in denen das Team an der Konfiguration des Routings und anderen kniffligen Elementen arbeitet.

image

Feature Toggles

In »Feature Toggles (aka Feature Flags)« von Pete Hodgson (https://oreil.ly/sIUNA) finden Sie Tipps zum Einsatz von Feature Toggles und Beispiele als Quellcode. Ich möchte da noch ein paar Empfehlungen ergänzen.

Zuerst: Versuchen Sie, die Anzahl an verwendeten Feature Toggles minimal zu halten. Feature Toggles und Bedingungen machen den Code unordentlicher, und er lässt sich schlechter verstehen, warten und debuggen. Sorgen Sie dafür, dass es die Toggles nur kurz gibt. Entfernen Sie so bald wie möglich Abhängigkeiten zur alten Implementierung und entfernen Sie die Toggles und den konditionalen Code. Jeder Feature Toggle, der länger als ein paar Wochen überlebt, ist vermutlich ein Konfigurationsparameter.

Benennen Sie Feature Toggles anhand dessen, was sie tun. Vermeiden Sie uneindeutige Namen wie new_networking_code. Der obige Beispiel-Toggle toggle_use_multiple_vlans macht beim Lesen klar, dass es sich um einen Feature Toggle und nicht um einen Konfigurationsparameter handelt. Man erfährt, dass man damit mehrere VLANs aktiviert, sodass man weiß, was der Toggle tut.

Und der Name macht klar, auf welche Art und Weise der Toggle funktioniert. Lesen Sie einen Namen wie toggle_multiple_vlans oder – schlimmer noch – toggle_vlans –, können Sie nicht sicher sein, ob der Code für mehrere VLANs aktiviert oder deaktiviert wird. Das führt zu Fehlern, wenn jemand die Bedingung falsch herum in seinem Code einsetzt.

Live-Infrastruktur ändern

Diese Techniken und Beispiele zeigen, wie man Infrastruktur-Code ändert. Kniffliger kann es sein, laufende Infrastruktur-Instanzen zu ändern – insbesondere, wenn man Ressourcen ändert, die von anderer Infrastruktur genutzt werden.

Wendet das ShopSpinner-Team beispielsweise die Änderung auf den shared-networking-stack-Code an, der das eine VLAN durch drei VLANs ersetzt (siehe »Abwärtskompatible Transformationen« auf Seite 411), muss man sich im Klaren darüber sein, was mit Ressourcen aus anderen Stacks geschieht, die dem ersten VLAN zugewiesen sind (siehe Abbildung 21-10).

Durch das Anwenden des Networking-Codes wird main_vlan zerstört, das aber drei Server-Instanzen enthält. In einer Live-Umgebung wird das Zerstören dieser Server oder das Entfernen aus dem Netzwerk dazu führen, dass darauf laufende Services unterbrochen werden.

image

Abbildung 21-10: Ändern von Networking-Strukturen, die im Einsatz sind

Die meisten Infrastruktur-Plattformen werden es ablehnen, eine Networking-Struktur zu zerstören, der Server-Instanzen zugeordnet sind, und die Operation wird fehlschlagen. Entfernt oder verändert die Code-Änderung, die Sie anwenden, andere Ressourcen, implementiert die Operation diese Änderungen an der Instanz vielleicht und belässt die Umgebung in einem Zwischenzustand zwischen der alten und neuen Version Ihres Stack-Codes. Das ist fast immer ausgesprochen unerwünscht.

Es gibt ein paar Möglichkeiten, mit solchen Änderungen an Live-Infrastruktur umzugehen. Eine ist, das alte VLAN main_vlan beizubehalten und die beiden neuen VLANs appserver_vlan_B und appserver_vlan_C hinzuzufügen.

So haben Sie weiterhin wie gewünscht drei VLANs, von denen eines allerdings anders benannt ist als die anderen. Behalten Sie das bestehende VLAN bei, können Sie eventuell auch andere Aspekte von ihm nicht ändern, wie zum Beispiel dessen IP-Adressbereich. Vielleicht gehen Sie hier weitere Kompromisse ein, indem Sie beispielsweise das alte VLAN kleiner halten als die beiden neuen.

Solche Kompromisse sind schlecht, denn sie führen zu inkonsistenten Systemen und zu Code, der sich nur schlecht warten und debuggen lässt.

Sie können andere Techniken nutzen, um Live-Systeme zu ändern und sie dabei in einem sauberen, konsistenten Zustand zu belassen. Eine ist, Infrastruktur-Ressourcen per Infrastruktur-Chirurgie zu verändern. Bei der anderen erweitern Sie Infrastruktur-Ressourcen und reduzieren sie später wieder.

Infrastruktur-Chirurgie

Manche Stack-Management-Tools, wie zum Beispiel Terraform, ermöglichen Ihnen den Zugriff auf die Datenstrukturen, die die Infrastruktur-Ressourcen auf Code abbilden. Das sind die gleichen Datenstrukturen, die auch im Stack-Data-Lookup-Pattern zur Dependency Discovery genutzt werden (siehe »Pattern: Stack Data Lookup« auf Seite 333).

Manche (aber nicht alle) Stack-Tools bieten die Option, ihre Datenstrukturen zu bearbeiten. Sie können das ausnutzen, um Änderungen an der Live-Infrastruktur vorzunehmen.

Das ShopSpinner-Team kann sein fiktives stack-Tool nutzen, um seine Stack-Datenstrukturen zu bearbeiten. Mitglieder des Teams werden das ausnutzen, um ihre Produktivumgebung so zu ändern, dass sie die drei neuen VLANs verwendet. Erst erstellen sie eine zweite Instanz ihres shared-networking-stack mit der neuen Version ihres Codes (siehe Abbildung 21-11).

image

Abbildung 21-11: Parallele Instanzen des produktiven Networking-Stacks

Jede der drei Instanzen des Stacks – die application-infrastructure-stack-Instanz sowie die alte und neue Instanz des shared-networking-stack – hat eine Datenstruktur, die angibt, welche Ressourcen in der Infrastruktur-Plattform zu diesem Stack gehören (siehe Abbildung 21-12).

image

Abbildung 21-12: Jede Stack-Instanz hat ihre eigene Stack-Datenstruktur.

Das ShopSpinner-Team wird main_vlan aus der Datenstruktur der alten Stack-Instanz in die Datenstruktur für die neue Stack-Instanz umziehen. Dann wird das Team damit appserver_vlan_A ersetzen.

Das VLAN in der Infrastruktur-Plattform wird sich nicht ändern, und die Server-Instanzen werden nicht angefasst. Diese Änderungen wirken sich nur in den Datenstrukturen des Stack-Tools aus.

Das Team nutzt das Stack-Tool, um main_vlan vom alten Stack in die neue Stack-Instanz zu verschieben:

$ stack datafile move-resource \

source-instance=shared-networking-stack-production-old \

source-resource=main_vlan \

destination-instance=shared-networking-stack-production-new

Success: Resource moved

Im nächsten Schritt wird appserver_vlan_A entfernt. Es hängt vom verwendeten Stack-Management-Tool ab, wie das umgesetzt werden kann. Der fiktive stack-Befehl macht das netterweise sehr einfach. Mit dem folgenden Befehl wird das VLAN in der Infrastruktur-Plattform zerstört und aus den Datenstruktur-Dateien entfernt:

$ stack datafile destroy-resource \

instance=shared-networking-stack-production-new \

resource=appserver_vlan_A

Success: Resource destroyed and removed from the datafile

Beachten Sie, dass die Teammitglieder appserver_vlan_A nicht aus dem Stack-Quellcode entfernt haben – wird der Code nun auf die Instanz angewendet, wird das VLAN wieder neu erstellt. Aber das tun sie nicht. Stattdessen führen sie einen Befehl zum Umbenennen der Ressource main_vlan aus, die sie aus der alten Stack-Instanz umgezogen haben:

$ stack datafile rename-resource \

instance=shared-networking-stack-production-new \

from=main_vlan \

to=appserver_vlan_A

Success: Resource renamed in the datafile

Wendet das Team den shared-networking-code auf die neue Instanz an, sollte sich nichts ändern. Denn alles im Code ist in der Instanz schon vorhanden.

Beachten Sie, dass die Fähigkeit, Ressourcen zu bearbeiten und zwischen Stacks zu verschieben, stark vom Stack-Management-Tool abhängt. Die meisten von den Cloud-Anbietern bereitgestellten Tools ermöglichen es – zumindest aktuell – nicht, Stack-Datenstrukturen zu bearbeiten.1

Man macht leicht einen Fehler, wenn man Stack-Datenstrukturen per Hand bearbeitet, daher ist das Risiko für Ausfälle groß. Sie könnten ein Skript schreiben, um die Befehle zu implementieren, und dieses in Upstream-Umgebungen testen. Aber diese Änderungen sind nicht idempotent. Die Teamkollegen gehen von einem bestimmten Ausgangszustand aus, und das Ausführen des Skripts kann unvorhersehbare Ergebnisse liefern, wenn etwas anders ist.

Es kann zum Debuggen nützlich sein, Einblick in Stack-Datenstrukturen zu haben, aber Sie sollten es vermeiden, diese zu bearbeiten. Manchmal kann es tatsächlich notwendig sein, solche Strukturen anzupassen, um einen Ausfall aufzulösen. Aber der Druck solcher Situationen kann dafür sorgen, dass Fehler noch wahrscheinlicher werden. Sie sollten Stackdaten nicht regelmäßig anpassen. Immer dann, wenn Sie die Strukturen verändern, sollte Ihr Team danach ein Blameless Postmortem (https://oreil.ly/qbx7x) durchführen, um sich darüber klar zu werden, wie die Situation das nächste Mal vermieden werden kann.

Sicherer werden Änderungen an Live-Infrastruktur durch Erweitern und Zusammenziehen vorgenommen.

Expand and Contract

Infrastruktur-Teams nutzen das Expand-and-Contract-Pattern (auch als Parallel Change bezeichnet, https://oreil.ly/InnHG), um Schnittstellen zu ändern, ohne für Ausfälle bei Consumern zu sorgen. Die Idee ist, dass zum Ändern der Schnittstelle eines Producers zwei Schritte gehören: Erst wird der Provider geändert, dann kommen die Consumer an die Reihe. Das Expand-and-Contract-Pattern entkoppelt diese Schritte.

Kern des Pattern ist, zuerst die neue Ressource hinzuzufügen, während die bestehende erhalten bleibt, dann die Consumer zur neuen Ressource wechseln zu lassen und schließlich die alte, jetzt ungenutzte Ressource zu entfernen. Jede dieser Änderungen wird über eine Pipeline ausgeliefert (siehe »Infrastruktur-Delivery-Pipelines« auf Seite 152), daher wird sie umfassend getestet.

Nehmen Sie eine Änderung per Expand and Contract vor, ähnelt das einer abwärtskompatiblen Transformation (siehe »Abwärtskompatible Transformationen« auf Seite 411). Bei dieser Technik wurde die alte Ressource ersetzt und die alte Schnittstelle auf eine der neuen Ressourcen umgebogen. Aber das Anwenden des neuen Codes auf eine laufende Instanz würde dafür sorgen, dass die alte Ressource zerstört wird, was entweder dazu führen würde, dass alle Kundinnen und Kunden, die damit verbunden sind, abgeschnitten sind, oder die Änderung würde komplett fehlschlagen. Daher sind ein paar zusätzliche Schritte erforderlich.

Der erste Schritt des ShopSpinner-Teams beim Einsatz von Expand and Contract für seine Änderung am VLAN ist daher, die neuen VLANs zum shared-networking-stack hinzuzufügen, das alte main_vlan aber beizubehalten:

vlans:

- main_vlan

address_range: 10.2.0.0/8

- appserver_vlan_A

address_range: 10.1.0.0/16

- appserver_vlan_B

address_range: 10.2.0.0/16

- appserver_vlan_C

address_range: 10.3.0.0/16

export:

- main_vlan: main_vlan.id

- appserver_vlan_A: appserver_vlan_A.id

- appserver_vlan_B: appserver_vlan_B.id

- appserver_vlan_C: appserver_vlan_C.id

Anders als bei der Technik paralleler Instanzen (siehe »Parallele Instanzen« auf Seite 408) oder bei der Infrastruktur-Chirurgie (siehe »Infrastruktur-Chirurgie« auf Seite 417) fügt das ShopSpinner-Team keine zweite Instanz des Stacks hinzu, sondern ändert nur die bestehende Instanz.

Durch das Anwenden dieses Codes sind die bestehenden Consumer-Instanzen nicht betroffen – sie sind weiterhin mit dem main_vlan verbunden. Das Team kann neue Ressourcen zu den neuen VLANs hinzufügen und Änderungen an den Consumern vornehmen, um sie ebenfalls dorthin umzuziehen.

Es hängt von der spezifischen Infrastruktur und der Plattform ab, wie Sie die Consumer-Ressourcen für den Einsatz der neuen Ressourcen anpassen. In manchen Fällen können Sie die Definition für die Ressource aktualisieren, damit diese mit der neuen Provider-Schnittstelle verbunden wird. In anderen müssen Sie eventuell die Ressource zerstören und neu bauen.

Das ShopSpinner-Team kann bestehende Instanzen virtueller Server nicht den neuen VLANs zuweisen. Aber das Team kann das Expand-and-Contract-Pattern nutzen, um die Server zu ersetzen. Der application-infrastructure-stack-Code definiert jeden Server mit einer statischen IP-Adresse, die Traffic auf diesen Server routet:

virtual_machine:

name: appserver-${SERVICE}-A

memory: 4GB

vlan: external_stack.shared_network_stack.main_vlan

static_ip:

name: address-${SERVICE}-A

attach: virtual_machine.appserver-${SERVICE}-A

Der erste Schritt ist nun, eine neue Server-Instanz hinzuzufügen, die mit dem neuen VLAN verbunden ist:

virtual_machine:

name: appserver-${SERVICE}-A2

memory: 4GB

vlan: external_stack.shared_network_stack.appserver_vlan_A

virtual_machine:

name: appserver-${SERVICE}-A

memory: 4GB

vlan: external_stack.shared_network_stack.main_vlan

static_ip:

name: address-${SERVICE}-A

attach: virtual_machine.appserver-${SERVICE}-A

Die erste virtual_machine-Anweisung erzeugt eine neue Server-Instanz namens appserver-${SERVICE}-A2. Die Pipeline des Teams liefert diese Änderung auf jede Umgebung aus. Die neue Server-Instanz wird jetzt noch nicht verwendet, auch wenn das Team schon ein paar automatisierte Tests hinzufügen kann, um zu zeigen, dass sie funktioniert.

Der nächste Schritt ist nun, den User-Traffic auf die neue Server-Instanz umzuschalten. Das Team nimmt eine weitere Änderung am Code vor und verändert die Anweisung static_ip:

virtual_machine:

name: appserver-${SERVICE}-A2

memory: 4GB

vlan: external_stack.shared_network_stack.appserver_vlan_A

virtual_machine:

name: appserver-${SERVICE}-A

memory: 4GB

vlan: external_stack.shared_network_stack.main_vlan

static_ip:

name: address-${SERVICE}-A

attach: virtual_machine.appserver-${SERVICE}-A2

Durch das Transportieren dieser Änderung durch die Pipeline wird der neue Server aktiv, und der alte Server erhält keinen Traffic mehr. Das Team kann nun sicherstellen, dass alles wie gewünscht läuft, und die Änderung problemlos auf den alten Server zurückrollen, wenn etwas schiefgegangen ist.

Sieht das Team, dass der neue Server gut läuft, kann es den alten Server aus dem Stack-Code entfernen:

virtual_machine:

name: appserver-${SERVICE}-A2

memory: 4GB

vlan: external_stack.shared_network_stack.appserver_vlan_A

static_ip:

name: address-${SERVICE}-A

attach: virtual_machine.appserver-${SERVICE}-A2

Wurde diese Änderung durch die Pipeline geschoben und auf alle Umgebungen angewendet, hat application-infrastructure-stack keine Abhängigkeit mehr zu main_vlan in shared-networking-stack. Nachdem die gesamte Consumer-Infrastruktur umgezogen wurde, kann das ShopSpinner-Team main_vlan aus dem Code für den Provider-Stack entfernen:

vlans:

- appserver_vlan_A

address_range: 10.1.0.0/16

- appserver_vlan_B

address_range: 10.2.0.0/16

- appserver_vlan_C

address_range: 10.3.0.0/16

export:

- appserver_vlan_A: appserver_vlan_A.id

- appserver_vlan_B: appserver_vlan_B.id

- appserver_vlan_C: appserver_vlan_C.id

Die VLAN-Änderung ist nun abgeschlossen, und die letzten Überbleibsel von main_vlan sind damit beseitigt.1

Zero-Downtime-Änderungen

Viele der in diesem Kapitel beschriebenen Techniken zeigen, wie Sie eine Änderung inkrementell implementieren. Idealerweise würden Sie die Änderung auf bestehende Infrastruktur anwenden, ohne dass die von ihr angebotenen Services unterbrochen werden. Bei manchen der Änderungen werden ganz unvermeidlich Ressourcen zerstört oder zumindest so geändert werden müssen, dass sie Services unterbrechen. Für solche Situationen gibt es ein paar bewährte Techniken.

Blue-Green-Änderungen

Bei einer Blue-Green-Änderung wird eine neue Instanz erstellt, zu dieser gewechselt und dann die alte Instanz entfernt. Das entspricht konzeptionell Expand and Contract (siehe »Expand and Contract« auf Seite 419), bei dem Ressourcen innerhalb einer Instanz einer Komponente (wie einem Stack) hinzugefügt und entfernt werden. Es handelt sich um eine Schlüsseltechnik für das Implementieren immutabler Infrastruktur (siehe »Immutable Infrastruktur« auf Seite 395).

Blue-Green-Änderungen erfordern einen Mechanismus für den Wechsel einer Workload von einer Instanz zu einer anderen, wie zum Beispiel einen Load Balancer für den Netz-Traffic. Ausgefeilte Implementierungen erlauben es, die Workload »austrocknen« zu lassen, neue Aufträge auf die neue Instanz zu leiten und darauf zu warten, dass die Arbeit auf der alten Instanz abgeschlossen ist, bevor sie zerstört wird. Manche Lösungen für automatisiertes Server-Clustering und Anwendungs-Clustering bietet das schon als Feature an und ermöglicht »Rolling Upgrades« für die Instanzen eines Clusters.

Blue-Green wird bei statischer Infrastruktur implementiert, indem zwei Umgebungen betreut werden. Eine Umgebung ist immer live, die andere ist bereit, die nächste Version zu übernehmen. Die Namen Blue und Green heben hervor, dass es sich um gleiche Umgebungen handelt, die beide live laufen können, statt eine primäre und eine sekundäre Umgebung darzustellen.

Ich habe mit einer Organisation zusammengearbeitet, die Blue-Green Data Centers implementiert hat. Bei einem Release wurden die Workloads für das gesamte System von einem Data Center zum anderen umgeschaltet. Diese Größenordnung wurde unhandlich, daher haben wir der Organisation dabei geholfen, das Deployment in einem kleineren Maßstab zu implementieren, sodass ein Blue-Green Deployment nur für den spezifischen Service vorgenommen wird, den man gerade aktualisiert.

Kontinuität

In Kapitel 1 haben wir den Gegensatz zwischen den klassischen »Eisenzeit«-Ansätzen beim Managen von Infrastruktur und den modernen »Cloud-Zeitalter«-Vorgehensweisen vorgestellt (siehe »Aus der Eisenzeit in das Cloud-Zeitalter« auf Seite 33). Als wir mehr mit echter Hardware gearbeitet und sie manuell verwaltet haben, waren die Änderungskosten sehr groß.

Auch die Kosten für einen Fehler waren hoch. Habe ich einen neuen Server ohne ausreichenden Speicher provisioniert, dauerte es eine Woche oder mehr, um mehr RAM zu bestellen, dieses mit in das Data Center zu nehmen, den Server herunterzufahren und aus dem Rack zu ziehen, ihn zu öffnen, das zusätzlich RAM einzubauen, ihn dann wieder in das Rack zu schieben und hochzufahren.

Die Kosten für eine Änderung mit Praktiken des Cloud-Zeitalters sind viel niedriger – und ebenso die Kosten und die Zeit für das Korrigieren eines Fehlers. Provisioniere ich einen Server mit zu wenig Speicher, kostet es mich nur ein paar Minuten, das zu korrigieren, indem ich eine Datei bearbeite und sie auf meinen virtuellen Server anwende.

Die Ansätze der Eisenzeit zur Kontinuität zielten darauf ab, Fehler zu verhindern. Sie optimierten auf MTBF – Mean Time Between Failure –, indem sie die Geschwindigkeit und Häufigkeit von Änderungen opferten. Cloud-Zeitalter-Ansätze optimieren auf MTTR – Mean Time to Recovery. Auch wenn manche Enthusiasten für die modernen Methoden dem Irrglauben erliegen, dass eine Konzentration auf MTTR bedeutet, MTBF zu opfern, ist das nicht wahr, wie in »Einwand »Wir müssen zwischen Geschwindigkeit und Qualität entscheiden«« auf Seite 37 erklärt wurde. Teams, die sich auf die Four-Key-Metrics konzentrieren (Geschwindigkeit und Häufigkeit von Änderungen, MTTR und Änderungsfehlerrate, siehe »Die Four Key Metrics« auf Seite 39), erreichen als Nebeneffekt eine starke MTBF. Es geht nicht um »move fast and break things«, sondern stattdessen um »move fast and fix things«.

Es gibt eine ganze Reihe von Elementen, mit denen in moderner Infrastruktur Kontinuität erreicht wird. Prävention – das Hauptaugenmerk der Änderungsmanagement-Praktiken des Cloud-Zeitalters – ist essenziell, aber Cloud-Infrastruktur und Automation ermöglichen das Verwenden effektiver agiler Entwicklungspraktiken zum Reduzieren von Fehlern. Zudem können wir neue Technologien und Praktiken ausnutzen, um Systeme wiederherzustellen und neu zu bauen und damit eine bessere Kontinuität zu erreichen, als man sich früher je vorstellen konnte. Und indem wir kontinuierlich die Mechanismen austesten, die Änderungen ausliefern und Systeme wiederherstellen, können wir Zuverlässigkeit und Vorbereitung auf eine ganze Reihe von Katastrophenszenarien sicherstellen.

Kontinuität durch das Verhindern von Fehlern

Wie schon erwähnt ging es bei den Ansätzen aus der Eisenzeit zum Steuern von Änderungen vor allem um Prävention. Weil die Kosten für das Beheben eines Fehlers hoch waren, haben Organisationen massiv in das Verhindern von Fehlern investiert. Und weil Änderungen vor allem manuell umgesetzt wurden, gehörte zur Prävention auch, zu beschränken, wer Änderungen vornehmen darf. Die einen mussten Änderungen detailliert planen und designen und andere haben jede Änderung umfassend begutachtet und durchdiskutiert. Die Idee war, dass Fehler abgefangen würden, indem sich mehr Personen mehr Zeit nehmen, um eine Änderung im Voraus zu durchdenken.

Ein Problem bei diesem Ansatz ist der Unterschied zwischen den Designdokumenten und der Implementierung. Etwas, das in einem Diagramm einfach aussieht, kann in der Realität kompliziert sein. Menschen machen Fehler, besonders wenn sie aufwendige und seltene Upgrades durchführen. Im Endergebnis haben klassische, selten durchgeführte, umfassend geplante, große Batch-Änderungsoperationen eine hohe Fehlerrate, und es sind oft längere Wiederherstellungsphasen notwendig.

Die in diesem Buch beschriebenen Praktiken und Patterns wollen Fehler verhindern, ohne die Häufigkeit und Geschwindigkeit von Änderungen zu opfern. Änderungen, die als Code definiert sind, repräsentieren ihre Implementierung besser als das ein Diagramm oder ein Designdokument jemals könnte. Kontinuierliches Integrieren, Anwenden und Testen von Änderungen während Ihrer Arbeit daran zeigt, ob sie für die Produktivumgebung bereit sind. Mit einer Pipeline zum Testen und Ausliefern von Änderungen wird sichergestellt, dass keine Schritte ausgelassen werden, und es wird eine Konsistenz über Umgebungen hinweg erzwungen. Das reduziert die Wahrscheinlichkeit von Fehlern in der Produktivumgebung.

Die zentrale Erkenntnis von agiler Softwareentwicklung und Infrastructure as Code ist das neu zu denkende Verhalten gegenüber Änderungen. Statt sie zu fürchten und sie so selten wie möglich durchzuführen, können Sie Fehler verhindern, indem Sie häufig Änderungen vornehmen. Sie können bei Änderungen nur besser werden, wenn Sie sie oft vornehmen und dabei kontinuierlich Ihre Systeme und Prozesse verbessern.

Eine weitere wichtige Einsicht ist, dass unsere Fähigkeit zum Replizieren und exakten Testen, wie sich Code im Produktivumfeld verhalten wird, mit zunehmend komplexer werdenden Systemen schlechter wird. Wir müssen uns bewusst sein, was wir noch vor der Produktivumgebung testen können und was nicht, und wie wir Risiken minimieren, indem wir die Sichtbarkeit unserer Produktivsysteme verbessern (siehe »Testen in der Produktivumgebung« auf Seite 158).

Kontinuität durch schnelles Wiederherstellen

Die bisher in diesem Kapitel beschriebenen Praktiken können die Downtime reduzieren. Durch das Beschränken der Größe von Änderungen, ein inkrementelles Umsetzen und ihr Testen (vor der Produktivumgebung) kann Ihre Änderungsfehlerrate verringern. Aber es ist nicht klug, anzunehmen, dass Fehler ganz verhindert werden können – daher müssen wir auch dazu in der Lage sein, schnell und einfach eine Umgebung wiederherzustellen.

Die in diesem Buch empfohlenen Praktiken erleichtern es, beliebige Teile Ihres Systems neu zu bauen. Es besteht aus lose gekoppelten Komponenten, die jeweils als idempotenter Code definiert sind. Sie können jede Komponenten-Instanz einfach reparieren oder stattdessen zerstören und neu bauen, indem Sie deren Code erneut anwenden. Sie werden allerdings die Kontinuität der Daten sicherstellen müssen, die auf einer Komponente gehostet werden, wenn Sie diese neu bauen, was in »Datenkontinuität in einem sich ändernden System« auf Seite 430 behandelt wird.

In manchen Fällen können Ihre Plattform oder Ihre Services automatisch ausgefallene Infrastruktur neu bauen. Ihre Infrastruktur-Plattform oder die Anwendungs-Runtime zerstören einzelne Komponenten, wenn sie einen Health Check nicht bestehen, und bauen sie wieder neu auf. Das kontinuierliche Anwenden von Code auf Instanzen (siehe »Code kontinuierlich anwenden« auf Seite 395) rollt automatisch alle Abweichungen vom Code zurück. Sie können eine Pipeline-Stage manuell anstoßen (siehe »Infrastruktur-Delivery-Pipelines« auf Seite 152), um Code auf eine ausgefallene Komponente neu anzuwenden.

In anderen Fehlerszenarien können diese Systeme nicht unbedingt ein Problem automatisch beheben. Eine Recheninstanz kann fehlerhaft arbeiten und trotzdem ihre Health Checks bestehen. Ein Infrastruktur-Element arbeitet eventuell nicht mehr korrekt, erfüllt aber immer noch die Code-Definition, sodass ein erneutes Anwenden des Codes nicht hilft.

Diese Szenarien erfordern zusätzliche Aktivitäten, um ausgefallene Komponenten zu ersetzen. Sie markieren vielleicht eine Komponente, sodass das automatisierte System sie als ausgefallen behandelt, zerstört und ersetzt. Oder wenn beim Wiederherstellen ein System zum Einsatz kommt, das Code erneut anwendet, müssten Sie vielleicht die Komponente selbst zerstören und es dem System ermöglichen, eine neue Instanz zu bauen.

Für alle Ausfallszenarien, bei denen jemand etwas tun muss, sollten Sie sicherstellen, dass Sie Tools, Skripte oder andere Mechanismen bereit haben, die sich leicht einsetzen lassen. Die Leute sollten nicht eine Folge von Schritten abarbeiten müssen – zum Beispiel ein Backup der Daten erstellen, bevor sie eine Instanz zerstören. Stattdessen sollten sie eine Aktion aufrufen, die alle erforderlichen Schritte selbst ausführt. Das Ziel ist, in einem Notfall nicht darüber nachdenken zu müssen, wie Sie Ihr System korrekt wiederherstellen.

Kontinuierliches Disaster Recovery

Ansätze zum Infrastruktur-Management aus der Eisenzeit betrachten das Disaster Recovery als ungewöhnliches Ereignis. Das Wiederherstellen nach einem Ausfall statischer Hardware erfordert häufig das Umlagern von Workloads auf einen getrennten Hardware-Pool, der nur dafür bereitgehalten wird.

Viele Organisationen testen ihre Wiederherstellunsgschritte nur selten – im besten Fall alle paar Monate, manchmal auch nur einmal jährlich. Ich habe viele Organisationen kennengelernt, die ihren Failover-Prozess noch nie richtig getestet haben. Die Annahme ist, dass das Team schon herausfinden wird, wie es sein Backup-System zum Laufen bringt, wenn es das jemals tun muss – auch wenn es ein paar Tage dauert.

Kontinuierliches Disaster Recovery nutzt die gleichen Prozesse und Tools, die zum Provisionieren und Ändern der Infrastruktur zum Einsatz kommen. Wie schon beschrieben können Sie Ihren Infrastruktur-Code anwenden, um ausgefallene Infrastruktur neu zu bauen – vielleicht mit etwas zusätzlicher Automation, um Datenverluste zu vermeiden.

Eines der Prinzipien von Infrastruktur im Cloud-Zeitalter ist die Annahme, dass Systeme unzuverlässig sind (siehe »Prinzip: Gehen Sie davon aus, dass Systeme unzuverlässig sind« auf Seite 44). Sie können Software nicht auf einer virtuellen Maschine installieren und davon ausgehen, dass sie dort so lange läuft, wie Sie das wollen. Ihr Cloud-Anbieter verschiebt, zerstört oder ersetzt eventuell die Maschine oder deren Host-System aus Wartungsgründen, für Sicherheits-Patches oder Upgrades. Daher müssen Sie dafür bereit sein, den Server bei Bedarf zu ersetzen.1

Behandeln Sie Disaster Recovery als Erweiterung des normalen Operations, wird es viel zuverlässiger, als wenn Sie es als Ausnahme ansehen. Ihr Team trainiert Ihren Wiederherstellungsprozess und arbeitet jeden Tag mit den Tools, wenn es Änderungen am Infrastruktur-Code und System-Updates vornimmt. Ändert jemand etwas an einem Skript oder an anderem Code, das das Provisionieren abbrechen lässt oder zu Datenverlust bei einem Update führt, passiert das normalerweise schon in einer Pipeline-Test-Stage, sodass Sie es schnell korrigieren können.

Chaos Engineering

Netflix hat beim kontinuierlichen Disaster Recovery und für das Infrastruktur-Management im Cloud-Zeitalter Pionierarbeit geleistet.2 Deren Chaos Monkey und Simian Army (https://oreil.ly/7aik3) führte das Konzept des kontinuierlichen Disaster Recovery noch einen Schritt weiter und bewies die Effektivität ihrer Kontinuitäts-Mechanismen, indem Fehler in Produktivsysteme eingeschleust wurden. Das entwickelte sich zum Feld des Chaos Engineering – »Die Lehre, mit einem System zu experimentieren, um Vertrauen in dessen Fähigkeiten zu schaffen.«3

Um es noch mal deutlich zu machen: Bei Chaos Engineering geht es nicht darum, unverantwortlich für Ausfälle des Produktivsystems zu sorgen. Experten experimentieren mit bestimmten Ausfallszenarien, die ihr System beherrschen sollte. Im Prinzip handelt es sich um Produktivtests, die zeigen, dass die Erkennungs- und Wiederherstellungsmechanismen korrekt funktionieren. Die Idee dahinter ist, schnell Feedback zu bekommen, wenn eine Änderung am System einen Nebeneffekt hat, der mit diesen Mechanismen interferiert.

Für Ausfälle planen

Ausfälle sind unvermeidlich. Sie können und sollten zwar Maßnahmen ergreifen, um sie weniger wahrscheinlich zu machen, aber Sie brauchen auch Lösungen, um ihre Auswirkungen zu begrenzen und besser mit ihnen umgehen zu können.

Ihr Team hält einen Workshop ab, um Ausfallszenarien zusammenzustellen und sich zu überlegen, was passieren kann, um dann entsprechende Gegenmaßnahmen zu planen.1 Sie können eine Liste mit Wahrscheinlichkeit und Auswirkung jedes Szenarios erstellen, daraus Aktionen ableiten, um die Szenarien anzugehen, und diese dann entsprechend im Backlog Ihres Teams zu priorisieren.

Für jedes Ausfallszenario gibt es eine Reihe von zu bedenkenden Aspekten:

Ursachen und Prävention

Welche Situationen könnten zu diesem Ausfall führen, und was können Sie tun, um sie weniger wahrscheinlich zu machen? So kann ein Server bei Lastspitzen zu wenig Plattenplatz haben. Sie könnten das angehen, indem Sie die Muster bei der Festplattennutzung analysieren und die Plattengröße erweitern, sodass auch bei höherer Last ausreichend Platz vorhanden ist. Zudem könnten Sie automatisierte Mechanismen implementieren, um die Nutzung kontinuierlich zu analysieren und Vorhersagen zu treffen, sodass der Plattenplatz präventiv erweitert werden kann, wenn sich Muster ändern. Ein weiterer Schritt wäre das automatisierte Anpassen der Festplattenkapazität, wenn die Last zunimmt.

Ausfallmodus

Was passiert beim Ausfall? Was können Sie tun, um die Konsequenzen ohne menschliches Eingreifen gering zu halten? Fehlt einem bestimmten Server Plattenplatz, könnte es beispielsweise sein, dass die darauf laufende Anwendung Transaktionen annimmt, sie aber nicht aufzeichnen kann. Das könnte ernsthafte Folgen haben, daher wäre eine Möglichkeit, die Anwendung so anzupassen, dass sie keine Transaktionen mehr annimmt, wenn sie sie nicht speichern kann. In vielen Fällen wissen Teams gar nicht, was passiert, wenn ein bestimmter Fehler auftritt. Idealerweise sorgt Ihr Ausfallmodus dafür, dass das System vollständig weiterarbeiten kann. Reagiert eine Anwendung beispielsweise nicht mehr, könnte Ihr Load Balancer keinen Traffic mehr dorthin leiten.

Erkennung

Wie werden Sie Ausfälle erkennen? Was können Sie tun, um sie schneller zu erkennen – vielleicht sogar schon vor ihrem Auftreten? Sie stellen vielleicht fest, dass die Festplatte keinen Platz mehr hat, wenn die Anwendung abstürzt und ein Kunde anruft, um sich bei Ihrem CEO zu beschweren. Da ist es doch besser, eine Benachrichtigung zu erhalten, wenn die Anwendung abstürzt. Noch besser ist es aber, benachrichtigt zu werden, wenn der Plattenplatz knapp wird, bevor die Platte ganz vollläuft.

Korrektur

Welche Schritte müssen Sie vornehmen, um das das System nach einem Ausfall wiederherzustellen? Wie weiter oben beschrieben kann Ihr System vielleicht in manchen Szenarien die Situation automatisch korrigieren – eventuell durch ein Zerstören und erneutes Bauen einer nicht reagierenden Anwendungs-Instanz. Bei anderen sind viele Schritte zum Reparieren und Neustarten eines Service erforderlich.

Kümmert sich Ihr System automatisch um ein Ausfallszenario, zum Beispiel durch einen Neustart einer nicht reagierenden Recheninstanz, sollten Sie sich genauer anschauen, was hinter dem Ausfall steckt. Warum hat die Instanz nicht mehr reagiert? Wie erkennen und korrigieren Sie das zugrunde liegende Problem? Es sollte nicht mehrere Tage dauern, um zu erkennen, dass Anwendungs-Instanzen alle paar Minuten neu gestartet werden.

Das Planen für Ausfälle ist ein fortlaufender Prozess. Immer dann, wenn Sie einen Zwischenfall mit Ihrem System haben – auch in einer Entwicklungs- oder Testumgebung –, sollte sich Ihr Team darüber Gedanken machen, ob das ein neues Ausfallszenario ist, für das Sie Schritte und Pläne ausarbeiten sollten.

Sie sollten Prüfungen implementieren, die Ihre Ausfallszenarien kontrollieren. Glauben Sie beispielsweise, dass die Anwendung keine Transaktionen mehr annehmen wird, wenn dem Server der Festplattenplatz ausgeht, dann neue Server-Instanzen hinzugefügt werden und Ihr Team eine Benachrichtigung erhält, sollten Sie einen automatisierten Test haben, der dieses Szenario durchtestet. Sie könnten das in einer Pipeline-Stage testen (Verfügbarkeitstests, wie in »Was sollten wir bei der Infrastruktur testen?« auf Seite 140 beschrieben) oder ein Chaos-Experiment nutzen.

image

Kontinuität inkrementell verbessern

Es ist einfach, ambitionierte Wiederherstellungsmaßnahmen zu definieren, bei denen Ihr System jeden erdenklichen Ausfall ohne Probleme verkraftet und dabei die Services am Laufen hält. Ich habe noch nie ein Team kennengelernt, das die Zeit und die Ressourcen dafür hatte, auch nur die Hälfe dessen zu bauen, was ihm lieb gewesen wäre.

Beim Verbinden von Ausfallszenarien und Gegenmaßnahmen können Sie einen inkrementellen Satz von Maßnahmen definieren, die sich implementieren lassen. Teilen Sie diese in einzelne Implementierungs-Stories auf und priorisieren Sie sie in Ihrem Backlog – abhängig von der Wahrscheinlichkeit des Szenarios, dem potenziellen Schaden und dem Implementierungsaufwand. So wäre es zum Beispiel nett, den Plattenplatz für Ihre Anwendung automatisch zu erweitern, wenn er knapp wird, aber eine Warnung vor dem Volllaufen der Platte ist auch schon ein wertvoller erster Schritt.

Datenkontinuität in einem sich ändernden System

Viele Praktiken und Techniken des Cloud-Zeitalters zum Deployen von Software und Managen von Infrastruktur empfehlen fröhlich, Ressourcen gelegentlich zu zerstören oder zu erweitern, wobei nur ganz kurz auf die Probleme mit Daten eingegangen wird. Es sei Ihnen verziehen, wenn Sie der Meinung sind, dass DevOps-Hipster die ganze Idee von »Daten« als Rückfall in die Eisenzeit betrachten – eine ordentliche Zwölf-Faktoren-App (https://12factor.net/de/) ist doch schließlich zustandslos. Aber zu vielen Systemen in der realen Welt gehören auch Daten, und die Leute können erstaunlich stark an ihnen hängen.

Daten können eine Herausforderung sein, wenn man ein System inkrementell ändert, wie das in »Unvollständige Änderungen in die Produktivumgebung übernehmen« auf Seite 407 beschrieben ist. Das Betreiben paralleler Instanzen der Storage-Infrastruktur kann zu Inkonsistenzen führen oder sogar Daten unbrauchbar machen. Viele Ansätze zum inkrementellen Ausliefern von Änderungen bauen darauf auf, sie zurückrollen zu können, was bei Änderungen am Datenschema nicht unbedingt möglich ist.

Das dynamische Hinzufügen, Entfernen und neu Bauen von Infrastruktur-Ressourcen, die Daten hosten, ist besonders schwierig. Aber es gibt abhängig von der Situation Möglichkeiten, damit umzugehen. Zu den Ansätzen gehören unter anderem Sperren, Aufteilen, Replizieren und erneutes Laden.

Sperren

Einige Infrastruktur-Plattformen und Stack-Management-Tools erlauben es Ihnen, bestimmte Ressourcen zu sperren, sodass sie nicht durch Befehle gelöscht werden, die sie ansonsten zerstören würden. Setzen Sie diese Einstellung für ein Storage-Element, wird das Tool keine Änderungen darauf anwenden und es Teammitgliedern erlauben, die Änderung manuell umzusetzen.

Es gibt dabei allerdings ein paar Probleme. Wenden Sie eine Änderung auf eine geschützte Ressource an, lässt das Tool in manchen Fällen den Stack möglicherweise in einem teilweise geänderten Status zurück, was für Services zu Downtime führen kann.

Aber das grundlegende Problem ist, dass der Schutz mancher Ressourcen vor automatisierten Änderungen zu einem manuellen Eingreifen ermutigt. Aber manuelle Arbeiten führen zu manuellen Fehlern. Es ist viel besser, einen Weg zu finden, einen Prozess zu automatisieren, die Ihre Infrastruktur sicher ändert.

Aufteilen

Sie können Daten aufteilen, indem Sie die Ressourcen, auf denen sie gehostet sind, von anderen Teilen des Systems abtrennen – beispielsweise, indem Sie aus ihnen einen eigenen Stack machen (ein Beispiel dafür finden Sie in »Pattern: Micro Stack« auf Seite 92). Sie können eine Recheninstanz gefahrlos zerstören und neu bauen und müssen nur deren Disk Volume abkoppeln und wieder neu ankoppeln.

Halten Sie die Daten in einer Datenbank, werden Sie noch flexibler und ermöglichen es eventuell sogar, weitere Recheninstanzen hinzuzufügen. Sie brauchen weiterhin eine Strategie für die Datenkontinuität für den Stack, der die Daten hostet, aber der Problembereich wird so verkleinert. Eventuell können Sie das Einhalten der Datenkontinuität komplett an andere übergeben, indem Sie einen gehosteten DBaaS-Service verwenden.

Replizieren

Abhängig von den Daten und der Art und Weise, wie sie verwaltet werden, können Sie sie eventuell auf mehrere Infrastruktur-Instanzen replizieren. Ein klassisches Beispiel ist ein verteiltes Datenbank-Cluster, dass die Daten über die Knoten hinweg repliziert.

Mit der richtigen Replikations-Strategie werden Daten von anderen Knoten im Cluster auf einen neu gebauten Knoten geladen. Diese Strategie schlägt fehl, wenn zu viele Knoten verloren gegangen sind, was bei einem großen Hosting-Ausfall geschehen kann. Daher funktioniert dieser Ansatz als erste Verteidigungslinie, es wird aber noch ein anderer Mechanismus für schwerere Ausfallszenarien benötigt.

Neu laden

Die bekannteste Lösung zur Datenkontinuität ist das Sichern und Wiederherstellen von Daten auf beziehungsweise von zuverlässiger Storage-Infrastruktur. Bauen Sie die Infrastruktur, die die Daten hostet, neu, erstellen Sie erst ein Backup der Daten. Nach dem Erstellen der neuen Instanz laden Sie sie dann dort wieder hoch. Sie können auch regelmäßige Backups durchführen, die Sie in Recovery-Szenarien wieder laden können, auch wenn Sie dann alle Datenänderungen verlieren werden, die zwischen dem Backup und dem Zeitpunkt des Wiederherstellens angefallen sind. Das lässt sich minimieren (und möglicherweise sogar ganz vermeiden), indem Datenänderungen an das Backup gestreamt werden, wie zum Beispiel durch das Schreiben eines Datenbank-Transaktionslogs.

Cloud-Plattformen stellen verschiedene Storage-Services zur Verfügung (wie in »Storage-Ressourcen« auf Seite 59 beschrieben), die unterschiedliche Zuverlässigkeitsstufen bieten. So haben beispielsweise Object Storage Services wie AWS S3 im Allgemeinen stärkere Garantien für die Lebensdauer von Daten als Block Storage Services wie AWS EBS. Sie könnten also Backups implementieren, indem Sie Daten in ein Object Storage Volume kopieren oder streamen.

Sie sollten nicht nur den Prozess für das Sichern der Daten automatisieren, sondern auch den für die Wiederherstellung. Ihre Infrastruktur-Plattform bietet eventuell Möglichkeiten, das einfach umzusetzen. So könnten Sie beispielsweise automatisch einen Snapshot eines Disk Storage Volumes ziehen, bevor Sie eine Änderung darauf anwenden.

Es kann sein, dass Sie Disk Volume Snapshots einsetzen können, um das Hinzufügen von Knoten zu einem System (wie einem Datenbank-Cluster) zu optimieren. Statt einen neuen Datenbank-Knoten mit einem leeren Storage Volume zu erzeugen, sorgt ein Verbinden mit einem Klon der Platte eines anderen Knotens eventuell für eine schnellere Synchronisation, sodass der Knoten auch eher online gehen kann.

»Ungetestete Backups sind das Gleiche wie keine Backups« ist in unserer Branche ein bekannter Spruch. Sie haben das automatisierte Testen schon für viele Aspekte Ihres Systems eingesetzt, wenn Sie den Praktiken von Infrastructure as Code gefolgt sind. Also können Sie es auch für Ihre Backups nutzen. Testen Sie Ihren Backup-Restore-Prozess in Ihrer Pipeline oder als Chaos-Experiment – produktiv oder nicht.

Ansätze zur Datenkontinuität mischen

Die beste Lösung ist oft eine Kombination aus Trennen, Replizieren und neu Laden. Durch das Trennen von Daten schaffen Sie die Möglichkeit, andere Teile des Systems flexibler zu managen. Durch das Replizieren stehen Daten einen Großteil der Zeit zur Verfügung. Und das neue Laden von Daten dient als Versicherung für extremere Situationen.

Zusammenfassung

Kontinuität ist bei Verfechtern moderner Praktiken aus dem Cloud-Zeitalter für das Infrastruktur-Management oft nur ein Randaspekt. Die meisten bekannten Ansätze, mit denen Systeme zuverlässig am Laufen gehalten werden, entstammen der Prämisse aus der Eisenzeit, dass Änderungen teuer und gefährlich sind. Diese Ansätze tendieren dazu, die Vorteile der Cloud, von Agile und anderen Vorgehensweisen zu eliminieren, die sich auf eine häufige Änderungsfrequenz konzentrieren.

Ich hoffe, dieses Kapitel hat Ihnen gezeigt, wie Sie eine Cloud-Zeitalter-Mentalität einsetzen können, um Systeme zuverlässiger zu machen – nicht trotz vieler Änderungen, sondern gerade deswegen. Sie können die dynamische Natur moderner Infrastruktur-Plattformen ausnutzen und den rigorosen Fokus auf Testen und Konsistenz implementieren, der aus den agilen Entwicklungspraktiken entstammt. Das Ergebnis ist ein hoher Grad an Vertrauen in das kontinuierliche Ausliefern von Verbesserungen an Ihrem System und das Nutzen von Ausfällen als Gelegenheit zum Lernen und Verbessern.