KAPITEL 6

Umgebungen mit Stacks bauen

In Kapitel 5 wurde ein Infrastruktur-Stack als Zusammenstellung von Infrastruktur-Ressourcen beschrieben, die Sie als eine Einheit managen. Eine Umgebung ist ebenfalls eine Zusammenstellung von Infrastruktur-Ressourcen. Ist also ein Stack das Gleiche wie eine Umgebung? Dieses Kapitel erklärt, dass das der Fall sein kann, es aber nicht sein muss.

Eine Umgebung ist eine Zusammenstellung von Software- und Infrastruktur-Ressourcen, die rund um einen bestimmten Zweck organisiert sind – zum Beispiel zum Unterstützen einer Testphase oder zum Bereitstellen eines Service in einer geografischen Region. Ein Stack – oder ein Satz von Stacks – ist eine Mittel zum Definieren und Managen einer Zusammenstellung von Infrastruktur-Ressourcen. Also nutzen Sie einen oder mehrere Stacks, um eine Umgebung zu implementieren. Sie können eine Umgebung als einen einzelnen Stack implementieren oder sie aus mehreren Stacks zusammenfügen. Sie könnten sogar mehrere Umgebungen in einem Stack erstellen, auch wenn Sie das nicht tun sollten.

Worum es bei Umgebungen geht

Das Konzept einer Umgebung ist eines der Dinge, die wir in der IT als gegeben hinnehmen. Aber häufig gehen wir von unterschiedlichen Bedeutungen aus, wenn wir den Begriff in verschiedenen Situationen einsetzen. In diesem Buch ist eine Umgebung eine Zusammenstellung von Infrastruktur-Ressourcen, die mit der Betriebsfähigkeit eines Systems in Zusammenhang stehen. Sie unterstützen also eine bestimmte Aktivität, wie zum Beispiel das Testen oder das Betreiben eines Systems. Meist gibt es mehrere Umgebungen, die jeweils eine Instanz des gleichen Systems ausführen.

Es gibt zwei typische Anwendungsfälle für den Einsatz mehrerer Umgebungen, auf denen Instanzen des gleichen Systems laufen. Einer ist das Unterstützen eines Auslieferungsprozesses, der andere das Betreiben mehrerer Produktivinstanzen des Systems.

Auslieferungsumgebungen

Der verbreitetste Anwendungsfall für mehrere Umgebungen ist das Unterstützen eines progressiven Software-Release-Prozesses – manchmal auch als Path to Production bezeichnet. Ein gegebener Build einer Anwendung wird Schritt für Schritt in jede Umgebung deployt, um verschiedene Entwicklungs- und Testaktivitäten zu unterstützen, bis er schließlich in die Produktivumgebung deployt wird (Abbildung 6-1).

image

Abbildung 6-1: Auslieferungsumgebungen von ShopSpinner

Ich werde diesen Satz an Umgebungen in diesem Kapitel nutzen, um Patterns zum Definieren von Umgebungen als Code zu illustrieren.

Mehrere Produktivumgebungen

Sie können auch mehrere Umgebungen für vollständige und unabhängige Kopien eines Systems im Produktivumfeld verwenden. Zu den Gründen dafür zählen unter anderem:

Fehlertoleranz

Fällt eine Umgebung aus, können andere weiterhin Ihren Service anbieten. Dazu gehört dann ein Failover-Prozess, der die Last von der ausgefallenen Umgebung verschiebt. Sie können auch Fehlertoleranz innerhalb einer Umgebung umsetzen, indem Sie mehrere Instanzen von Infrastruktur-Elementen nutzen – wie zum Beispiel ein Server-Cluster. Das Betreiben einer zusätzlichen Umgebung verdoppelt die gesamte Infrastruktur und sorgt für eine höhere Fehlertoleranz – wenn auch zu höheren Kosten. In »Kontinuität« auf Seite 423 finden Sie Fortführungsstrategien, die sich Infrastructure as Code zunutze machen.

Skalierbarkeit

