Dieses Kapitel kombiniert die Kapitel 3 und 4 und beschreibt Wege, um Infrastruktur-Ressourcen, die von einer Infrastruktur-Plattform bereitgestellt werden, per Code zu managen.
Das Konzept, über das ich dabei reden werde, ist der Infrastruktur-Stack. Ein Stack ist eine Zusammenstellung von Infrastruktur-Ressourcen, die gemeinsam definiert und geändert werden. Die Ressourcen in einem Stack werden gemeinsam provisioniert, um eine Stack-Instanz zu schaffen, wozu ein Stack-Management-Tool zum Einsatz kommt. Mit dem gleichen Tool werden Änderungen am Stack-Code vorgenommen und auf eine Instanz angewendet.
Dieses Kapitel beschreibt Patterns zum Gruppieren von Infrastruktur-Ressourcen in Stacks.
Ein Infrastruktur-Stack ist eine Zusammenstellung von Infrastruktur-Ressourcen, die Sie als eine Einheit definieren, provisionieren und aktualisieren (Abbildung 5-1).
Sie schreiben Quellcode zum Definieren der Elemente eines Stacks, bei denen es sich um Ressourcen und Services handelt, die Ihre Infrastruktur-Plattform bereitstellt. So kann Ihr Stack beispielsweise eine virtuelle Maschine (siehe »Computing-Ressourcen« auf Seite 58), ein Disk Volume (siehe »Storage-Ressourcen« auf Seite 59) und ein Subnetz (siehe »Networking-Ressourcen« auf Seite 61) enthalten.
Sie führen ein Stack-Management-Tool aus, das Ihren Stack-Quellcode einliest und die API der Cloud-Plattform verwendet, um die im Code definierten Elemente zusammenzustellen und eine Instanz Ihres Stacks zu provisionieren.
Abbildung 5-1: Ein Infrastruktur-Stack ist eine Zusammenstellung von Infrastruktur-Elementen, die als eine Gruppe gemanagt werden.
Beispiele für Stack-Management-Tools sind:
Manche Werkzeuge zur Serverkonfiguration (auf die ich in Kapitel 11 näher eingehe) besitzen Erweiterungen, um mit Infrastruktur-Stacks arbeiten zu können. Beispiele dafür sind Ansible Cloud Modules (https://oreil.ly/5grn4), Chef Provisioning (https://oreil.ly/7DoV4, mittlerweile nicht mehr unterstützt), Puppet Cloud Management (https://oreil.ly/I72s5) und Salt Cloud (https://oreil.ly/z4_eB).
»Stack« als Begriff Die meisten Stack-Management-Tools nennen sich selbst nicht so. Jedes Tool besitzt seine eigene Terminologie, um die Infrastruktur-Einheit zu beschreiben, die es managt. In diesem Buch beschreibe ich Patterns und Praktiken, die für jedes dieser Tools relevant sein sollten. Ich habe mich dazu entschieden, das Wort Stack zu verwenden. Verschiedene Personen haben mich darauf hingewiesen, dass es für dieses Konzept einen weitaus besseren Begriff gibt. Aber alle diese Personen hatten ein anderes Wort dafür im Kopf. Aktuell gibt es in der Branche keine Übereinstimmung darüber, wie dieses Ding zu benennen ist. Daher werde ich weiterhin das Wort Stack einsetzen. |
Jeder Stack wird durch Quellcode definiert, der deklariert, welche Infrastruktur-Elemente enthalten sein sollten. Terraform-Code (.tf-Dateien) und CloudFormation-Templates sind Beispiele für Infrastruktur-Stack-Code. Ein Stack-Projekt enthält den Quellcode, der die Infrastruktur für einen Stack definiert.
In Abbildung 5-1 sehen Sie die Ordnerstruktur für ein Stack-Quellcode-Projekt für das fiktive Stackmaker-Tool.
stack-project/
├── src/
│ ├── dns.infra
│ ├── load_balancers.infra
│ ├── networking.infra
│ └── webserver.infra
└── test/
Sie können ein einzelnes Stack-Projekt nutzen, um mehr als eine Stack-Instanz zu provisionieren. Lassen Sie das Stack-Tool für das Projekt laufen, nutzt es die Plattform-API, um sicherzustellen, dass die Stack-Instanz existiert und sie zum Projektcode passt. Ist die Stack-Instanz nicht vorhanden, erstellt das Tool sie. Gibt es die Stack-Instanz schon, passt sie aber nicht exakt zum Code, passt das Tool die Instanz an, um für Übereinstimmung zu sorgen.
Ich beschreibe diesen Prozess oft als »Anwenden« des Codes auf eine Instanz.
Ändern Sie den Code und lassen das Tool erneut laufen, ändert es die Stack-Instanz, um Ihre Änderungen abzubilden. Führen Sie das Tool nochmals aus, ohne weitere Änderungen am Code vorgenommen zu haben, sollte es die Stack-Instanzen so belassen, wie sie sind.
Infrastruktur-Codebasen für Systeme, die nicht vollständig auf einer containerbasierten oder Serverless-Anwendungsarchitektur aufbauen, tendieren dazu, viel Code zum Provisionieren und Konfigurieren von Servern zu enthalten. Selbst containerbasierte Systeme müssen Host-Server aufsetzen und Container starten. Die ersten Mainstream-Tools für Infrastructure as Code wie CFEngine, Puppet oder Chef wurden verwendet, um Server zu konfigurieren.
Sie sollten Code, der Server aufsetzt, von Code entkoppeln, der Stacks baut. Damit lässt er sich leichter verstehen, Änderungen werden vereinfacht, weil sie entkoppelt sind, und der Server-Code unterstützt Wiederverwendung und Testen.
Stack-Code spezifiziert normalerweise, welche Art Server zu erstellen sind, und übergibt Informationen über die Umgebung, in der sie laufen werden, indem ein Tool zur Serverkonfiguration aufgerufen wird. Listing 5-2 ist ein Beispiel für eine Stack-Definition, die das fiktive servermaker-Tool aufruft, um einen Server zu konfigurieren.
virtual_machine:
name: appserver-waterworks-${environment}
source_image: shopspinner-base-appserver
memory: 4GB
provision:
tool: servermaker
parameters:
maker_server: maker.shopspinner.xyz
role: appserver
environment: ${environment}
Dieser Stack definiert eine Anwendungsserver-Instanz, die aus einem Server-Image namens shopspinner-appserver erstellt wird und 4 GB RAM enthält. Zur Definition gehört eine Klausel, die einen Provisionierungsprozess anstartet, der Servermaker ausführt. Der Code übergibt zudem eine Reihe von Parametern an das Servermaker-Tool. Zu diesen gehört die Adresse eines Konfigurationsservers (maker_server) mit Konfigurationsdateien und eine Rolle appserver, die Servermaker nutzt, um zu entscheiden, welche Konfiguration auf diesen spezifischen Server anzuwenden ist. Außerdem wird der Name der Umgebung übergeben, die die Konfiguration zum Anpassen des Servers nutzen kann.
Die meisten Sprachen der verbreiteten Stack-Management-Tools sind Low-Level-Infrastruktur-Sprachen. Sie bieten die Infrastruktur-Ressourcen der Plattform, mit der sie arbeiten, direkt an (die Arten von Ressourcen sind in »Infrastruktur-Ressourcen« auf Seite 57 aufgeführt).
Es ist Ihre Aufgabe als Infrastruktur-Coder, den Code zu schreiben, der diese Ressourcen zu etwas Sinnvollem zusammenführt, wie zum Beispiel in Listing 5-3.
address_block:
name: application_network_tier
address_range: 10.1.0.0/24"
vlans:
- appserver_vlan_A
address_range: 10.1.0.0/16
name: shopspinner_appserver_A
vlan: application_network_tier.appserver_vlan_A
gateway:
name: public_internet_gateway
address_block: application_network_tier
inbound_route:
gateway: public_internet_gateway
public_ip: 192.168.99.99
incoming_port: 443
destination:
virtual_machine: shopspinner_appserver_A
port: 8443
Dieses ausgedachte und vereinfachte Pseudocode-Beispiel definiert eine virtuelle Maschine, einen Adressblock und ein VLAN sowie ein Internet Gateway. Dann verbindet es alles miteinander und definiert eine eingehende Verbindung, die eintreffende Anfragen an https://192.168.99.99 an Port 8443 der virtuellen Maschine weiterleitet.1
Die Plattform bietet eventuell selbst schon eine höhere Abstraktionsebene an – zum Beispiel ein Anwendungs-Hosting-Cluster. Die von der Plattform bereitgestellten Cluster-Elemente provisionieren dann vielleicht automatisch Serverinstanzen und Netzwerk-Routen. Aber Low-Level-Infrastruktur-Code bildet direkt auf die Ressourcen und Optionen ab, die von der Plattform-API angeboten werden.
Eine High-Level-Infrastruktur-Sprache definiert Entitäten, die sich nicht direkt auf Ressourcen der zugrunde liegenden Plattform abbilden lassen. So deklariert beispielsweise eine High-Level-Code-Version von Listing 5-3 die Grundlagen des Anwendungsservers – siehe Listing 5-4.
application_server:
public_ip: 192.168.99.99
In diesem Beispiel werden durch das Anwenden des Codes entweder die Networking- und Server-Ressourcen aus dem vorigen Beispiel provisioniert, oder der Code des Beispiels erkennt bestehende Ressourcen, die genutzt werden können. Das Tool oder die Bibliothek, das/die von diesem Code aufgerufen wird, entscheidet, welche Werte für die Netzwerk-Ports und das VLAN zu verwenden sind und wie der virtuelle Server aufgebaut wird.
Viele Anwendungs-Hosting-Lösungen, wie zum Beispiel PaaS-Plattformen oder Packaged Clusters (siehe »Packaged Cluster Distribution« auf Seite 271), bieten diese Abstraktionsschicht. Sie schreiben einen Deployment-Deskriptor für Ihre Anwendung, und die Plattform allokiert die Infrastruktur-Ressourcen, auf die sie deployt wird.
In anderen Fällen bauen Sie vielleicht Ihre eigene Abstraktionsschicht, indem Sie Bibliotheken oder Module schreiben. In Kapitel 16 finden Sie mehr dazu.
Eine Herausforderung beim Infrastruktur-Design ist die Entscheidung, wie Sie die Stacks bezüglich Größe und Struktur definieren. Sie könnten ein einzelnes Stack-Code-Projekt erstellen, um Ihr gesamtes System zu managen. Aber das wird unhandlich, wenn Ihr System größer wird. In diesem Abschnitt werde ich Patterns und Antipatterns für das Strukturieren von Infrastruktur-Stacks beschreiben.
Die folgenden Patterns beschreiben jeweils Möglichkeiten, die Elemente eines Systems in einem Stack oder mehreren Stacks zusammenzufassen. Sie können sie als Kontinuum betrachten:
Ein Monolithic Stack ist ein Infrastruktur-Stack, der zu viele Elemente enthält und die Wartung damit erschwert (siehe Abbildung 5-2).
Von anderen Patterns unterscheidet sich ein monolithischer Stack dadurch, dass die Anzahl an Infrastruktur-Elementen oder ihrer Beziehungen untereinander im Stack nur schwer im Griff zu behalten ist.
Abbildung 5-2: Ein Monolithic Stack ist ein Infrastruktur-Stack mit zu vielen Elementen, wodurch die Wartung erschwert wird.
Viele bauen monolithische Stacks, weil der einfachste Weg zum Hinzufügen eines neuen Elements zu einem System das Ergänzen des bestehenden Projekts ist. Jeder neue Stack sorgt für zusätzliche bewegliche Teile, die eventuell orchestriert, integriert und getestet werden müssen.
Ein einzelner Stack lässt sich einfacher managen. Für eine übersichtliche Anzahl an Infrastruktur-Elementen mag ein monolithischer Stack in Ordnung sein. Aber meist ist er ganz natürlich unkontrolliert gewachsen und außer Kontrolle geraten.
Ein monolithischer Stack kann passend sein, wenn Ihr System klein und einfach ist. Er passt nicht, wenn Ihr System wächst und länger zum Provisionieren und Aktualisieren benötigt.
Es ist riskanter, einen großen als einen kleineren Stack zu ändern. Es können dann mehr Dinge können schiefgehen – und er besitzt einen größeren Explosionsradius. Die Auswirkung einer fehlgeschlagenen Änderung kann umfassender sein, da es mehr Services und Anwendungen im Stack gibt. Größere Stacks lassen sich auch nur langsamer provisionieren und ändern, wodurch sie schwerer zu managen sind.
Aufgrund der Trägheit und des Risikos von Änderungen eines monolithischen Stacks tendieren manche dazu, Änderungen seltener vorzunehmen. Diese zusätzliche Reibung kann zu mehr technischen Schulden führen.
Der direkte Explosionsradius ist der Wirkungsbereich des Codes, den der Befehl zum Anwenden Ihrer Änderung beinhaltet.1 Führen Sie beispielsweise terraform apply aus, gehört zum direkten Explosionsradius der gesamte Code in Ihrem Projekt. Der indirekte Explosionsradius beinhaltet andere Elemente des Systems, die von den Ressourcen in Ihrem direkten Explosionsradius abhängen und durch einen Ausfall dieser Ressourcen beeinflusst würden. |
Sie bauen einen monolithischen Stack, indem Sie ein Infrastruktur-Stack-Projekt erstellen und dann kontinuierlich Code hinzufügen, anstatt es in mehrere Stacks aufzuteilen.
Das Gegenteil eines monolithischen Stacks ist ein Micro Stack (siehe »Pattern: Micro Stack« auf Seite 92), der zum Ziel hat, Stacks klein zu halten, sodass sie sich einfacher warten und verbessern lassen. Ein monolithischer Stack kann ein Application Group Stack sein (siehe »Pattern: Application Group Stack« auf Seite 89), der unkontrolliert gewachsen ist.
Es ist eine Frage der Einschätzung, ob es sich bei Ihrem Infrastruktur-Stack um einen Monolithen handelt. Zu den Symptomen eines monolithischen Stacks gehören:
Ein zentraler Indikator dafür, ob ein Stack monolithisch wird, ist, wie viele Personen gleichzeitig an Änderungen arbeiten. Je üblicher es ist, dass mehrere Personen gleichzeitig am Stack arbeiten, desto mehr Zeit verbringen Sie mit dem Koordinieren von Änderungen. Noch schlimmer wird es, wenn mehrere Teams Änderungen am gleichen Stack vornehmen. Haben Sie regelmäßig Fehlfunktionen und Konflikte beim Deployen von Änderungen an einem gegebenen Stack, ist er eventuell zu groß.
Feature Branching (https://oreil.ly/025IQ) ist eine Strategie, um damit umzugehen, aber sie kann für zusätzliche Reibungen und weiteren Overhead beim Ausliefern sorgen. Ist es üblich, für die Arbeit an einem Stack auf Feature Branches zurückzugreifen, deutet das darauf hin, dass er monolithisch geworden sein könnte.
CI (https://oreil.ly/uJwYF) ist ein nachhaltigerer Weg, mit dem ein paralleles Arbeiten mehrerer Entwicklerinnen und Entwickler an einem einzelnen Stack sicherer gestaltet werden kann. Aber wenn ein Stack zunehmend monolithisch wird, braucht der CI Build länger, und es wird schwerer, eine gute Build-Disziplin aufrechtzuerhalten. Ist die CI Ihres Teams zu nachlässig, ist das ein weiteres Zeichen dafür, dass Ihr Stack ein Monolith ist.
Diese Probleme beziehen sich auf ein einzelnes Team, das an einem Infrastruktur-Stack arbeitet. Arbeiten mehrere Teams an einem gemeinsamen Stack, ist das ein klarer Hinweis darauf, dass Sie darüber nachdenken sollten, ihn in mehrere, besser handhabbare Elemente zu unterteilen.
Ein Application Group Stack beinhaltet die Infrastruktur für mehrere zusammenhängende Anwendungen oder Services. Die Infrastruktur für alle diese Anwendungen wird als Gruppe provisioniert und gemanagt (siehe Abbildung 5-3).
Abbildung 5-3: Ein Application Group Stack hostet mehrere Prozesse in einer einzelnen Instanz des Stacks.
So beinhaltet beispielsweise der Produktanwendungs-Stack von ShopSpinner getrennte Services für das Browsen in Produkten, das Suchen nach Produkten und das Managen eines Einkaufskorbs. Die Server und andere Infrastruktur für all dies sind in einer einzelnen Stack-Instanz zusammengefasst.
Definiert man die Infrastruktur mehrerer zusammengehöriger Services gemeinsam, kann das das Managen der Anwendung als eine Einheit erleichtern.
Dieses Pattern kann gut funktionieren, wenn ein einzelnes Team für die Infrastruktur und das Deployment all der Elemente der Anwendung verantwortlich ist. Ein Application Group Stack kann die Grenzen des Stacks und Zuständigkeiten des Teams aufeinander abbilden.
Multiservice-Stacks sind manchmal als Zwischenschritt nützlich, wenn man von einem monolithischen Stack zu Service Stacks wechseln will.
Bringen Sie die Infrastruktur für mehrere Anwendungen zusammen, kombinieren Sie so auch den Zeitaufwand, das Risiko und die Updatefrequenz von Änderungen. Das Team muss bei jeder Änderung das Risiko des gesamten Stacks managen, auch wenn sich nur ein Teil ändert. Dieses Pattern ist ineffizient, wenn sich manche Teile des Stacks häufiger ändern als andere.
Der Zeitaufwand zum Provisionieren, Ändern und Testen eines Stacks basiert auf dem gesamten Stack. Daher gilt auch hier: Ist es üblich, nur einen Teil eines Stacks gleichzeitig zu verändern, sorgt ein Gruppieren für zusätzlichen, unnötigen Overhead und mehr Risiken.
Um einen Application Group Stack zu erstellen, definieren Sie ein Infrastruktur-Projekt, das die gesamte Infrastruktur für ein Set von Services anlegt. Sie können all die Elemente der Anwendung mit einem einzelnen Befehl provisionieren und auch wieder abräumen.
Dieses Pattern hat das Risiko, zu einem monolithischen Stack zu werden (siehe »Antipattern: Monolithic Stack« auf Seite 86). In die andere Richtung führt das Aufteilen jedes Service in einen Application Group Stack zu einem Service Stack.
Ein Service Stack managt die Infrastruktur für jede deploybare Anwendungskomponente in einem eigenen Infrastruktur-Stack (siehe Abbildung 5-4).
Service Stacks bringen die Grenzen der Infrastruktur mit der Software in Übereinstimmung, die auf ihnen läuft. Damit beschränken Sie den Explosionsradius einer Änderung auf einen Service, was den Prozess zum Schedulen von Änderungen vereinfacht. Service-Teams können für die Infrastruktur zuständig sein, die zu ihrer Software gehört.
Abbildung 5-4: Ein Service Stack managt die Infrastruktur für jede deploybare Anwendungskomponente in einem eigenen Infrastruktur-Stack.
Service Stacks können gut mit Microservices-Anwendungsarchitekturen zusammen funktionieren.1 Zudem helfen sie Organisationen mit autonomen Teams dabei, sicherzustellen, dass jedes Team für seine eigene Infrastruktur zuständig sein kann.2
Haben Sie mehrere Anwendungen – jede mit einem Infrastruktur-Stack –, kann es zu unnötiger Verdopplung von Code kommen. So kann beispielsweise in jedem Stack Code enthalten sein, der festlegt, wie ein Anwendungsserver zu provisionieren ist. Verdopplungen können zu Inkonsistenzen führen, wie zum Beispiel den Einsatz unterschiedlicher Betriebssystem-Versionen oder verschiedener Netzwerkkonfigurationen. Sie können dagegensteuern, indem Sie Module nutzen, um Code gemeinsam zu verwenden (wie in Kapitel 16).
Jede Anwendung oder jeder Service hat ein eigenes Infrastruktur-Code-Projekt. Beim Erstellen einer neuen Anwendung kann ein Team Code aus der Infrastruktur einer anderen Anwendung kopieren. Oder es nutzt ein Referenzprojekt mit vorgefertigtem Code zum Erstellen neuer Stacks.
In manchen Fällen ist jeder Stack vollständig und teilt keine Infrastruktur mit anderen Anwendungs-Stacks. In anderen Fällen erstellen Teams Stacks mit Infrastruktur, die mehrere Anwendungs-Stacks unterstützt. Mehr über die verschiedenen Patterns finden Sie in Kapitel 17.
Das Service-Stack-Pattern liegt zwischen einem Application Group Stack (siehe »Pattern: Application Group Stack« auf Seite 89) mit mehreren Anwendungen in einem einzelnen Stack und einem Micro Stack, der die Infrastruktur für eine einzelne Anwendung auf mehrere Stacks verteilt.
Das Micro-Stack-Pattern teilt die Infrastruktur für einen einzelnen Service auf mehrere Stacks auf (siehe Abbildung 5-5).
Abbildung 5-5: Micro Stacks teilen die Infrastruktur für einen einzelnen Service auf mehrere Stacks auf.
So können Sie beispielsweise jeweils ein Stack-Projekt für das Networking, die Server und die Datenbank haben.
Verschiedene Elemente einer Service-Infrastruktur können sich mit unterschiedlicher Geschwindigkeit ändern. Oder sie haben unterschiedliche Charakteristiken, sodass es einfacher ist, sie separat zu managen. Beispielsweise beinhalten manche Methoden zum Managen von Serverinstanzen ihr häufiges Zerstören und erneutes Bauen.1 Aber manche Services nutzen persistente Daten in einer Datenbank oder auf einem Disk Volume. Managt man die Server und Daten in getrennten Stacks, können sie unterschiedliche Lebenszyklen besitzen, wobei der Server-Stack viel häufiger neu gebaut wird als der Daten-Stack.
Auch wenn kleineren Stacks prinzipiell einfacher sind, führt die erhöhte Anzahl an beweglichen Teilen zu zusätzlicher Komplexität. In Kapitel 17 werden Techniken zum Umgang mit der Integration zwischen mehreren Stacks beschrieben.
Für einen neuen Micro Stack muss ein neues Stack-Projekt erstellt werden. Sie müssen die Grenzen an den richtigen Stellen zwischen den Stacks ziehen, um ihre Größe passend zu gestalten und sie leicht managen zu können. Die zugehörigen Patterns bieten Lösungen dafür an. Sie müssen eventuell auf unterschiedliche Stacks integrieren, was ich in Kapitel 17 beschreibe.
Micro Stacks stellen das entgegengesetzte Ende des Spektrums zu einem monolithischen Stack dar (siehe »Antipattern: Monolithic Stack« auf Seite 86), bei dem ein einzelner Stack die gesamte Infrastruktur für ein System enthält.
Infrastruktur-Stacks sind grundlegende Bausteine für automatisierte Infrastruktur. Die Patterns in diesem Kapitel sind ein Ausgangspunkt für Diskussionen über das Organisieren von Infrastruktur in Stacks.