Die Entwicklung und Wartung von Infrastruktur kann für Ops- und SRE-Teams eine abschreckende Erfahrung sein – nach dem Motto: „Never change a running system.“ Doch das muss nicht so sein!
Michael Vittrup Larsen
Michael is a Consultant at our office in Aarhus. He has an MScEE from Aalborg University where he specialized in digital signal processing. Before joining us Michael was working on building and optimizing cloud infrastructure for the telecoms sector. In his spare time, he likes to explore the wilderness on his mountain bike.
Es gibt drei zentrale DevOps-Praktiken, die unsere Softwarebereitstellung und operative Leistungsfähigkeit beeinflussen (Lesetipp: „Infrastructure as Code“ von Kief Morris):
Alles als Code definieren
Alle laufenden Arbeiten kontinuierlich testen und bereitstellen
Kleine, einfache Komponenten entwickeln, die sich unabhängig voneinander ändern lassen
Wenn ihr moderne Software entwickelt, sind diese Prinzipien für euch keine Überraschung. Aber wusstet ihr, dass sie auch für die Entwicklung moderner Infrastruktur gelten?
In diesem Blogbeitrag erfahrt ihr, wie ihr Infrastruktur kontinuierlich testet – auch bekannt als Continuous Integration.
Continuous Integration für Infrastructure as Code
Softwareentwickler haben sich an schnelle Zyklen gewöhnt, ohne dabei Abstriche bei Qualität und Stabilität zu machen. Möglich wird das durch automatisierte Tests aller Codeänderungen, sobald sie in Git eingecheckt werden. Änderungen, die die automatisierten Tests nicht bestehen, dürfen nicht in unsere Codebasis gemergt werden. Automatisierte Tests sind die Leitplanken, die eine schnelle Softwareentwicklung ermöglichen.
Dieselben Continuous-Integration-Praktiken lassen sich auch auf Infrastruktur anwenden. Wir können Richtlinien für Eigenschaften unserer Infrastruktur definieren. Diese Richtlinien lassen sich durch Tests validieren. So erreichen wir die Kombination aus Geschwindigkeit, Stabilität und Qualität, die wir aus der Softwareentwicklung erwarten.
Infrastruktur umfasst nicht nur Server
Mit „Infrastruktur“ meinen wir in diesem Kontext alles, was wir über einen „Infrastructure as Data“-Ansatz programmatisch betreiben können. Dazu gehören beispielsweise:
Server
Netzwerke
Benutzerauthentifizierung und -autorisierung
Application Deployments
Pipeline-Konfiguration
Typische Beispiele sind alles, was wir mit Terraform verwalten können, sowie alle Kubernetes-Ressourcentypen.
Infrastructure as Code vs. Infrastructure as Data
Die Begriffe „Infrastructure as Code“ und „Infrastructure as Data“ werden oft synonym verwendet, meinen aber unterschiedliche Dinge. Außerdem wird häufig „deklarative Infrastruktur“ verwendet, um das zu beschreiben, was wir hier als „Infrastructure as Data“ bezeichnen. Entscheidend ist jedoch:
Wir können Aussagen über Infrastruktur treffen, die als Daten definiert ist. Bei Infrastructure as Code ist das deutlich schwieriger.
Der folgende Befehl erstellt mithilfe der AWS CLI eine VM-Instanz in der AWS Cloud. Dabei werden ein bestimmtes Maschinen-Image (ein „AMI – Amazon Machine Image“) und eine Maschinenkonfiguration („Instance Type“) angegeben.
Dieser Befehl könnte in einem Skript stehen, und wir hätten „Infrastructure as Code“. Stellt euch jedoch vor, das Skript sollte idempotent sein, sodass es beim zweimaligen Ausführen nur eine VM erstellt. Das ließe sich mit entsprechender Skriptlogik umsetzen, doch das Skript könnte dadurch letztlich ziemlich komplex und fehleranfällig werden.
Ein alternativer Ansatz wäre beispielsweise Terraform. Damit könnten wir die folgende „Datenstruktur“ definieren, die den gewünschten Zustand unserer Infrastruktur beschreibt. Diese „Infrastructure as Data“ hat den Vorteil, dass Terraform die gesamte Logik übernimmt, die nötig ist, um unseren gewünschten Zustand mit dem tatsächlichen Zustand der Infrastruktur abzugleichen. Kubernetes funktioniert ähnlich, wenn auch mit anderen „Datenstrukturen“ (auch als „Kubernetes Resource YAML“ bezeichnet).
Neben der Tatsache, dass beispielsweise Terraform oder Kubernetes einen Teil der Komplexität des Infrastrukturmanagements übernehmen, bietet auch die Art der Infrastrukturdefinition einen weiteren Vorteil. Bei Infrastructure as Code ist es generell sehr schwierig, Auswirkungen und Korrektheit des Codes zu beurteilen. Ein Tool, das eine solche Überprüfung durchführt, müsste zudem mehrere Sprachen und externe Tools unterstützen.
Stellt euch zum Beispiel Shell-Skripte vor, die AWS CLI zusammen mit jq, sed, awk, grep usw. verwenden. Mit dem Datenstrukturansatz ist es deutlich einfacher, da die Datenstruktur in der Regel ein relativ einfaches Format hat und nur die Schemata der einzelnen Objekte in der Datenstruktur domänenspezifisch sind, etwa AWS-Terraform-Ressourcen oder Kubernetes-Ressourcen.
Daher arbeiten Tools wie Open Policy Agent mit Daten, und zu den Anwendungsfällen gehört Infrastruktur als Daten.
Open Policy Agent und verwandte Tools
Sehen wir uns nun an, wie wir diese Tools nutzen können, um Richtlinien für unsere Infrastruktur als Daten umzusetzen:
Open Policy Agent (OPA) ist das grundlegende Tool, auf dem die folgenden Tools aufbauen. OPA ist eine Policy Engine, mit der wir Richtlinien als Code definieren können, gegen den unsere Infrastruktur getestet werden kann. Richtlinien werden in einer Sprache namens Rego definiert.
Conftest ist ein Tool, das OPA integriert und strukturierte Daten in verschiedenen Formaten wie YAML und JSON akzeptiert. Bei diesen Daten kann es sich beispielsweise um Kubernetes-Ressourcen im YAML-Format oder einen Terraform-Plan im JSON-Format handeln. Mit Conftest können wir Rego-Richtlinien gegen diese Daten auswerten. Conftest eignet sich ideal für die Validierung von Infrastruktur als Teil einer CI-Pipeline.
Regula ist eine Rego-Bibliothek, die zusammen mit Conftest verwendet werden kann und speziell für Terraform-Infrastrukturdaten entwickelt wurde.
GateKeeper ist eine Integration von OPA in Kubernetes. GateKeeper ist ein Kubernetes Admission Controller, der Kubernetes-Ressourcenoperationen über die Kubernetes API steuert. Er kann beispielsweise die Erstellung von PODs anhand von Rego-Richtlinien erlauben oder verweigern, die von OPA gegen die POD-Definition ausgewertet werden. GateKeeper wird also nicht während des CI-Prozesses eingesetzt (dort verwenden wir Conftest), sollte aber mit denselben Richtlinien wie Conftest verwendet werden, um zu steuern, was tatsächlich auf Kubernetes deployt wird.
Die Sprache Rego
Rego ist eine Datenabfragesprache. Im Kern der Sprache steht das Konzept der Regeln, die aus Abfragen bestehen, welche einen bestimmten Zustand der abgefragten Daten bestätigen.
Sehen wir uns ein einfaches Beispiel mit dem folgenden Datensatz an:
Wir können diesen Datensatz mit Rego-Regeln abfragen und auf Grundlage einer Reihe von Bedingungen neue Datensätze erstellen. In diesem Sinne ähneln Rego-Regeln Datenbankabfragen. Die folgende Regel erstellt einen Datensatz mit allen menschlichen Kreaturen aus dem Datensatz:
Die Codezeilen dieser Regel sind wie folgt zu lesen:
Der Regelname lautet „humans“, sie nimmt die Eingabe „name“ entgegen und gibt einen Datensatz zurück, der auf den „creature“-Zuweisungen im Regelkörper basiert.
Weise „creature“ einen beliebigen Wert aus dem Datensatz „creatures“ zu – die Unterstrich-Variable bedeutet „Schleifenvariable, deren Wert uns nicht interessiert“. Innerhalb dieser Regel nimmt „creature“ alle vier Werte aus der Liste „creatures“ an. Das ähnelt also einem for-Loop.
Bestätige, dass „creature“ ein Mensch ist. Wenn eine Bedingung zu false ausgewertet wird, enthält der resultierende Datensatz nicht den jeweiligen Wert von „creature“.
Weise „name“ aus der jeweiligen „creature“ zu.
Beachtet, dass wir keinen Wert für die Eingabe „name“ angegeben haben. Das bedeutet, dass diese Variable nicht an einen bestimmten Wert gebunden ist und die Regeln daher ohne Einschränkung des Werts von „name“ aufgelöst werden. Hätten wir einen Wert angegeben, wäre unser generierter Datensatz entsprechend eingeschränkt:
In Rego steht das Gleichheitszeichen „=“ sowohl für Vergleich als auch für Zuweisung. Es kann sowohl von links nach rechts als auch von rechts nach links zuweisen und sollte daher als mathematische Gleichung betrachtet werden. Die folgende Zeile aus der obigen Regel war bei der ersten Ausführung der Regel eine Zuweisung, weil wir keinen Wert für „name“ angegeben hatten, und bei der späteren Ausführung der Regel ein Vergleich.
Die Reihenfolge der Anweisungen und sogar die Reihenfolge der Elemente auf der linken und rechten Seite des Gleichheitszeichens „=“ spielt keine Rolle. Daher ist die folgende Regeldefinition mit der obigen identisch:
Wir können unsere beiden Datensätze mit einer Regel wie der folgenden zusammenführen:
In dieser Regel erfolgt die Verknüpfung des Datensatzes „favorite_food“ mit der Liste dessen, was jede Kreatur mag, in der letzten Zeile. Beachtet erneut den Unterstrich, der für die Indizierung in die Liste „likes“ verwendet wird – der tatsächliche Index ist in diesem Fall nicht relevant.
Eine typische Vorgehensweise mit Rego besteht darin, grundlegende Regeln zu erstellen und diese anschließend zu komplexeren Regeln zu kombinieren. Die folgende Regel kombiniert die beiden vorherigen Regeln und gibt eine Liste von Menschen zurück, die die Lebensmittel in der Favoritenliste mögen:
Wenn wir diese Regel ausführen, erhalten wir:
Beispiel – AWS-Tagging-Richtlinie
Tagging ist eine sehr nützliche Methode beim Deployment von Ressourcen auf der AWS-Cloud-Plattform. Stellen wir uns vor, wir haben eine Unternehmensrichtlinie, nach der alle Ressourcen mit einem „Owner“-Tag versehen werden müssen, der festlegt, wer für eine bestimmte Ressource verantwortlich ist. Wie können wir das mit Rego umsetzen?
Die folgende Richtlinie (angepasst aus Projektbeispielen von Regula) setzt diese Tagging-Anforderung mit Conftest und Regula um.
Zunächst definieren wir eine Liste von AWS-Ressourcentypen, die Tagging unterstützen (die Liste ist der Übersichtlichkeit halber gekürzt), sowie eine Liste von Tags, die wir für unsere Ressourcen voraussetzen. aws_instance ist der oben gezeigte VM-Ressourcentyp, und das Folgende, das zwei Mengen definiert, unterscheidet sich kaum von anderen Sprachen.
Als Nächstes definieren wir eine Rego-Regel, die unsere AWS-Ressourcenliste mit der Liste der AWS-Ressourcentypen verknüpft, die mit Tags versehen werden können. Das ähnelt stark unserem obigen Regelbeispiel für „humans“:
Als Nächstes definieren wir eine Rego-Funktion, die für eine Ressource die Tags dieser Ressource mit unserer Liste erforderlicher Tags vergleicht.
Abschließend definieren wir die Richtlinie. Anhand des Ergebnisses der obigen Rego-Regel bzw. -Funktion entscheiden wir, ob wir eine bestimmte Ressource zulassen oder ablehnen:
Beispiel – Richtlinie für Kubernetes-Service-Typen
Wenn euer Team mehrere Services in Kubernetes bereitstellt, möchtet ihr Load Balancer und TLS-Zertifikate möglicherweise global verwalten, etwa über eine mehrstufige Routing-Architektur für die Verkehrssteuerung. In einer solchen Situation möchten wir sicherstellen, dass Teams keine Kubernetes-Services vom Typ „LoadBalancer“ bereitstellen. Wie könnte eine Rego-Richtlinie dafür aussehen?
Die folgende Richtlinie setzt diese Anforderung für Kubernetes-Service-Typen um und verwendet „input“ als Quelle für die bewertete Kubernetes-Ressource.
Ihr fragt euch jetzt vielleicht: „Warum nicht einfach Kubernetes RBAC nutzen, um einzuschränken, was bereitgestellt werden kann?“
Der Unterschied besteht darin, dass wir mit Rego-Richtlinien unsere Infrastrukturspezifikation vor dem Deployment validieren. Das unterscheidet sich von der Durchsetzung einer Richtlinie, nachdem die Infrastruktur bereitgestellt wurde. Ähnlich führen wir Tests als Teil von CI-Pipelines aus, um Fehler vor dem Deployment zu erkennen.
Richtlinien mit GateKeeper durchsetzen
Die beiden vorherigen Richtlinienbeispiele haben gezeigt, wie sich Infrastrukturressourcen beispielsweise als Teil einer CI-Pipeline validieren lassen, bevor Änderungen für das Deployment akzeptiert werden. Rego-Richtlinien können über den Kubernetes-Admission-Controller GateKeeper auf ähnliche Weise in Kubernetes durchgesetzt werden. GateKeeper-Richtlinien werden in Rego implementiert und in Kubernetes über Custom Resource Definitions konfiguriert. Eine Richtlinie, die der obigen für Kubernetes-Services vom Typ LoadBalancer sehr ähnelt, findet ihr beispielsweise in der Kubernetes-Gatekeeper-Vorlage für Service-Typen.
Typische Dinge, die eure Richtlinien überprüfen könnten:
hostPath-Volumes nicht zulassen
Ressourcenlimits und -anforderungen für Container festlegen – und dabei sinnvolle Werte verwenden
Container-Images nur aus bestimmten Registries beziehen; gegebenenfalls zusätzlich sicherstellen, dass Container-Tags Digests und keine veränderlichen Tags (wie „1.0“) sind
Bestimmte NodePort-Bereiche nicht zulassen
Erforderliche und/oder eindeutige Tags
Gültige und/oder eindeutige Ingress-Pfade
PODs verfügen über Readiness- und Liveness-Probes
Container nicht als root ausführen
GateKeeper-Richtlinienvorlagen können über Parameter konfiguriert werden, sodass sich unterschiedliche Richtlinien für verschiedene Namespaces einrichten lassen. Ein Beispiel findet ihr in der GateKeeper-Richtlinie ContainerLimits.
Einschränkungen deklarativer Tests
Wenn Infrastruktur mit einem deklarativen „as data“-Ansatz definiert wird, ist der sinnvolle Umfang von Tests begrenzt. Tests führen schnell dazu, dass wir unsere eigenen deklarativen Aussagen testen, wie im folgenden Beispiel:
Deklarative Tests sind beispielsweise besonders nützlich für Richtlinien und Schnittstellen zwischen Teams. Hier liegen die Verantwortung für die Zuweisung und den obigen Test bei unterschiedlichen Parteien.
Die in diesem Blogbeitrag gezeigten „Data Assertions“ können ebenfalls nur Aussagen über die verfügbaren Daten treffen. Komplexe Systeme können Daten an vielen verschiedenen Orten und in unterschiedlichen Formaten enthalten, wobei der Datenzugriff aus Sicherheitsgründen oft getrennt ist. Dadurch ist es schwierig oder sogar unmöglich, die Daten zu beschaffen, die für die Implementierung von Tests notwendig sind.
Abschließende Worte
Wir haben gesehen, wie sich Richtlinien für Terraform- und Kubernetes-Ressourcen schreiben lassen. Rego und die auf OPA basierenden Tools können jedoch für eine Vielzahl von Richtlinienprüfungen eingesetzt werden. Wenn eure Pipelines beispielsweise in YAML definiert sind, warum Änderungen dann nicht mit einer Rego-basierten Richtlinie prüfen, bevor ihr sie übernehmt?
Die Lernkurve für Rego mag etwas steil sein. Da Rego jedoch für so viele Arten von Infrastructure as Code genutzt werden kann, ist die dafür investierte Zeit möglicherweise gut angelegt, um nicht eine Vielzahl unterschiedlicher Tools für Richtlinien einsetzen zu müssen. Entscheidend ist, dass eure Infrastruktur „als Daten“ und nicht „als Code“ definiert ist. Der Ansatz von Rego zur Abfrage von Datenstrukturen funktioniert nicht mit „Infrastructure as Code“.
Rego kann Daten aus externen Systemen einbinden und in Richtlinien verwenden. Der Umfang der möglichen Prüfungen ist daher groß.
Doch es gibt Dinge, die Rego-Richtlinien nicht leisten können. Rego-Richtlinien lassen sich für eure Infrastruktur der Kategorie „Komponententests“ zuordnen. Bei dynamischen Aspekten wie Interaktionen zwischen Anwendungskomponenten, Netzwerklatenzen und -ausfällen sowie Charakteristiktests sind Rego-Richtlinien weniger geeignet. Lasst euch dadurch aber nicht von der höheren Geschwindigkeit und Robustheit abhalten, die Rego-Richtlinien zu eurem Continuous Delivery für die Infrastruktur beitragen können!
- DevOps
- Cloud
- CI/CD
Subscribe to our newsletter
Related blogs