Sie können die Arbeitslast auf mehrere Umgebungen verteilen. Das geschieht oft geografisch mit einer eigenen Umgebung für jede Region. Mehrere Umgebungen können genutzt werden, um gleichzeitig Skalierbarkeit und Fehlertoleranz zu erreichen. Gibt es einen Ausfall in einer Region, wird die Last auf die Umgebung einer anderen Region verschoben, bis der Fehler behoben ist.

Abtrennen

Sie können mehrere Instanzen einer Anwendung oder eines Service für verschiedene Nutzungsgruppen betreiben – zum Beispiel für unterschiedliche Kunden. Das Betreiben dieser Instanzen in getrennten Umgebungen kann die Aufteilung unterstützen. Eine stärkere Abtrennung kann dabei helfen, rechtliche oder vertragliche Anforderungen zu erfüllen und den Kundinnen und Kunden mehr Vertrauen ermöglichen.

Bei ShopSpinner läuft jeweils ein eigener Anwendungsserver für alle E-Commerce-Kundinnen und -Kunden. Als sich die Firma nach Nordamerika, Europa und Südasien ausdehnt, entscheidet sie sich dazu, für jede dieser Regionen eine eigene Umgebung zu erstellen (siehe Abbildung 6-2).

image

Abbildung 6-2: Regionale Umgebungen von ShopSpinner

Durch den Einsatz komplett getrennter Umgebungen statt einer einzelnen Umgebung, die über die Regionen verteilt ist, kann ShopSpinner besser sicherstellen, dass es die unterschiedlichen Regelungen zum Speichern von Kundendaten in den verschiedenen Ländern erfüllen kann. Und wenn es Änderungen vornehmen muss, für die Downtime erforderlich ist, kann es das in jeder Region zu einer anderen Zeit machen. Damit wird es einfacher, die Downtime auf die unterschiedlichen Zeitzonen abzustimmen.

Später kann ShopSpinner einen Vertrag mit einer Apothekenkette namens The Medicine Barn abschließen. The Medicine Barn muss aus rechtlichen Gründen seine Kundendaten getrennt von anderen Firmen halten. Daher bietet das Shop-Spinner-Team an, für mehr Geld eine komplett getrennte Umgebung nur für The Medicine Barn einzusetzen, statt auf die normalen, gemeinsam genutzten Umgebungen zurückzugreifen.

Umgebungen, Konsistenz und Konfiguration

Da mehrere Umgebungen Instanzen des gleichen Systems betreiben sollen, sollte die Infrastruktur in jeder Umgebung konsistent sein. Konsistenz über Umgebungsgrenzen hinweg ist einer der Hauptgründe für Infrastructure as Code.

Unterschiede zwischen Umgebungen sorgen dafür, dass es auch zu unterschiedlichem Verhalten kommen kann. Testen Sie Software in einer Umgebung, kann es sein, dass Probleme in einer anderen Umgebung nicht gefunden werden. Es ist sogar möglich, dass Software in manche Umgebungen erfolgreich deployt werden kann, während dies bei anderen nicht möglich ist.

Andererseits werden Sie normalerweise bestimmte Unterschiede zwischen Umgebungen benötigen. Testumgebungen sind vielleicht kleiner als Produktivumgebungen. Verschiedene Personen haben eventuell unterschiedliche Berechtigungen in den verschiedenen Umgebungen. Umgebungen für unterschiedliche Kunden besitzen unterschiedliche Features und Eigenschaften. Zumindest Namen und IDs werden sich unterscheiden (appserver-test, appserver-stage, appserver-prod). Daher müssen Sie mindestens ein paar Aspekte Ihrer Umgebungen konfigurieren.

Zentraler Aspekt bei Ihren Umgebungen ist Ihre Test- und Auslieferungsstrategie. Wird der gleiche Infrastruktur-Code auf jede Umgebung angewendet, können Sie beim Testen in der einen Umgebung eher darauf vertrauen, dass er auch in anderen Umgebungen funktionieren wird. Das können Sie aber nicht, wenn sich die Infrastruktur zu sehr zwischen den Instanzen unterscheidet.

