Kapitel 4. Entwurf von Infrastrukturanwendungen
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Im vorangegangenen Kapitel haben wir gelernt, wie man eine Infrastruktur darstellt und welche verschiedenen Ansätze und Probleme es bei der Bereitstellung von Tools gibt. In diesem Kapitel schauen wir uns an, was es braucht, um Anwendungen zu entwickeln, die die Infrastruktur bereitstellen und verwalten. Wir berücksichtigen die Anliegen des vorangegangenen Kapitels und konzentrieren uns darauf, die Welt der Infrastruktur als Software zu erschließen, manchmal auch Infrastruktur als Anwendung genannt.
In einer Cloud-Umgebung müssen traditionelle Infrastrukturbetreiber/innen zu Softwareentwickler/innen für die Infrastruktur werden. Diese Praxis ist noch im Entstehen begriffen und unterscheidet sich von anderen betrieblichen Rollen in der Vergangenheit. Wir müssen dringend damit beginnen, Muster zu erforschen und Standards zu setzen.
Ein grundlegender Unterschied zwischen Infrastruktur als Code und Infrastruktur als Software besteht darin, dass die Software kontinuierlich läuft und die Infrastruktur auf der Grundlage des Reconciler-Patterns, das wir später in diesem Kapitel erläutern werden, erstellt oder verändert. Außerdem besteht das neue Paradigma hinter Infrastruktur als Software darin, dass die Software nun eine traditionellere Beziehung zum Datenspeicher hat und eine API für die Definition des gewünschten Zustands bereitstellt. So kann die Software beispielsweise die Darstellung der Infrastruktur im Datenspeicher nach Bedarf ändern und den Datenspeicher selbst verwalten! Gewünschte Zustandsänderungen zum Abgleich werden über die API an die Software gesendet und nicht mehr über den statischen Code-Repository.
Der erste Schritt in Richtung Infrastruktur als Software ist, dass Infrastrukturbetreiber erkennen, dass sie Softwareentwickler sind. Wir heißen euch alle herzlich willkommen! Frühere Tools (z. B. Konfigurationsmanagement) hatten ähnliche Ziele, um die Aufgaben der Infrastrukturbetreiber zu verändern, aber oft lernten die Betreiber nur, wie man eine begrenzte DSL mit engem Anwendungsbereich schreibt (d. h. Abstraktion von einzelnen Knoten).
Als Infrastrukturingenieur musst du nicht nur die grundlegenden Prinzipien der Entwicklung, der Verwaltung und des Betriebs von Infrastrukturen beherrschen, sondern dein Fachwissen auch in Form einer soliden Anwendung umsetzen. Diese Anwendungen stellen die Infrastruktur dar, die wir verwalten und verändern werden.
Die Entwicklung von Software zur Verwaltung von Infrastrukturen ist kein einfaches Unterfangen. Wir haben alle wichtigen Probleme und Bedenken einer herkömmlichen Anwendung, und wir entwickeln in einem schwierigen Umfeld. Es ist schwierig in dem Sinne, dass die Entwicklung von Infrastrukturen eine fast lächerliche Aufgabe ist, nämlich Software zu entwickeln, um Infrastrukturen bereitzustellen, damit du dann dieselbe Software auf der neu geschaffenen Infrastruktur ausführen kannst.
Zu Beginn müssen wir die Feinheiten der Softwareentwicklung in diesem neuen Bereich verstehen. Wir werden uns die in der Cloud Native Community bewährten Muster ansehen, um zu verstehen, wie wichtig es ist, sauberen und logischen Code in unseren Anwendungen zu schreiben. Aber zuerst: Woher kommt die Infrastruktur?
Das Bootstrapping-Problem
Am Sonntag, den 22. März 1987, schickte Richard M. Stallman eine E-Mail an die GCC-Mailingliste, um zu berichten, dass er den C-Compiler erfolgreich mit sich selbst kompiliert hatte:
Dieser Compiler kompiliert sich korrekt auf dem 68020 und tat dies kürzlich auch auf dem vax. Er hat kürzlich Emacs korrekt auf dem 68020 kompiliert und hat auch tex-in-C und Kyoto Common Lisp kompiliert. Allerdings hat er wahrscheinlich noch zahlreiche Fehler, die ich hoffe, dass du sie für mich findest.
Ich werde einen Monat lang weg sein, also werden Fehler, die jetzt gemeldet werden, bis dahin nicht bearbeitet.
Richard M. Stallman
Dies war ein entscheidender Wendepunkt in der Geschichte der Software, da wir Software so entwickelten, dass sie sich selbst kompilieren konnte. Stallman hatte buchstäblich einen Compiler geschaffen, der sich selbst kompilieren konnte. Selbst diese Aussage als Wahrheit zu akzeptieren, kann philosophisch schwierig sein.
Heute lösen wir dasselbe Problem mit der Infrastruktur. Ingenieure müssen Lösungen für fast unmögliche Probleme finden, wenn ein System sich selbst hochfährt und zur Laufzeit zum Leben erwacht.
Ein Ansatz ist die manuelle Bereitstellung des ersten Teils der Infrastruktur in der Cloud und der Infrastrukturanwendungen. Dieser Ansatz funktioniert zwar, aber in der Regel muss der Betreiber die anfängliche Bootstrap-Infrastruktur zerstören, nachdem eine geeignetere Infrastruktur bereitgestellt wurde. Dieser Ansatz ist mühsam, schwer zu wiederholen und anfällig für menschliche Fehler.
Ein eleganterer und cloudbasierter Ansatz zur Lösung dieses Problems ist die (in der Regel korrekte) Annahme, dass derjenige, der versucht, die Infrastruktursoftware zu booten, über einen lokalen Rechner verfügt, den wir zu unserem Vorteil nutzen können. Der vorhandene Rechner (dein Computer) dient als erstes Deployment-Tool, um die Infrastruktur in einer Cloud automatisch zu erstellen. Nachdem die Infrastruktur aufgebaut ist, kann sich dein lokales Deployment-Tool dann selbst in die neu erstellte Infrastruktur deployen und kontinuierlich laufen. Gute Deployment-Tools ermöglichen es dir, diesen Vorgang nachher einfach zu bereinigen.
Nachdem das anfängliche Infrastruktur-Bootstrap-Problem gelöst ist, können wir die Infrastrukturanwendungen nutzen, um eine neue Infrastruktur zu booten. Der lokale Computer ist nun aus der Gleichung herausgenommen, und wir arbeiten jetzt vollständig in der Cloud.
Die API
In früheren Kapiteln haben wir die verschiedenen Methoden zur Darstellung von Infrastruktur besprochen. In diesem Kapitel werden wir das Konzept einer API für die Infrastruktur untersuchen.
Wenn die API in Software implementiert wird, geschieht das höchstwahrscheinlich über eine Datenstruktur. Je nachdem, welche Programmiersprache du verwendest, kannst du dir die API also als Klasse, Wörterbuch, Array, Objekt oder Struktur vorstellen.
Die API wird eine beliebige Definition von Datenwerten sein, vielleicht eine Handvoll Strings, ein paar Ganzzahlen und ein Boolescher Wert. Die API wird von einer Art Kodierung wie JSON oder YAML kodiert und dekodiert oder vielleicht sogar in einer Datenbank gespeichert.
Eine versionierbare API für ein Programm ist für die meisten Softwareentwickler/innen eine gängige Praxis. So kann sich das Programm im Laufe der Zeit bewegen, verändern und wachsen. Die Entwickler/innen können die Unterstützung älterer API-Versionen ankündigen und Rückwärtskompatibilität garantieren. Bei der Entwicklung von Infrastruktur als Software wird aus diesen Gründen eine API bevorzugt.
Eine API als Schnittstelle für die Infrastruktur zu finden, ist einer der vielen Hinweise darauf, dass ein Nutzer mit der Infrastruktur als Software arbeitet. Traditionell ist die Infrastruktur als Code eine direkte Darstellung der Infrastruktur, die ein Nutzer verwaltet, während eine API eine Abstraktion über den genauen zugrunde liegenden Ressourcen ist, die verwaltet werden.1
Letztlich ist eine API nur eine Datenstruktur, die eine Infrastruktur darstellt.
Der Zustand der Welt
Im Kontext einer Infrastruktur als Software-Tool ist die Welt die Infrastruktur, die wir verwalten. Der Zustand der Welt ist also nur eine geprüfte Darstellung der Welt, wie sie für unser Programm existiert.
Der Zustand der Welt wird schließlich in eine In-Memory-Darstellung der Infrastruktur zurückgeführt. Diese In-Memory-Darstellung sollte der ursprünglichen API entsprechen, die zur Deklaration der Infrastruktur verwendet wurde. Die geprüfte API oder der Zustand der Welt muss normalerweise gespeichert werden.
Ein Speichermedium (manchmal auch als Statusspeicher bezeichnet) kann verwendet werden, um die frisch geprüfte API zu speichern. Bei dem Medium kann es sich um ein beliebiges herkömmliches Speichersystem handeln, z. B. ein lokales Dateisystem, einen Cloud-Objektspeicher oder eine Datenbank. Wenn die Daten in einem dateisystemähnlichen Speicher abgelegt werden, wird das Tool die Daten höchstwahrscheinlich in einer logischen Weise kodieren, so dass die Daten zur Laufzeit leicht kodiert und dekodiert werden können. Gängige Kodierungen hierfür sind JSON, YAML und TOML.
Vorsicht
Wenn du mit der Entwicklung deines Programms beginnst, wirst du dich vielleicht dabei ertappen, dass du privilegierte Informationen zusammen mit den übrigen Daten speichern möchtest. Je nach deinen Sicherheitsanforderungen und dem Ort, an dem du die Daten speichern möchtest, kann dies eine bewährte Methode sein oder auch nicht.
Es ist wichtig, sich daran zu erinnern, dass die Speicherung von Geheimnissen eine Schwachstelle sein kann. Wenn du eine Software entwickelst, die den grundlegendsten Teil des Stacks kontrolliert, ist die Sicherheit von entscheidender Bedeutung. Daher lohnt es sich in der Regel, den zusätzlichen Aufwand zu betreiben, um sicherzustellen, dass die Geheimnisse sicher sind.
Neben den Meta-Informationen über das Programm und die Anmeldedaten des Cloud-Providers muss ein Ingenieur auch Informationen über die Infrastruktur speichern. Es ist wichtig, sich daran zu erinnern, dass die Infrastruktur in irgendeiner Form dargestellt wird, idealerweise so, dass sie für das Programm leicht zu entschlüsseln ist. Es ist auch wichtig, sich daran zu erinnern, dass Änderungen an einem System nicht sofort, sondern erst im Laufe der Zeit vorgenommen werden.
Diese Daten zu speichern und leicht zugänglich zu machen, ist ein wichtiger Teil der Entwicklung der Anwendung für das Infrastrukturmanagement. Die Definition der Infrastruktur allein ist wahrscheinlich der intellektuell wertvollste Teil des Systems. Schauen wir uns ein einfaches Beispiel an, um zu sehen, wie diese Daten und das Programm zusammenarbeiten werden.
Hinweis
Es ist wichtig, die Beispiele 4-1 bis 4-4 zu wiederholen, da sie als konkrete Beispiele für den Unterricht im weiteren Verlauf des Kapitels dienen.
Ein Beispiel für einen Dateisystem-Statusspeicher
Stell dir einen Datenspeicher vor, der einfach ein Verzeichnis namens state ist. In diesem Verzeichnis gibt es drei Dateien:
-
meta_information.yaml
-
secrets.yaml
-
infrastructure.yaml
Dieser einfache Datenspeicher kann die Informationen, die für ein effektives Infrastrukturmanagement benötigt werden, genau kapseln.
In den Dateien secrets.yaml und infrastructure.yaml wird die Darstellung der Infrastruktur gespeichert, und in der Datei meta_information.yaml(Beispiel 4-1) werden weitere wichtige Informationen gespeichert, z. B. wann die Infrastruktur zuletzt bereitgestellt wurde, wer sie bereitgestellt hat und Protokollierungsinformationen.
Beispiel 4-1. state/meta_information.yaml
lastExecution
:
exitCode
:
0
timestamp
:
2017-08-01 15:32:11 +00:00
user
:
kris
logFile
:
/var/log/infra.log
Die zweite Datei, secrets.yaml, enthält private Informationen, die während der Ausführung des Programms zur Authentifizierung auf beliebige Weise verwendet werden(Beispiel 4-2).
Warnung
Auch hier gilt, dass das Speichern von Geheimnissen auf diese Weise unsicher sein kann. Wir verwenden secrets.yaml nur als Beispiel.
Beispiel 4-2. state/secrets.yaml
apiAccessToken
:
a8233fc28d09a9c27b2e2f
apiSecret
:
8a2976744f239eaa9287f83b23309023d
privateKeyPath
:
~/.ssh/id_rsa
Die dritte Datei, infrastructure.yaml, enthält eine verschlüsselte Darstellung der API, einschließlich der verwendeten API-Version(Beispiel 4-3). Hier finden wir die Darstellung der Infrastruktur, wie z. B. Netzwerk- und DNS-Informationen, Firewall-Regeln und Definitionen virtueller Maschinen.
Beispiel 4-3. state/infrastructure.yaml
location
:
"San
Francisco
2"
name
:
infra1
dns
:
fqdn
:
infra.example.com
network
:
cidr
:
10.0.0.0/12
serverPools
:
-
bootstrapScript
:
/opt/infra/bootstrap.sh
diskSize
:
large
workload
:
medium
memory
:
medium
subnetHostsCount
:
256
firewalls
:
-
rules
:
-
ingressFromPort
:
22
ingressProtocol
:
tcp
ingressSource
:
0.0.0.0/0
ingressToPort
:
22
image
:
ubuntu-16-04-x64
Die Datei infrastructure.yaml scheint auf den ersten Blick nicht mehr als ein Beispiel für Infrastruktur als Code zu sein. Doch wenn du genau hinsiehst, wirst du sehen, dass viele der definierten Direktiven eine Abstraktion über der konkreten Infrastruktur sind. Die Direktive subnetHostsCount
ist beispielsweise ein Integer-Wert und definiert die beabsichtigte Anzahl von Hosts für ein Subnetz.
Das Programm übernimmt die Aufteilung des größeren CIDR-Wertes (Classless Interdomain Routing), der in network
für den Betreiber definiert ist. Der Betreiber gibt kein Subnetz an, sondern nur, wie viele Hosts er haben möchte. Die Software erledigt den Rest für den Betreiber.
Wenn das Programm läuft, kann es die API aktualisieren und die neue Darstellung in den Datenspeicher (in diesem Fall einfach eine Datei) schreiben. Um mit unserem subnetHostsCount
Beispiel fortzufahren, nehmen wir an, dass das Programm eine Subnetz-CIDR für uns herausgesucht hat. Die neue Datenstruktur könnte etwa so aussehen wie in Beispiel 4-4.
Beispiel 4-4. state/infrastructure.yaml
location
:
"San
Francisco
2"
name
:
infra1
dns
:
fqdn
:
infra.example.com
network
:
cidr
:
10.0.0.0/12
serverPools
:
-
bootstrapScript
:
/opt/infra/bootstrap.sh
diskSize
:
large
workload
:
medium
memory
:
medium
subnetHostsCount
:
256
assignedSubnetCIDR
:
10.0.100.0/24
firewalls
:
-
rules
:
-
ingressFromPort
:
22
ingressProtocol
:
tcp
ingressSource
:
0.0.0.0/0
ingressToPort
:
22
image
:
ubuntu-16-04-x64
Beachte, dass das Programm die assignedSubnetCIDR
Richtlinie geschrieben hat, nicht der Betreiber. Denke auch daran, dass das Programm, das die API aktualisiert, ein Zeichen dafür ist, dass ein Nutzer mit der Infrastruktur als Software interagiert.
Erinnere dich daran, dass dies nur ein Beispiel ist und nicht unbedingt dafür spricht, eine Abstraktion für die Berechnung eines Subnetzes CIDR zu verwenden. Verschiedene Anwendungsfälle können unterschiedliche Abstraktionen und Implementierungen in der Anwendung erfordern. Eine der schönen und mächtigen Dinge bei der Entwicklung von Infrastrukturanwendungen ist, dass die Benutzer die Software so entwickeln können, wie sie es für die Lösung ihrer Probleme für notwendig halten.
Der Datenspeicher (die Datei infrastructure.yaml ) kann nun wie ein herkömmlicher Datenspeicher in der Softwareentwicklung betrachtet werden, d.h. das Programm kann die volle Schreibkontrolle über die Datei haben.
Das birgt Risiken, ist aber auch ein großer Gewinn für den Ingenieur, wie wir noch sehen werden. Die Darstellung der Infrastruktur muss nicht in Dateien auf einem Dateisystem gespeichert werden. Stattdessen kann sie in jeder beliebigen Speicherung gespeichert werden, z. B. in einer herkömmlichen Datenbank oder in einem Key/Value-Speichersystem.
Um zu verstehen, wie komplex die Software mit dieser neuen Darstellung der Infrastruktur umgeht, müssen wir die beiden Zustände im System verstehen - den erwarteten Zustand in Form der API, die in der Datei infrastructure.yaml zu finden ist, und den tatsächlichen Zustand, der in der Realität beobachtet (oder überprüft) werden kann, oder den Zustand der Welt.
In diesem Beispiel hat die Software noch nichts getan und wir befinden uns am Anfang der Verwaltungszeitlinie. Der tatsächliche Zustand der Welt wäre also nichts, während der erwartete Zustand der Welt das wäre, was in der Datei infrastructure.yaml gekapselt ist.
Das Versöhnungsmuster
Das Reconciler-Pattern ist ein Software-Pattern, das für die Verwaltung von Cloud Native Infrastructure verwendet oder erweitert werden kann. Das Pattern setzt die Idee durch, dass es zwei Repräsentationen der Infrastruktur gibt - die erste ist der tatsächliche Zustand der Infrastruktur, die zweite der erwartete Zustand der Infrastruktur.
Das Reconciler-Muster zwingt den Ingenieur dazu, zwei unabhängige Wege zu haben, um eine dieser Darstellungen zu erhalten und eine Lösung zu implementieren, um den tatsächlichen Zustand mit dem erwarteten Zustand in Einklang zu bringen.
Das Versöhnungsmuster kann man sich als einen Satz von vier Methoden und vier philosophischen Regeln vorstellen:
-
Verwende eine Datenstruktur für alle Ein- und Ausgaben.
-
Stelle sicher, dass die Datenstruktur unveränderlich ist.
-
Halte die Ressourcenkarte einfach.
-
Bringe den tatsächlichen Zustand mit dem erwarteten Zustand in Einklang.
Das sind starke Garantien, auf die sich der Nutzer des Musters verlassen kann. Außerdem befreien sie den Nutzer von den Implementierungsdetails.
Regel 1: Verwende eine Datenstruktur für alle Eingaben und Ausgaben
Die Methoden, die das Reconciler-Muster implementieren, dürfen nur eine Datenstruktur annehmen und zurückgeben.2 Die Struktur muss außerhalb des Kontexts der Reconciler-Implementierung definiert werden, aber die Implementierung muss sich dessen bewusst sein.
Indem er nur eine Datenstruktur als Eingabe akzeptiert und eine als Ausgabe zurückgibt, kann der Verbraucher jede in seinem Datenspeicher definierte Struktur abgleichen, ohne sich darum kümmern zu müssen, wie dieser Abgleich erfolgt. Dadurch können die Implementierungen auch zur Laufzeit oder mit verschiedenen Versionen des Programms geändert, modifiziert oder umgestellt werden.
Während wir die erste Regel so oft wie möglich befolgen wollen, ist es auch sehr wichtig, Datenstruktur und Codebasis nie eng miteinander zu koppeln. Beachte immer die bewährten Methoden zur Abstraktion und Trennung und verwende nie Teilmengen der API, um Funktionen oder Klassen zu übergeben.
Regel 2: Sicherstellen, dass die Datenstruktur unveränderlich ist
Stell dir eine Datenstruktur wie einen Vertrag oder eine Garantie vor. Im Rahmen des Reconciler-Patterns werden die tatsächliche und die erwartete Struktur zur Laufzeit im Speicher festgelegt. Dadurch wird garantiert, dass die Strukturen vor einem Abgleich korrekt sind. Wird die Struktur während des Abgleichs der Infrastruktur geändert, muss eine neue Struktur mit derselben Garantie erstellt werden. Eine kluge Infrastrukturanwendung erzwingt die Unveränderlichkeit der Datenstruktur, so dass selbst bei dem Versuch eines Ingenieurs, eine Datenstruktur zu verändern, diese nicht funktioniert oder das Programm fehlerhaft (oder vielleicht sogar nicht kompilierbar) ist.
Die Kernkomponente einer Infrastrukturanwendung ist die Fähigkeit, eine Darstellung auf eine Reihe von Ressourcen abzubilden. Eine Ressource ist eine einzelne Aufgabe, die ausgeführt werden muss, um die Anforderungen an die Infrastruktur zu erfüllen. Jede dieser Aufgaben ist dafür verantwortlich, die Infrastruktur in irgendeiner Weise zu verändern.
Grundlegende Beispiele sind die Bereitstellung einer neuen virtuellen Maschine, die Einrichtung eines neuen Netzwerks oder die Bereitstellung einer bestehenden virtuellen Maschine. Jede dieser Arbeitseinheiten wird als Ressource bezeichnet. Jede Datenstruktur sollte einer bestimmten Anzahl von Ressourcen zugeordnet werden. Die Anwendung ist dafür verantwortlich, die Struktur zu verstehen und die Ressourcen zu erstellen. Ein Beispiel dafür, wie die API einzelnen Ressourcen zugeordnet wird, ist in Abbildung 4-1 zu sehen.
Das Reconciler-Pattern zeigt einen stabilen Ansatz für die Arbeit mit einer Datenstruktur, die Ressourcen verändert. Da das Reconciler-Pattern den Vergleich von Ressourcenzuständen erfordert, müssen Datenstrukturen unveränderlich sein. Das bedeutet, dass jedes Mal, wenn die Datenstruktur aktualisiert werden muss, eine neue Datenstrukturerstellt werden muss .
Hinweis
Achte auf Mutationen in der Infrastruktur. Jedes Mal, wenn eine Mutation auftritt, ist die eigentliche Datenstruktur veraltet. Eine clevere Infrastrukturanwendung wird sich dessen bewusst sein und entsprechend damit umgehen.
Eine einfache Lösung wäre, die Datenstruktur im Speicher zu aktualisieren, sobald eine Mutation auftritt. Wenn der tatsächliche Zustand bei jeder Mutation aktualisiert wird, kann der Abstimmungsprozess als eine Reihe von Änderungen des tatsächlichen Zustands im Laufe der Zeit beobachtet werden, bis er schließlich mit dem erwarteten Zustand übereinstimmt und die Abstimmung abgeschlossen ist.
Regel 3: Halte die Ressourcenkarte einfach
Hinter den Kulissen des Abstimmungsmusters befindet sich eine Implementierung. Eine Implementierung ist einfach ein Satz von Code, der Methoden zum Erstellen, Ändern und Löschen von Infrastrukturen enthält. Ein Programm kann viele Implementierungen haben.
Jede Implementierung muss letztendlich eine Datenstruktur auf eine Gruppe von Ressourcen abbilden. Die Gruppe von Ressourcen muss auf logische Weise gruppiert werden, damit das Programm über jede einzelne Ressource nachdenken kann.
Neben der Erstellung des Grundmodells der Ressourcen musst du auch den Abhängigkeiten der einzelnen Ressourcen große Aufmerksamkeit schenken. Viele Ressourcen sind von anderen Ressourcen abhängig, d.h. viele Teile der Infrastruktur sind von anderen Teilen abhängig. So muss z.B. erst ein Netzwerk vorhanden sein, bevor eine virtuelle Maschine in das Netzwerk eingefügt werden kann.
Das Abstimmungsmuster schreibt vor, dass die einfachste Datenstruktur für die Gruppierung von Ressourcen verwendet werden sollte.
Die Lösung des Ressourcenkartenproblems ist eine technische Entscheidung und kann sich bei jeder Implementierung ändern. Es ist wichtig, die Datenstruktur sorgfältig auszuwählen, da der Reconciler aus technischer Sicht stabil und leicht zugänglich sein muss.
Hinweis
Zwei gängige Strukturen für die Abbildung von Daten sind Mengen und Graphen.
Eine Menge ist eine flache Liste von Ressourcen, die iteriert werden kann. In vielen Programmiersprachen werden diese Listen, Mengen, Arrays oder Ähnliches genannt.
Ein Graph ist eine Sammlung von Scheitelpunkten, die über Zeiger miteinander verbunden sind. Der Scheitelpunkt eines Graphen ist in der Regel eine Struktur oder eine Klasse, je nach Programmiersprache. Ein Scheitelpunkt ist über einen Zeiger, der irgendwo im Scheitelpunkt definiert ist, mit einem anderen Scheitelpunkt verbunden. Eine Graphimplementierung kann jeden der Scheitelpunkte besuchen, indem sie über den Zeiger von einem zum anderen springt.
Beispiel 4-5 ist ein Beispiel für einen einfachen Vertex in der Programmiersprache Go.
Beispiel 4-5. Beispiel Scheitelpunkt
// Vertex is a data structure that represents a single point on a graph.
// A single Vertex can have N number of children vertices, or none at all.
type
Vertex
struct
{
Name
string
Children
[]
*
Vertex
}
Ein Beispiel für die Durchquerung des Graphen ist die rekursive Iteration durch jedes Kind. Diese Durchquerung wird manchmal auch als " Walking the Graph" bezeichnet.Beispiel 4-6 ist ein Beispiel für die rekursive Durchquerung jedes Vertex im Graphen mit Hilfe eines in Go geschriebenen Deep-First-Traversals.
Beispiel 4-6. Depth-first traversal
// recursiveWalk will recursively dig into all children,
// and their children accordingly and echo the name of
// the vertex currently being visited to STDOUT.
func
recursiveWalk
(
v
*
Vertex
){
fmt
.
Printf
(
"Currently visiting vertex: %s\n"
,
v
.
Name
)
for
_
,
child
:=
range
v
.
Children
{
recursiveWalk
(
child
)
}
}
Auf den ersten Blick scheint eine einfache Implementierung eines Graphen eine vernünftige Wahl für die Lösung der Ressourcenzuordnung zu sein, da Abhängigkeiten durch den logischen Aufbau des Graphen gehandhabt werden können. Ein Graph würde zwar funktionieren, birgt aber auch Risiken und Komplexität. Das größte Risiko bei der Implementierung eines Graphen für die Ressourcenzuordnung wären Zyklen im Graphen. Ein Zyklus ist, wenn ein Knoten eines Graphen über mehr als einen Pfad auf einen anderen Knoten zeigt, was bedeutet, dass das Durchlaufen des Graphen eine endlose Operation ist.
Ein Graph kann bei Bedarf verwendet werden, aber in den meisten Fällen sollte das Reconciler-Muster mit einer Menge von Ressourcen und nicht mit einem Graphen abgebildet werden. Die Verwendung einer Menge ermöglicht es dem Reconciler, prozedural durch die Ressourcen zu iterieren, und bietet einen linearen Ansatz zur Lösung des Mapping-Problems. Außerdem ist das Rückgängigmachen oder Löschen von Infrastrukturen so einfach wie das Iterieren durch die Menge in umgekehrt.
Regel 4: Der tatsächliche Zustand muss mit dem erwarteten Zustand übereinstimmen
Das Reconciler-Pattern garantiert, dass der Benutzer genau das erhält, was beabsichtigt war, oder einen Fehler. Auf diese Garantie kann sich der Ingenieur, der den Reconciler verwendet, verlassen. Das ist wichtig, denn der Benutzer sollte sich nicht darum kümmern müssen, zu überprüfen, ob die Mutation des Reconcilers idempotent war und wie erwartet endete. Letztendlich ist die Implementierung dafür verantwortlich. Mit dieser Garantie ist die Verwendung des Reconciler-Patterns in komplexeren Operationen, wie z. B. in einem Controller oder Operator, jetzt viel einfacher.
Die Implementierung sollte, bevor sie zum aufrufenden Code zurückkehrt, prüfen, ob die neu abgestimmte tatsächliche Datenstruktur mit der ursprünglichen erwarteten Datenstruktur übereinstimmt. Ist dies nicht der Fall, sollte ein Fehler auftreten. Der Verbraucher sollte sich nie um die Validierung der API kümmern und darauf vertrauen können, dass der Reconciler einen Fehler macht, wenn etwas schief läuft.
Da die Datenstrukturen unveränderlich sind und die API einen Fehler macht, wenn das Abstimmungsmuster nicht erfolgreich ist, können wir der API ein hohes Maß an Vertrauen entgegenbringen. Bei komplexen Systemen ist es wichtig, dass du dich darauf verlassen kannst, dass deine Software auf vorhersehbare Weise funktioniert oder fehlschlägt.
Die Methoden des Reconciler-Musters
Mit den Informationen und Regeln des Versöhnungsmusters, die wir gerade erklärt haben, wollen wir uns nun ansehen, wie einige dieser Regeln umgesetzt wurden. Dazu schauen wir uns die Methoden an, die für eine Anwendung benötigt werden, die das Reconciler-Muster implementiert.
Die erste Methode des Reconciler-Patterns ist GetActual()
. Diese Methode wird manchmal auch als Audit bezeichnet und dient dazu, den aktuellen Zustand der Infrastruktur abzufragen. Die Methode erstellt eine Karte der Ressourcen und ruft dann prozedural jede Ressource auf, um zu sehen, was, wenn überhaupt, vorhanden ist. Die Methode aktualisiert die Datenstruktur auf der Grundlage der Abfragen und gibt eine gefüllte Datenstruktur zurück, die darstellt, was tatsächlich läuft.
Eine viel einfachere Methode, GetExpected()
, liest den beabsichtigten Zustand der Welt aus dem Datenspeicher. Im Fall des Beispiels infrastructure.yaml(Beispiel 4-4) würde GetExpected()
diese YAML einfach unmarshalieren und in Form der Datenstruktur im Speicher zurückgeben. Bei diesem Schritt wird keine Ressourcenprüfung durchgeführt.
Die spannendste Methode ist die Reconcile()
Methode, bei der der Versöhnungsimplementierung sowohl der tatsächliche Zustand der Welt als auch der erwartete Zustand der Welt übergeben wird.
Dies ist der Kern des absichtsgesteuerten Verhaltens des Reconciler-Patterns. Die zugrundeliegende Reconciler-Implementierung würde dieselbe Ressourcenzuordnungslogik verwenden, die in GetActual()
verwendet wird, um einen Satz von Ressourcen zu definieren. Die Reconciler-Implementierung würde dann mit diesen Ressourcen arbeiten und jede einzelne unabhängig voneinander abgleichen.
Es ist wichtig, die Komplexität der einzelnen Schritte des Ressourcenabgleichs zu verstehen. Die Implementierung des Abgleichs muss auf zwei Arten funktionieren.
Erhalte zunächst die Ressourceneigenschaften aus dem Soll- und Ist-Zustand. Als Nächstes wendest du Änderungen am minimalen Satz von Eigenschaften an, damit der Ist-Zustand dem Soll-Zustand entspricht.
Wenn sich die beiden Darstellungen der Infrastruktur zu irgendeinem Zeitpunkt widersprechen, muss die Reconciler-Implementierung eingreifen und die Infrastruktur verändern. Nachdem der Reconciliation-Schritt abgeschlossen ist, muss die Reconciler-Implementierung eine neue Darstellung erstellen und dann zur nächsten Ressource übergehen. Nachdem alle Ressourcen abgeglichen wurden, gibt die Reconciler-Implementierung eine neue Datenstruktur an den Aufrufer der Schnittstelle zurück. Diese neue Datenstruktur repräsentiert nun genau den tatsächlichen Zustand der Welt und sollte eine Garantie haben, dass sie mit der ursprünglichen tatsächlichen Datenstruktur übereinstimmt.
Die letzte Methode des Versöhnungsmusters ist die Methode Destroy()
. Das Wort Destroy()
wurde absichtlich anstelle von Delete()
gewählt, weil wir wollen, dass der Ingenieur weiß, dass die Methode die Infrastruktur zerstören und nicht deaktivieren soll. Die Implementierung der Methode Destroy()
ist einfach. Sie verwendet dieselbe Ressourcenzuordnung wie in den vorangegangenen Implementierungsmethoden, bearbeitet die Ressourcen aber nur in umgekehrter Reihenfolge.
Beispiel für das Pattern in Go
Beispiel 4-7 ist das Versöhnungsmuster, das in vier Methoden in der Programmiersprache Go definiert ist.
Hinweis
Mach dir keine Sorgen, wenn du Go nicht kennst. Das Muster kann problemlos in jeder Sprache implementiert werden. Wir verwenden Go, weil es den Ein- und Ausgabetyp jeder Methode klar definiert. Lies bitte die Kommentare zu jeder Methode, denn dort steht, was jede Methode tun muss und wann sie verwendet werden sollte.
Beispiel 4-7. Die Schnittstelle des Abstimmungsmusters
// The reconciler interface below is an example of the reconciler pattern.
// It should be used whenever a user intends on mutating infrastructure based on a
// state that might have changed over time.
type
Reconciler
interface
{
// GetActual takes no arguments for input and returns a populated data
// structure as well as a possible error. The data structure should
// contain a complete representation of the infrastructure.
// This is sometimes called an audit. This method
// should be used to get a real-time representation of what infrastructure is
// in existence.
GetActual
()
(
*
Api
,
error
)
// GetExpected takes no arguments for input and returns a populated data
// structure that represents what infrastructure an operator has declared to
// exist, as well as a possible error. This is sometimes called expected or
// intended state. This method should be used to get a real-time representation
// of what infrastructure an operator intends to be in existence.
GetExpected
()
(
*
Api
,
error
)
// Reconcile takes two arguments.
// actualApi is a populated data structure that is returned from the GetActual
// method. expectedApi is a populated data structure that is returned from the
// GetExpected method. Reconcile will return a populated data structure that is
// a representation of the new "actual" state, as well as a possible error.
// By definition, the data structure returned here should match
// the data structure returned from the GetExpected method. This method is
// responsible for making changes to infrastructure.
Reconcile
(
actualApi
,
expectedApi
*
Api
)
(
*
Api
,
error
)
// Destroy takes one argument.
// actualApi is a populated data structure that is returned from the GetActual
// method. Destroy will return a populated data structure that is a
// representation of the new "actual" state, as well as a possible error. By
// definition, the data structure returned here should match
// the data structure returned from the GetExpected method.
Destroy
(
actualApi
*
Api
)
(
*
Api
,
error
)
}
Die Beziehung zur Wirtschaftsprüfung
Mit der Zeit wird die letzte Prüfung unserer Infrastruktur veraltet, was das Risiko erhöht, dass unsere Darstellung der Infrastruktur ungenau ist. Der Kompromiss besteht also darin, dass ein Betreiber die Häufigkeit der Prüfungen gegen die Genauigkeit der Darstellung der Infrastruktur tauschen kann.
Eine Abstimmung ist implizit eine Prüfung. Wenn sich nichts geändert hat, stellt der Abstimmer fest, dass nichts getan werden muss, und der Vorgang wird zu einer Prüfung, bei der bestätigt wird, dass unsere Darstellung der Infrastruktur korrekt ist.
Wenn sich in unserer Infrastruktur etwas geändert hat, erkennt der Reconciler diese Änderung und versucht, sie zu korrigieren. Nach Abschluss des Reconcile ist der Zustand der Infrastruktur garantiert korrekt. Wir haben also implizit die Infrastruktur erneut geprüft.
Eine leichtgewichtige und stabile Reconciler-Implementierung kann leistungsstarke Ergebnisse liefern, die schnell abgeglichen werden und dem Betreiber Vertrauen in eine genaue Darstellung der Infrastruktur geben.
Verwendung des Reconciler-Musters in einem Controller
Orchestrierungswerkzeuge wie Kubernetes bieten eine Plattform, auf der wir Anwendungen bequem ausführen können. Die Idee eines Controllers ist es, einen Regelkreis für einen beabsichtigten Zustand zu bedienen. Kubernetes baut auf dieser Grundlage auf. Das Reconciler-Pattern macht es einfach, von Kubernetes kontrollierte Objekte zu überprüfen und abzugleichen.
Stell dir eine Schleife vor, die das Abstimmungsmuster in den folgenden Schritten endlos durchlaufen würde:
-
Rufe
GetExpected()
auf und lese aus einem Datenspeicher den beabsichtigten Zustand der Infrastruktur. -
Rufe
GetActual()
auf und lese aus einer Umgebung, um den aktuellen Zustand der Infrastruktur zu erfahren. -
Ruf
Reconcile()
an und versöhne die Staaten.
Das Programm, das das Abstimmungsmuster auf diese Weise implementiert, würde als Controller dienen. Die Schönheit des Musters wird sofort deutlich, denn es ist leicht zu erkennen, wie klein das Programm für den Controller selbst sein müsste.
Außerdem ist eine Änderung der Infrastruktur so einfach wie eine Mutation des State Stores. Der Controller liest die Änderung beim nächsten Aufruf von GetExpected()
und löst einen Abgleich aus. Der für die Infrastruktur verantwortliche Betreiber kann sich darauf verlassen, dass im Hintergrund eine stabile und zuverlässige Schleife läuft, die seinen Willen in seiner Infrastrukturumgebung durchsetzt. Jetzt verwaltet ein Betreiber die Infrastruktur, indem er eine Anwendung verwaltet.
Das zielführende Verhalten des Regelkreises ist sehr stabil. Das hat sich in Kubernetes bewährt, wo wir Fehler hatten, die unbemerkt geblieben sind, weil der Regelkreis grundsätzlich stabil ist und sich mit der Zeit selbst korrigiert.
Bei Edge-Triggering besteht die Gefahr, dass du deinen Zustand kompromittierst und nicht mehr in der Lage bist, ihn wiederherzustellen. Bei Level-Triggering ist das Muster sehr nachsichtig und lässt Raum für Komponenten, die sich nicht so verhalten, wie sie sollten. Das ist es, was Kubernetes so gut funktionieren lässt.
Joe Beda, CTO von Heptio
Um die Infrastruktur zu zerstören, müssen wir dem Controller lediglich mitteilen, dass wir die Infrastruktur zerstören wollen. Dies kann auf verschiedene Arten geschehen. Eine Möglichkeit wäre, dass der Controller eine deaktivierte Statusdatei respektiert. Dies könnte durch das Umschalten eines Bits von ein auf aus dargestellt werden.
Eine andere Möglichkeit wäre, den Inhalt des Zustands zu löschen. Unabhängig davon, wie ein Betreiber eine Destroy()
signalisiert, ist der Controller bereit, die praktische Destroy()
Methode aufzurufen.
Fazit
Infrastrukturingenieure sind jetzt Softwareentwickler, die fortschrittliche und hochgradig verteilte Systeme aufbauen - und rückwärts arbeiten. Sie müssen Software schreiben, die die Infrastruktur verwaltet, für die sie verantwortlich sind.
Obwohl es viele Ähnlichkeiten zwischen den beiden Disziplinen gibt, muss man das Handwerk des Infrastrukturmanagements ein Leben lang erlernen. Schwierige Probleme, wie z.B. der Aufbau einer Infrastruktur, entwickeln sich ständig weiter und erfordern, dass Ingenieure immer wieder neue Dinge lernen. Außerdem muss die Infrastruktur ständig gewartet und optimiert werden, was die Ingenieurinnen und Ingenieure mit Sicherheit sehr lange beschäftigen wird.
Das Kapitel hat den Nutzer mit leistungsfähigen Mustern und Grundlagen für die Abbildung von mehrdeutigen API-Strukturen in granulare Ressourcen ausgestattet. Die Ressourcen können in deinem lokalen Rechenzentrum, auf einer privaten Cloud oder in einer öffentlichen Cloud eingesetzt werden.
Um zuverlässige Anwendungen für das Infrastrukturmanagement zu entwickeln, ist es wichtig, die Grundlagen der Funktionsweise dieser Muster zu verstehen. Die in diesem Kapitel vorgestellten Muster sollen Ingenieuren einen Ausgangspunkt und Inspiration für die Entwicklung von deklarativen Anwendungen für das Infrastrukturmanagement bieten.
Bei der Entwicklung von Infrastrukturmanagement-Anwendungen gibt es keine richtige oder falsche Antwort, solange die Anwendungen der Unix-Philosophie entsprechen: "Tu eine Sache. Mach es gut."
1 Erinnere dich an Kapitel 1, dass cloud-native Anwendungen mehr als nur rohe Infrastrukturkomponenten benötigen und dass die Abstraktionen, die wir über die API bereitstellen, von den Anwendungen als Teil einer Plattform direkt genutzt werden können sollten.
2 In Kapitel 8 über Prüfungen findest du noch mehr Gründe, warum dies wichtig ist.
Get Cloud Native Infrastruktur now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.