Eventuell können Sie Ihr Vertrauen stärken, indem Sie den Infrastruktur-Code mit verschiedenen Konfigurationswerten testen. Aber es wird nicht sehr praktikabel sein, viele verschiedene Werte zu testen. In solchen Fällen brauchen Sie vielleicht eine zusätzliche Validierung, beispielsweise durch Post-Provisioning-Tests oder das Monitoring der Produktivumgebungen. Detaillierter gehe ich auf das Testen und Ausliefern in Kapitel 8 ein.

Patterns zum Bauen von Umgebungen

Wie schon erwähnt ist eine Umgebung eine konzeptionelle Zusammenstellung von Infrastruktur-Elementen, während es sich bei einem Stack um eine konkrete Zusammenstellung solcher Elemente handelt. Ein Stack-Projekt ist der Quellcode, den Sie verwenden, um eine oder mehrere Stack-Instanzen zu erstellen. Wie sollten Sie also Stack-Projekte und -Instanzen einsetzen, um Umgebungen zu implementieren?

Ich werde zwei Antipatterns und ein Pattern zum Implementieren von Umgebungen über Infrastruktur-Stacks beschreiben. Jedes dieser Patterns beschreibt eine Möglichkeit, mehrere Umgebungen mithilfe von Infrastruktur-Stacks zu definieren. Manche Systeme werden wie in »Patterns und Antipatterns für das Strukturieren von Stacks« auf Seite 86 beschrieben aus mehreren Stacks aufgebaut. In »Umgebungen mit mehreren Stacks erstellen« auf Seite 104 werde ich erläutern, wie das für mehrere Umgebungen aussieht.

Antipattern: Multiple-Enviroment Stack

Ein Multiple-Environment Stack definiert und managt die Infrastruktur für mehrere Umgebungen als eine Stack-Instanz.

Gibt es zum Beispiel drei Umgebungen zum Testen und Betreiben einer Anwendung, enthält ein einzelnes Stack-Projekt den Code für alle drei Umgebungen (siehe Abbildung 6-3).

image

Abbildung 6-3: Ein Multiple-Environment Stack managt die Infrastruktur für mehrere Umgebungen als eine Stack-Instanz.

Motivation

Viele Leute erzeugen solch eine Struktur, wenn sie sich mit einem neuen Stack-Tool vertraut machen, weil es ganz natürlich erscheint, neue Umgebungen zu einem bestehenden Projekt hinzuzufügen.

Konsequenzen

Beim Ausführen eines Tools zum Aktualisieren einer Stack-Instanz umfasst der Anwendungsbereich einer potenziellen Änderung alles in diesem Stack. Haben Sie einen Fehler oder einen Konflikt in Ihrem Code, ist alles in der Instanz gefährdet.1

Befindet sich Ihre Produktivumgebung in der gleichen Stack-Instanz wie eine andere Umgebung, riskieren Sie bei einer Änderung dieser anderen Umgebung, ein Problem mit der Produktivumgebung auszulösen. Ein Coding-Fehler, unerwartete Abhängigkeiten oder sogar ein Bug in Ihrem Tool kann zum Ausfall der Produktivumgebung führen, wenn Sie doch eigentlich nur eine Testumgebung anpassen wollten.

Zugehörige Patterns

Sie können den Explosionsradius von Änderungen beschränken, indem Sie Umgebungen in getrennte Stacks aufteilen. Ein offensichtlicher Weg dazu ist das Copy-Paste Environment (siehe »Antipattern: Copy-Paste Environments« auf Seite 100), bei dem jede Umgebung ein eigenes Stack-Projekt ist, auch wenn das als Antipattern angesehen wird.

Ein besserer Ansatz ist das Reusable-Stack-Pattern (siehe »Pattern: Reusable Stack« auf Seite 102). Dabei dient ein einzelnes Projekt zum Definieren der generischen Struktur für eine Umgebung, das dann zum Managen einer separaten Stack-Instanz für jede Umgebung verwendet wird. Auch wenn dabei nur ein einzelnes Projekt zum Einsatz kommt, wird dieses immer nur auf eine Umgebungsinstanz gleichzeitig angewendet. Damit ist der Explosionsradius für Änderungen auf diese eine Umgebung begrenzt.

Antipattern: Copy-Paste Environments

Das Antipattern der Copy-Paste Environments nutzt ein eigenes Quellcode-Projekt für jede Infrastruktur-Stack-Instanz.

In unserem Beispiel mit den drei Umgebungen test, staging und production gibt es ein eigenes Infrastruktur-Projekt für jede dieser drei Umgebungen (siehe Abbildung 6-4). Anpassungen erfolgen durch Bearbeiten des Codes in einer Umgebung und das anschließende Kopieren dieser Änderungen in die anderen Umgebungen.

image

Abbildung 6-4: Ein Copy-Paste Environment besitzt eine eigene Kopie des Quellcode-Projekts für jede Instanz.

Motivation

Copy-Paste Environments sind ein intuitiver Weg, mehrere Umgebungen zu warten. Durch sie wird das Problem des Explosionsradius beim Multiheaded-Stack-Antipattern vermieden. Sie können zudem jede Stack-Instanz leicht anpassen.

Anwendbarkeit

Copy-Paste Environments können passen, wenn Sie unterschiedliche Instanzen warten und ändern wollen und Sie sich keine Sorgen um verdoppelten Code oder die Konsistenz machen.

Konsequenzen

Es kann schwierig sein, mehrere Copy-Paste Environments zu betreuen. Wollen Sie eine Code-Änderung vornehmen, müssen Sie sie in jedes Projekt kopieren. Vermutlich müssen Sie auch jede Instanz getrennt testen, da eine Änderung in einer funktionieren kann, in einer anderen aber nicht.

Copy-Paste Environments leiden häufig unter Konfigurationsdrift (siehe »Konfigurationsdrift« auf Seite 48). Nutzen Sie solche Umgebungen zum Ausliefern, verringern Sie die Zuverlässigkeit des Deployment-Prozesses und die Validität der Tests, weil es Inkonsistenzen zwischen den Umgebungen geben kann.

Ein Copy-Paste Environment kann beim ersten Einrichten noch konsistent sein, aber mit der Zeit kommt es zu Abweichungen.

Implementierung

Sie erstellen ein Copy-Paste Environment, indem Sie den Projektcode einer Stack-Instanz in ein neues Projekt kopieren. Dann bearbeiten Sie den Code, um ihn an die neue Instanz anzupassen. Nehmen Sie eine Änderung an einem Stack vor, müssen Sie sie per Copy-and-paste auf alle anderen Stack-Projekte übertragen, dabei aber die Besonderheiten der Projekte beibehalten.

Zugehörige Patterns

Environment Branches (siehe »Code aus einem Quellcode-Repository ausliefern« auf Seite 367) können als eine Form von Copy-Paste Environments betrachtet werden. Jeder Branch besitzt eine Kopie des Codes und die Leute kopieren Code zwischen den Branches per Merging. Ein kontinuierliches Anwenden von Code (siehe »Code kontinuierlich anwenden« auf Seite 395) kann die Fallstricke von Copy-Paste vermeiden, weil es garantiert, dass sich der Code nicht von einer Umgebung zur nächsten unterscheidet. Bearbeiten Sie den Code beim Mergen in einen Umgebungs-Branch, entsteht die Gefahr des Copy-Paste-Antipattern.

Das Wrapper-Stack-Pattern (siehe »Pattern: Wrapper-Stack« auf Seite 120) ähnelt ebenfalls den Copy-Paste Environments. Ein Wrapper-Stack nutzt ein eigenes Stack-Projekt für jede Umgebung, um Konfigurationsparameter setzen zu können. Aber der Code für den Stack ist in Stack-Komponenten wie zum Beispiel wiederverwendbarem Modul-Code implementiert. Dieser Code selbst wird nicht für jede Umgebung kopiert, sondern wie ein wiederverwendbarer Stack übertragen. Aber wenn den Wrapper-Stack-Projekten mehr als die grundlegenden Stack-Instanz-Parameter hinzugefügt werden, kann sich daraus das Copy-Paste-Environment-Antipattern entwickeln.

Sollen Stack-Instanzen den gleichen Stack repräsentieren, ist das Reusable-Stack-Pattern normalerweise passender.

Pattern: Reusable Stack

Ein Reusable Stack ist ein Infrastruktur-Quellcode-Projekt, das zum Erstellen mehrerer Instanzen eines Stacks genutzt wird (siehe Abbildung 6-5).

Motivation

Sie erstellen einen Reusable Stack, um mehrere konsistente Instanzen der Infrastruktur zu betreuen. Nehmen Sie Änderungen am Stack-Code vor, können Sie diese auf eine Instanz anwenden und testen und dann die gleiche Code-Version verwenden, um weitere zusätzliche Instanzen zu erstellen oder zu aktualisieren. Sie können neue Instanzen des Stacks mit minimalem Aufwand provisionieren – eventuell sogar automatisch.

image

Abbildung 6-5: Mehrere Stack-Instanzen werden aus einem einzelnen Reusable-Stack-Projekt erstellt.

Ein Beispiel: Das ShopSpinner-Team hat gemeinsam nutzbaren Code aus verschiedenen Stack-Projekten extrahiert, die alle einen Anwendungsserver nutzen. Teammitglieder haben den gemeinsamen Code in einem Modul untergebracht, das von jedem der Stack-Projekte verwendet wird. Später haben sie festgestellt, dass die Stack-Projekte für ihre Kundenanwendungen auch sehr ähnlich aussehen. Neben dem Modul zum Erstellen eines Anwendungsservers besaß jeder Stack Code zum Erstellen von Datenbanken und dedizierte Logging- und Reporting-Services für jeden Kunden.

Das Ändern und Testen der Änderungen an diesem Code für die verschiedenen Kunden wurde zunehmend aufwendiger, und ShopSpinner gewann jeden Monat weitere Kunden dazu. Daher entschied das Team, ein einzelnes Stack-Projekt zu erstellen, das einen Kundenanwendungs-Stack definiert. Dieses Projekt nutzt weiterhin das gemeinsame Java-Anwendungsserver-Modul und ein paar weitere Applikationen (Jira und GoCD). Aber es enthält auch Code zum Einrichten des Rests der kundenabhängigen Infrastruktur.

Wird nun ein neuer Kunde gewonnen, nutzen die Teammitglieder das Stack-Projekt für alle Kunden, um eine neue Instanz anzulegen. Beim Beheben von Fehlern oder Umsetzen von Verbesserungen in der Projekt-Codebasis wenden Sie diese auf Testinstanzen an, um sicherzustellen, dass alles in Ordnung ist, dann rollen sie die Änderung nach und nach auf die Kundeninstanzen aus.

Anwendbarkeit

Sie können einen Reusable Stack zum Ausliefern von Umgebungen oder für mehrere Produktivumgebungen verwenden. Das Pattern ist nützlich, wenn Sie nicht viele Unterschiede zwischen den Umgebungen haben. Es lässt sich weniger gut anwenden, wenn Ihre Umgebungen stark angepasst werden müssen.

Konsequenzen

Die Fähigkeit, mehrere Stacks aus dem gleichen Projekt zu provisionieren und zu aktualisieren, verbessert Skalierbarkeit, Zuverlässigkeit und Durchsatz. Sie können mehr Instanzen mit weniger Aufwand managen, Änderungen mit einem geringeren Fehlerrisiko umsetzen und sie schneller auf mehr Systemen ausrollen.

Normalerweise werden Sie bestimmte Aspekte des Stacks für verschiedene Instanzen auch unterschiedlich konfigurieren müssen – und wenn es sich nur um Namen handelt. Ich werde mich ein ganzes Kapitel lang damit befassen (siehe Kapitel 7).

Sie sollten Ihren Stack-Projektcode testen, bevor Sie Änderungen an geschäftskritischer Infrastruktur vornehmen. Ich werde darauf in mehreren Kapiteln eingehen, unter anderem in Kapitel 8 und Kapitel 9.

Implementierung

Sie erstellen einen Reusable Stack als Infrastruktur-Stack-Projekt und führen dann das Stack-Management-Tool jedes Mal aus, wenn Sie eine Instanz des Stacks anlegen oder aktualisieren wollen. Nutzen Sie die Syntax des Stack-Tool-Befehls, um ihm mitzuteilen, welche Instanz Sie erstellen oder aktualisieren wollen. Bei Terraform würden Sie beispielsweise für jede Instanz eine andere State-Datei oder einen anderen Workspace festlegen. Bei CloudFormation übergeben Sie eine eindeutige Stack-ID für jede Instanz.

Der folgende Beispielbefehl provisioniert zwei Stack-Instanzen aus einem einzelnen Projekt mit einem fiktiven Befehl namens stack. Der Befehl übernimmt ein Argument env, das eindeutige Instanzen identifiziert:

> stack up env=test --source mystack/src

SUCCESS: stack 'test' created

> stack up env=staging --source mystack/src

SUCCESS: stack 'staging' created

Als Faustregel sollten Sie einfache Parameter zum Definieren von Unterschieden zwischen Stack-Instanzen verwenden – Strings, Zahlen oder manchmal auch Listen. Zudem sollte sich die von einem Reusable Stack erzeugte Infrastruktur zwischen den Instanzen nicht sehr unterscheiden.

Zugehörige Patterns

Der Reusable Stack ist eine Verbesserung des Copy-Paste-Environment-Antipattern (siehe »Antipattern: Copy-Paste Environments« auf Seite 100), weil er es einfacher macht, mehrere Instanzen konsistent zu halten.

Das Wrapper-Stack-Pattern (siehe »Pattern: Wrapper-Stack« auf Seite 120) nutzt Stack-Komponenten, um Parameterwerte für jede Instanz zu setzen.

Umgebungen mit mehreren Stacks erstellen

Das Reusable-Stack-Pattern beschreibt ein Vorgehen beim Implementieren mehrerer Umgebungen. In Kapitel 5 habe ich verschiedene Wege beschrieben, wie man die Infrastruktur eines Systems mit mehreren Stacks strukturieren kann (siehe »Patterns und Antipatterns für das Strukturieren von Stacks« auf Seite 86). Es gibt diverse Möglichkeiten, wie Sie Ihre Stacks implementieren können, um diese beiden Dimensionen aus Umgebungen und Systemstruktur implementieren zu können.

Der einfache Fall ist, das komplette System als einzelnen Stack zu implementieren. Provisionieren Sie eine Instanz des Stacks, haben Sie eine vollständige Umgebung. Ich habe das im Diagramm für das Reusable-Stack-Pattern (Abbildung 6-5) dargestellt.

Aber Sie sollten größere Systeme in mehrere Stacks aufteilen. Orientieren Sie sich beispielsweise am Service-Stack-Pattern (siehe »Pattern: Service Stack« auf Seite 91), haben Sie einen separaten Stack für jeden Service (siehe Abbildung 6-6).

image

Abbildung 6-6: Beispiel für einen eigenen Infrastruktur-Stack pro Service

Um mehrere Umgebungen zu erstellen, provisionieren Sie eine Instanz jedes Service Stacks für jede Umgebung (siehe Abbildung 6-7).

image

Abbildung 6-7: Jede Umgebung mit mehreren Stacks bauen

Sie würden so etwas wie die folgenden Befehle nutzen, um eine vollständige Umgebung mit mehreren Stacks aufzubauen:

> stack up env=staging --source product_browse_stack/src

SUCCESS: stack 'product_browse-staging' created

> stack up env=staging --source product_search_stack/src

SUCCESS: stack 'product_search-staging' created

> stack up env=staging --source shopping_basket_stack/src

SUCCESS: stack 'shopping_basket-staging' created

In Kapitel 15 sind Strategien beschrieben, um Systeme in mehrere Stacks aufzuteilen, in Kapitel 17 geht es darum, wie man die Infrastruktur über Stacks hinweg integriert.

Zusammenfassung

Reusable Stacks sollten das Arbeitspferd für die meisten Teams sein, die große Infrastrukturen managen müssen. Ein Stack ist eine nützliche Einheit zum Testen und Ausliefern von Änderungen. So wird sichergestellt, dass jede Instanz einer Umgebung konsistent definiert und gebaut wird. Die Vollständigkeit eines Stacks (im Gegensatz zu Modulen) als Änderungseinheit verbessert die Möglichkeit, Änderungen einfach schnell und regelmäßig auszuliefern.