Wie bereits im ersten Kapitel erläutert, ist die klassische IT funktionsorientiert strukturiert: Das Operations-Team ist für den Betrieb der Systeme zuständig, stellt Datenbanken bereit, führt Sicherungen durch, löst Mitarbeiteranfragen und passt Konfigurationen an. DevOps hingegen verfolgt das genaue Gegenteil: »You build it, you run it« – eine Aussage, die von Amazons Chief Technology Officer und Vizepräsident Werner Vogels geprägt wurde und das Vorgehensmodell auf den Punkt bringt. Ein Entwicklungsteam ist dabei nicht nur für die Implementierung der Software, sondern auch für die Stabilität im Produktivbetrieb verantwortlich.
Anstieg der Komplexität
Die Komplexität der IT-Infrastruktur hat stark zugenommen. Microservices und die Migration von Anwendungen in Public Clouds haben zur Folge, dass die traditionelle Definition der Infrastruktur als Computing-Power, Netzwerk und Speicher, die früher als Gesamtpaket gemietet wurden, nicht mehr ausreicht. Die Managed Services der Hyperscaler bieten eine zusätzliche Abstraktionsebene zwischen physischer Hardware und Anwendungen in Containern. Zusätzlich werden externe Ressourcen wie Datenbanken, Objektspeicher und Load-Balancer für die Lastverteilung erforderlich. Diese Anforderungen sind zu komplex, um von einem dedizierten Team gelöst zu werden.
Terraform, Pulumi und Crossplane
Wie bekannt ist, dienen Werkzeuge für IaC wie Terraform1, Pulumi2 oder Crossplane3 dazu, eine umfassende Infrastruktur mithilfe von einfachen Terraform-Manifesten oder YAML-Dateien zu beschreiben und immer wieder aufs Neue zu erstellen. Sie bilden die Basis, auf der ein Entwicklungsteam eigenständig eigene Anwendungen und Umgebungen entwickeln und betreiben kann. Die vollständige Beschreibung der Infrastrukturkomponenten und ihrer Konfiguration befindet sich in einem Git-Repository als Manifeste. Im Idealfall reduzieren sie durch ihr Design auch die Komplexität der Plattform, indem sie möglichst deklarativ und präzise gestaltet sind und das gewünschte Ergebnis beschreiben.
Die meisten Cloud-Anbieter bieten mittlerweile eigene IaC-Lösungen an, die plattformspezifisch sind. Im Gegensatz dazu arbeiten Terraform, Crossplane und Pulumi als unabhängige Tools und gehen über die reine Cloud-Ressourcen Provisionierung hinaus.
Bei GitOps werden im Wesentlichen alle Aspekte einer Infrastruktur und der darauf laufenden Software deklarativ beschrieben. Sie enthalten keine Implementierungsdetails, das heißt, wie der Zielzustand erreicht werden soll. Dies ist die Aufgabe des IaC-Tools. Der GitOps-Controller verwendet diese Beschreibungen dann für das Deployment der Infrastruktur.
In diesem Kapitel beschreiben wir, wie GitOps mit Crossplane, Terraform und Pulumi kombiniert werden kann, um Infrastrukturen zu bauen und zu verwalten. Der Prozess besteht aus folgenden Punkten:
Das Ziel ist es, mit allen drei Plattformen eine EC2-Instanz zu provisionieren.
Seit 2014 wird Terraform von HashiCorp als Open-Source-Projekt weiterentwickelt. Mit Terraform ist es möglich, Ressourcen auf einfache und übersichtliche Weise zu beschreiben. Die Provisionierung der Ressourcen wird von Terraform automatisch durchgeführt. Zur Beschreibung der Ressourcen nutzt Terraform die HCL, die auf JSON basiert. HCL erlaubt nicht nur die statische Deklaration von Ressourcen, sondern auch die Verwendung von Variablen, Schleifen, booleschen und mathematischen Ausdrücken. Terraform gewährleistet eine saubere Auflösung der Befehle und Abhängigkeiten.
Das folgende Beispiel zeigt, wie eine AWS-EC2-Instanz mit Terraform definiert werden kann.
Listing 11–1
Anlegen einer EC2-Instanz mit Terraform
terraform {
2
required_providers {
3
aws = {
4
source = "hashicorp/aws"
5
version = "~> 4.16"
6
}
7
}
8
9
required_version = ">= 1.2.0"
10
}
11
12
provider "aws" {
13
region = "us-west-2"
14
}
15
16
resource "aws_instance" "app_server" {
17
ami = "ami-830c94e3"
18
instance_type = "t2.micro"
19
20
tags = {
21
Name = "ExampleAppServerInstance"
22
}
23
}
Der terraform-Block dient sowohl zur Konfiguration von Terraform selbst als auch der benötigten Provider. Hier können die benötigten Provider aufgelistet werden. Terraform installiert diese mittels der Terraform Registry. Der provider-Block dient zur Konfiguration der jeweiligen Provider. In diesem Fall geben wir an, welche AWS-Region wir nutzen möchten.
Der ressource-Block beinhaltet die konkrete Ressource. Eine Ressource kann eine physische oder virtuelle Komponente sein, wie in diesem Fall eine EC2-Instanz, oder eine logische Ressource, wie zum Beispiel ein Kubernetes-Deployment. Ressourcenblöcke können Argumente enthalten, die zur Konfiguration der Ressource verwendet werden. Argumente können Maschinengrößen, Abbildnamen für Festplatten oder VPC-IDs sein. Die AWS Provider Reference4 enthält eine Liste der erforderlichen und optionalen Argumente für jede Ressource. In diesem Beispiel wird eine EC2-Instanz provisioniert, die anhand dieser AMI ein Ubuntu-Image und t2.micro als Instanztyp verwendet. Zusätzlich wird ein Tag für die Annotation der Ressource gesetzt.
Für die Bereitstellung der Ressourcen muss aktiv ein apply erfolgen: Hierzu werden die beiden Kommandos terraform plan und terraform apply bereitgestellt: Mit terraform plan kann ein dry-run durchgeführt werden; dieser Befehl dient lediglich zur Nachvollziehbarkeit. So ist ersichtlich, welche Ressourcen angelegt, verändert oder gelöscht werden. Da zwischen terraform plan und terraform apply eine Abweichung auftreten kann, wird empfohlen, den Plan in eine Datei zu speichern und beim terraform apply diesen als Parameter anzugeben.
Das obige Beispiel erzeugt folgende Ausgabe in der Konsole.
Listing 11–2
Ausgabe nach terraform plan
1
> terraform plan
2
3
Terraform will perform the following actions:
4
5
# aws_instance.app_server will be created
6
+ resource "aws_instance" "app_server" {
7
+ ami = "ami-830c94e3"
8
+ arn =
9
(known after apply)
10
+ associate_public_ip_address =
11
(known after apply)
12
...
13
+ tags = {
14
+ "Name" = "ExampleAppServerInstance"
15
}
16
+ tags_all = {
17
+ "Name" = "ExampleAppServerInstance"
18
}
19
}
20
21
Plan: 1 to add, 0 to change, 0 to destroy.
Eine persistente/dauerhafte Veränderung des Zustands der Ressourcen erfolgt mit terraform apply. Sofern notwendig, werden automatisiert neue Ressourcen angelegt und bestehende verändert oder gelöscht.
Im Folgenden eine Ausgabe, wenn ein apply durchgeführt wird:
Listing 11–3
Ausgabe nach terraform apply
1
> terraform apply
2
3
aws_instance.app_server: Creating...
4
aws_instance.app_server: Still creating... [10s elapsed]
5
aws_instance.app_server: Still creating... [20s elapsed]
6
aws_instance.app_server: Still creating... [30s elapsed]
7
aws_instance.app_server: Still creating... [40s elapsed]
aws_instance.app_server: Still creating... [50s elapsed]
9
aws_instance.app_server: Creation complete after 56s
10
[id=i-0382a478c32c4af1f]
11
12
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Terraform Provider
Ein wichtiges Bindeglied zwischen den Cloud-Provider APIs und der Terraform-Ressource sind Provider.
Abb. 11–1
Terraform Provider
Dabei handelt es sich um Plugins, welche die definierten Ressourcen in API-Befehle übersetzen. Neben den Terraform-Providern für AWS oder Azure zur Provisionierung von Cloud-Ressourcen gibt es eine Vielzahl an weiteren Providern für GitHub5, Kubernetes6 und Splunk7. Die Terraform Registry8 ist die erste Anlaufstelle für öffentlich verfügbare Provider. Über diese Website kann schnell der passende Provider herausgesucht werden. Zudem werden für jeden Provider gleich die passende Dokumentation zu Ressourcen sowie Codebeispiele mitgeliefert. Trotz der anbieterunabhängigen HCL beziehen sich die Ressourcentypen immer auf einen bestimmten Provider. Im Falle einer Portierung zu einem anderen Provider müssen die betroffenen Ressourcen angepasst werden.
Terraform State
Nach jeder Operation wird der Zustand der Terraform-Ressourcen in einer separaten Datei abgespeichert. Der State ist die Zustandsbeschreibung aller definierten Ressourcen und eine Bestandsaufnahme derer Metadaten. Er wird außerdem benötigt, um alle Abhängigkeiten darzustellen und aufzulösen. Die Datei, in der dieser State gespeichert wird, ist standardmäßig die Datei terraform.tfstate. Diese liegt immer im JSON-Format vor.
Wie bereits beschrieben, wird vor jeder Operation von Terraform der Zustand abgefragt, ausgewertet und aktualisiert. Dazu werden alle verwendeten Terraform-Provider angewiesen, den aktuellen Zustand jeder einzelnen definierten Ressource zu liefern. Je nach Größe, Komplexität und Provider des Terraform-Projekts kann dies unterschiedlich lange dauern. Die Aktualisierung ist jedoch der einzige Weg, um zu verhindern, dass Terraform mit veralteten Metadaten arbeitet. Es ist wichtig zu betonen: Änderungen an der von Terraform verwalteten Infrastruktur sollten nur über Terraform vorgenommen werden. Änderungen von außerhalb führen entweder zu einem nicht mehr aktualisierbaren Zustand oder zu einer entsprechenden Warnung. Der Zustand kann sowohl lokal als auch remote in einem S3-Bucket gespeichert werden. Dazu wird eine Terraform Config benötigt, um den entsprechenden Bucket zu adressieren.
Module in Terraform
Um Wiederholungen im Code zu vermeiden und Code möglichst so zu schreiben, dass eine Mehrfachnutzung leicht möglich ist, können Terraform-Ressourcen als Module zusammengefasst werden. Durch das Auslagern von Code können Teile des Codes in einen abgekapselten, klar definierten und in sich stimmigen Baustein ausgelagert werden. Das nachfolgende Beispiel veranschaulicht dieses Prinzip.
Zunächst legen wir einen neuen Ordner an für das neue Modul und erstellen die Dateien main.tf, variables.tf und outputs.tf.
Abb. 11–2
Module in Terraform
In der modules/bucket/main.tf unseres Moduls können wir nun modulabhängige Ressourcen definieren:
Listing 11–4
Ressource innerhalb unseres Moduls
1
resource "x_cloud_s3_bucket" "bucket" {
2
name = var.name
3
# further config
4
}
Für das Modul sind zwei zusätzliche Dateien modules/bucket/variables.tf und modules/bucket/outputs.tf definiert. Die erste dient als Schnittstelle zwischen Modul und Ressourcen außerhalb dieses Kontexts. Hier können Werte von außerhalb in das Modul eingebracht werden. Dazu werden Variablen wie folgt angelegt:
Listing 11–5
Übergabe externer Variablen in unser Modul
Im Modul selbst können wir diese Variable mit var. nutzen.
Oft müssen zusätzliche Ressourcen für eine Ressource definiert werden, die möglicherweise in anderen Modulen bearbeitet wird. Zum Beispiel müssen für eine VPC zusätzliche Subnetze oder Security Groups definiert werden, oder wenn wir spezifizieren möchten, in welchem VPC unsere EC2-Instanz deployt werden soll. Um Werte aus dem Modul weiterverwenden zu können, dient die outputs.tf. Output-Variablen werden hier wie folgt zusammengefasst:
Listing 11–6
Output-Variablen zur Nutzung außerhalb des Moduls
1
output "name" {
2
value = x_cloud_s3_bucket.bucket.name
3
}
Diese Variable zeigt auf ein konkretes Attribut einer Ressource – in diesem Fall auf den Namen.
In der main.tf können wir zu unseren Ressourcen unser Modul definieren und nutzen.
Listing 11–7
Nutzung eines Moduls in der main.tf
1
variable "bucket_list" {
2
default = [
3
"Item1",
4
"Item2"
5
]
6
}
7
resource "x_cloud.public_keypair" "keypair" {
8
# further config
9
}
10
resource "x_cloud.virtual_machine" "vm" {
11
# further config
12
}
13
resource "x_cloud.elb" "elb" {
14
# further config
15
}
16
module "bucket" {
17
count = 2
18
source = "./modules/bucket"
19
name = element(var.bucket_list, count.index)
20
}
In diesem Beispiel möchten wir zwei Buckets provisionieren. Dazu sind die Namen der Buckets in dem Variablen-Block definiert. Wir übergeben diese Liste weiter an unsere Moduldefinition. Diese Definition beinhaltet neben dem Zielverzeichnis auch die Schnittstellenvariable. So übergeben wir die Namen von außen in dieses Modul. Auf diese Weise haben wir die Duplizierung des Codes gespart.
In diesem Beispiel werden zwei Buckets erstellt. Die Namen der Buckets sind in einem Variablen-Block definiert, der anschließend an die Moduldeklaration übergeben wird. Diese Definition enthält sowohl das Zielverzeichnis als auch die Schnittstellenvariable. Dadurch werden nur die Namen von außen an das Modul übergeben.
Die Terraform-Registry9 enthält neben Providern auch viele vorgefertigte Module, die genutzt werden können. Dazu müssen lediglich die entsprechende source und version definiert werden.
Listing 11–8
Importieren eines Moduls aus der Terraform-Registry
1
module "security-group" {
2
source = "terraform-aws-modules/security-group/aws"
3
version = "5.1.0"
4
}
HashiCorp hat bekannt gegeben, dass sie ab sofort für alle zukünftigen Veröffentlichungen ihrer Produkte anstelle der Mozilla Public License 2.0 (MPL 2.0) nun die Business Source License (BSL) 1.1 verwenden werden10. Diese Lizenz erfüllt jedoch nicht die Kriterien der Open Source Initiative und schränkt die Nutzung des Codes in Cloud-Systemen, die mit den Produkten und Dienstleistungen von HashiCorp konkurrieren, ein11. HashiCorp verabschiedet sich vom Open-Source-Ansatz, den es seit seiner Gründung verfolgt hat. Die Änderung wurde zügig umgesetzt und Terraform wird seit dem Minor-Release 1.6 unter der neuen Lizenz lizenziert.
Wir als Endanwender oder Einzelpersonen sind von Lizenzänderung per se nicht betroffen. Dies ist explizit in der FAQ-Seite12 ausdrücklich so beschrieben. Der entscheidende Faktor betrifft die Regelung, unter welchen Bedingungen als Konkurrent zu HashiCorp angesehen wird.
Legitimität und potenzielle Auswirkungen der Lizenzänderung
HashiCorp ist ein börsennotiertes Unternehmen, das durch seine kommerziellen Angebote wie Terraform Cloud und Vault Geld erwirtschaften möchte. Diese Gewinne sind auch dazu da, um die verschiedenen Projekte weiterzuentwickeln. Aus diesem Grund ist es durchaus legitim, die eigenen Vermögenswerte zu schützen und den Konkurrenten von HashiCorp keine Hilfe zu gewähren. So ist es bereits vorgekommen, dass Unternehmen wie AWS Open-Source-Projekte wie den Elastic-Stack oder MariaDB genommen und auf dieser Basis einen Managed Service entwickelt und vertrieben haben13. Die Beschreibung, wie man als Unternehmen mit den Angeboten von HashiCorp konkurrieren kann, ist jedoch sehr vage. Angenommen, wir entwickeln ein ähnliches Produkt wie HashiCorp Vault und stellen die Infrastruktur mit Terraform bereit, stehen wir dann direkt in Konkurrenz zu HashiCorp? Weiterhin behält sich HashiCorp vor, weitere Lizenzänderungen vorzunehmen, sodass eine weitere Einschränkung der Nutzung denkbar wäre.
OpenTofu
Mehrere Unternehmen, Projekte und Einzelpersonen haben sich zu OpenTF zusammengeschlossen und ein Manifest14 verfasst. Das Ziel von OpenTF ist es, dass HashiCorp diese Lizenzänderung zurückzieht und Terraform weiterhin Open Source bleibt. Andernfalls würde man das Projekt »forken«, eine eigene Version erstellen und diese einer Stiftung übergeben, um sicherzustellen, dass das Projekt weiterhin unter einer freien Lizenz als Open Source weiterentwickelt wird.
Da es seitens HashiCorp keine Rückgängigmachung gab und auch keine Absicht dazu mitgeteilt wurde, erfolgte nun ein Fork von Terraform, der OpenTofu genannt wird, ehemals bekannt als OpenTF15. Dieses Projekt steht nun unter der Schirmherrschaft der Linux Foundation16. Die ersten Unterstützer sind Unternehmen wie Gruntwork, Spacelift, Harness, Env0 und Scalar, die im Terraform-Ökosystem aktiv sind. Sie haben zugesagt, die Kosten für 18 Vollzeit-Entwickelnde für fünf Jahre zu tragen. Weitere Unternehmen und Einzelpersonen haben sich bereiterklärt, das Projekt ebenfalls zu unterstützen.
Gemäß der Ankündigung wird es zunächst keine Unterschiede zwischen Terraform und OpenTofu bis zur Version 1.5 von Terraform geben. Allerdings könnte sich dies im Laufe der Zeit ändern. Für die nachfolgenden Beispiele bleibt Terraform weiterhin unser Einsatztool.
In unserem Berateralltag sehen wir häufig Terraform-Code, der genutzt wird, um Infrastruktur zu provisionieren. Auf dieser Infrastruktur wird dann ein Kubernetes-Cluster aufgebaut, auf dem Argo CD oder Flux installiert wird. Terraform hat wesentlich dazu beigetragen, IaC in Unternehmen zu etablieren und die DevOps-Kultur insgesamt zu fördern.
Es liegt nahe, Terraform auch mit GitOps zu kombinieren, um alle Vorteile von GitOps auch in diesem Kontext nutzen zu können. Wie wir bereits gesehen haben, eignet sich die deklarative Sprache von Terraform (HCL) für die Versionierung in Git. Es gibt jedoch viele Herausforderungen, die ein potenzieller Terraform-Controller für Argo CD oder Flux bewältigen muss.
Herausforderungen von Terraform
HCL ist eine einfach zu erlernende Sprache, aber eine übermäßige Nutzung der Ressourcen-Dynamisierung kann die Komplexität erhöhen. Zu beachten ist, dass Terraform kein automatisches Rollback vorsieht. Im Fehlerfall ist der Benutzer dafür verantwortlich, den ursprünglichen Zustand wiederherzustellen – oder eben mit Git. Neben den definierten Ressourcen umfasst der Zustand auch die Metadaten, die nicht direkt durch die Terraform-Ressourcen definiert sind, sondern als Entitäten, Objekte oder Relationen im jeweiligen Terraform-Provider vorliegen. Dazu können auch die Reihenfolge und die Abhängigkeiten zwischen den Ressourcen gehören. Die entsprechende Logik ist im jeweiligen Terraform-Provider implementiert und wird im State entsprechend über Relationen abgebildet. Im Falle eines Rollbacks müssen die entsprechenden Abhängigkeiten ebenfalls aufgelöst werden.
Wie wir in unseren Beispielen gesehen haben, wird die State-Datei im Root-Verzeichnis des Terraform-Projekts gespeichert. Wenn man mit Terraform anfängt oder allein an einem Projekt arbeitet, mag das noch akzeptabel sein. Sobald man jedoch in einem Team zusammenarbeitet und Ressourcen definiert, kann es schwierig bis unmöglich werden, sicherzustellen, dass alle Beteiligten immer über den aktuellen Stand verfügen. Ansonsten wird von unterschiedlichen Realitäten ausgegangen.
TF-Controller für Flux
Für Flux wird der Weave GitOps Terraform Controller17 angeboten, der die Verwaltung von Terraform-Ressourcen nach GitOps-Prinzipien unterstützt. Ferner bietet es eine Vielzahl von Features wie Mandantenfähigkeit oder die Erkennung von State-Drifts. Dabei erzeugt und orchestriert der Controller dedizierte Runner-Pods, die dann entsprechend die Kommandos terraform plan oder terraform apply ausführen und die Infrastruktur aufbauen. Der Zustand wird als Secret im Cluster gespeichert und ist so für den Benutzer transparent.
Argo CD Terraform Controller
Für Argo CD gibt es den argocd-terraform-controller18. Die Entwicklung ist jedoch ins Stocken geraten und stellt derzeit noch keine ausgereifte Lösung zur Verwaltung von Terraform-Ressourcen dar. Zudem existiert noch ein Proposal für das Refactoring der Argo CD Controller analog zu Flux19.
Flux Subsystem for Argo
Alternativ kann Flux als Subsystem in Argo CD verwendet werden20. Das Flux Subsystem for Argo (FSA, auch bekannt als Flamingo) ist eine Lösung zur Integration von Flux in Argo CD und verbindet somit beide Welten innerhalb einer Argo CD Control Plane, ohne dass die gesamte GitOps-Installation auf ein neues Tool migriert werden muss. Es beinhaltet alle Funktionen von Argo CD und die zusätzlichen Vorteile und Funktionen von Flux wie beispielsweise die Möglichkeit, IaC mit Terraform zu verwalten.
Abb. 11–3
Flux-Subsystem in Argo CD
Als praktisches Beispiel wollen wir nun mithilfe des TF-Controllers und Flamingo eine Terraform-Ressource bereitstellen, nämlich eine EC2-Instanz wie im anfänglichen Beispiel in Abschnitt 11.1.1 auf Seite 292. Mittels Flamingo bleiben wir im Argo-CD-Ökosystem und nutzen zusätzlich die Funktionen von Flux, um Terraform beziehungsweise den TF-Controller nutzen zu können.
Wir werden dabei grob betrachtet folgende Schritte durchlaufen:
Am Ende werden die Terraform-Ressourcen, die wir in Schritt 3 erstellt haben, kontinuierlich auf GitOps-Art überwacht und ausgerollt.
Es wird davon ausgegangen, dass Argo CD bereits auf dem Cluster installiert ist. Für Flamingo muss die bestehende Argo CD-Installation erweitert werden. Dazu muss folgender Befehl ausgeführt werden:
Listing 11–9
Flamingo als Erweiterung für Argo CD installieren
1
export FSA_VERSION=v2.7.10-fl.15-main-688d2fd7
2
kustomize build https://github.com/flux-subsystem-argo/
3
flamingo/release?ref=${FSA_VERSION} \
4
| yq e '. | select(.kind == "Deployment" or
5
.kind=="StatefulSet")'
6
| kubectl -n argocd apply -f -
Nun installieren wir zusätzlich Flux, damit es als Subsystem fungieren kann – entweder über die CLI oder als Helm-Chart.
Listing 11–10
Flux installieren
1
curl -s https://fluxcd.io/install.sh | sudo bashyo
2
3
flux install
Listing 11–11
Helm-Chart als Alternative zur CLI-Installation
1
helm install -n flux-system flux oci://ghcr.io/
2
fluxcd-community/charts/flux2
In der Web-UI ändert sich durch die Installation von Flamingo das Look & Feel. Die Benutzerfreundlichkeit von Argo CD bleibt weiterhin bestehen.
Im nächsten Schritt installieren wir den TF-Controller für Flux. Dazu verwenden wir das Helm-Chart und generieren eine Argo CD Application. Dies kann entweder über die Web-UI oder durch Ausführen der folgenden YAML-Datei gestartet werden.
Abb. 11–4
Flamingo Argo CD
Listing 11–12
Argo CD Application für den TF-Controller Helm-Chart
1
apiVersion: argoproj.io/v1alpha1
2
kind: Application
3
metadata:
4
name: tf-controller
5
namespace: argocd
6
spec:
7
project: default
8
source:
9
repoURL: 'https://weaveworks.github.io/tf-controller'
10
targetRevision: 0.15.1
11
chart: tf-controller
12
destination:
13
server: 'https://kubernetes.default.svc'
14
namespace: flux-system
15
syncPolicy:
16
syncOptions:
17
- FluxSubsystem=true
18
- ApplyOutOfSyncOnly=true
19
- AutoCreateFluxResources=true
Damit wir das Flux-Subsystem direkt ansprechen und Flux-Ressourcen erstellen können, muss in der Konfiguration der Sync-Policy FluxSubsystem=true und AutoCreateFluxResources=true gesetzt sein. Nach der Ausführung haben wir eine lauffähige Installation:
Abb. 11–5
TF-Controller
Nun können wir unsere Terraform-Ressourcen bereitstellen. Wir erstellen ein neues Repository mit folgender Struktur:
Abb. 11–6
Anlegen einer Terraform-Ressource
Wir legen eine Datei terraform/main.tf an, um eine EC2-Instanz zu erzeugen:
Listing 11–13
Definition einer exemplarischen EC2-Ressource in Terraform
1
terraform {
2
required_providers {
3
aws = {
4
source = "hashicorp/aws"
5
version = "~> 4.16"
6
}
7
}
8
required_version = ">= 1.2.0"
9
}
10
11
provider "aws" {
12
region = "us-west-2"
13
}
14
resource "aws_instance" "app_server" {
16
ami = "ami-830c94e3"
17
instance_type = "t2.micro"
18
tags = {
19
Name = "EC2Instance"
20
}
21
}
Zusätzlich erzeugen wir Flux-Ressourcen. Wir benötigen die Ressource flux/git-repository.yaml, die den Pfad zu unserem Git-Repository enthält.
Listing 11–14
Das Git-Repository wird dem TF-Controller zur Verfügung gestellt
1
apiVersion: source.toolkit.fluxcd.io/v1
2
kind: GitRepository
3
metadata:
4
name: gitops-book-tf
5
spec:
6
interval: 30s
7
url: ssh://git@gitlab.com/gitops-book/
8
terraform-infrastructure.git
9
ref:
10
branch: main
11
secretRef:
12
name: ssh-credentials
Um auf das Repository per SSH zugreifen zu können, wird ein privater Schlüssel verwendet, der als Secret im Cluster gespeichert wird. Es ist zu beachten, dass Secrets im Git-Repository nichts zu suchen haben.
Listing 11–15
SSH-Private-Key werden für den Zugriff auf das Repository bereitgestellt
1
apiVersion: v1
2
kind: Secret
3
metadata:
4
name: ssh-credentials
5
type: Opaque
6
stringData:
7
identity: |
8
# Private Key
9
known_hosts: |
10
# Known Hosts
Die folgende Konfiguration muss erstellt werden, um den TF-Controller verwenden zu können:
Listing 11–16
Terraform-Ressource mit den AWS-Credentials als Referenz
1
apiVersion: infra.contrib.fluxcd.io/v1alpha1
2
kind: Terraform
3
metadata:
4
name: gitops-book-tf
5
spec:
6
path: ./terraform
7
approvePlan: auto
8
interval: 30s
9
sourceRef:
10
kind: GitRepository
11
name: gitops-book-tf
12
runnerPodTemplate:
13
spec:
14
envFrom:
15
- secretRef:
16
name: aws-credentials
Hier verweisen wir auf die Ressource GitRepository und geben an, in welchem Verzeichnis sich die Terraform-Ressourcen befinden. Dann definieren wir das Attribut approvePlan und weisen den Controller an, in welchem Modus er die Ressourcen verwalten soll. Wenn nichts angegeben wird, wird standardmäßig der plan-and-manually-apply-Modus verwendet. Der TF-Controller erzeugt einen Plan und speichert diesen in der GitRepository-Ressource. Der Befehl terraform apply muss anschließend manuell ausgeführt werden. In diesem Beispiel wollen wir den Prozess vollständig automatisieren und setzen den Wert auf auto. Der TF-Controller führt nach terraform plan den Befehl terraform apply aus. Mit dem Attribut interval definieren wir ein Zeitintervall, nach dem der Abgleich stattfindet. Damit Terraform auf AWS Ressourcen provisionieren kann, definiert AWS AWS_ACCESS_KEY_ID und AWS_SECRET_ACCESS_KEY als Secret und referenziert diese in der Ressource.
Listing 11–17
AWS-Access-Key und -Secret werden als Kubernetes-Secret für den TF-Controller bereitgestellt.
1
apiVersion: v1
2
kind: Secret
3
metadata:
4
name: aws-credentials
5
type: Opaque
stringData:
7
AWS_ACCESS_KEY_ID: # Access Key ID
8
AWS_SECRET_ACCESS_KEY: # Secret
9
AWS_REGION: us-west-2
Wir erstellen nun eine Argo CD Application. Dies kann über die Oberfläche oder als entsprechende Kubernetes-Ressource erfolgen. Für Letzteres verwenden wir die folgende YAML-Datei.
Listing 11–18
Eine Argo CD Application für das Flux-Subsystem wird erstellt.
1
apiVersion: argoproj.io/v1alpha1
2
kind: Application
3
metadata:
4
name: tf-application
5
namespace: argocd
6
spec:
7
project: default
8
source:
9
repoURL: 'ssh://git@gitlab.com/gitops-book
10
/terraform-infrastructure.git'
11
path: flux
12
targetRevision: main
13
destination:
14
server: 'https://kubernetes.default.svc'
15
namespace: dev-infra
16
syncPolicy:
17
syncOptions:
18
- FluxSubsystem=true
19
- CreateNamespace=true
20
- ApplyOutOfSyncOnly=true
21
- AutoCreateFluxResources=true
Hier geben wir wieder an, dass das Flux-Subsystem verwendet werden soll. Nach erfolgreicher Synchronisation werden alle Ressourcen erstellt.
Der TF-Controller erzeugt nun alle 30 Sekunden einen Runner-Pod und führt eine Reconciliation durch.
Abb. 11–7
Die Ressourcen des TF-Controllers
Listing 11–19
Durch die Intervalldefinition wird alle 30 Sekunden ein TF-Runner-Pod erzeugt
1
> kubectl get -n dev-infra pods
2
NAME READY STATUS RESTARTS AGE
3
gitops-book-tf-tf-runner 1/1 Running 0 15s
Wenn wir uns die Logs anschauen, sehen wir, dass nacheinander die drei Befehle terraform init, terraform plan und terraform apply ausgeführt werden.
Listing 11–20
Ausführung von Terraform innerhalb eines Runner-Pods
1
..
2
3
Initializing the backend...
4
5
Successfully configured the backend "kubernetes"!
6
Terraform will automatically
7
use this backend unless the backend configuration changes.
8
9
Initializing provider plugins...
10
- Finding hashicorp/aws versions matching "~> 4.16"...
11
- Installing hashicorp/aws v4.67.0...
12
- Installed hashicorp/aws v4.67.0 (signed by HashiCorp)
13
14
Terraform has been successfully initialized!
15
...
16
aws_instance.app_server: Creating...
aws_instance.app_server: Still creating... [10s elapsed]
18
aws_instance.app_server: Still creating... [20s elapsed]
19
aws_instance.app_server: Still creating... [30s elapsed]
20
aws_instance.app_server: Still creating... [40s elapsed]
21
aws_instance.app_server: Creation complete after 46s
22
23
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
24
..
Darüber hinaus wird nach terraform plan der erzeugte Plan als Secret gespeichert. Abschließend prüfen wir auf der AWS-Konsole, ob die Terraform-Ressource tatsächlich provisioniert wurde.
Abb. 11–8
Prüfen der Ressourcen in der AWS-Konsole
Flux kann auch als Subsystem genutzt werden, um beide Welten miteinander zu verschmelzen. Allerdings sollten wir berücksichtigen, dass wir viel zusätzliches Drumherum gebaut haben, um Terraform ausführen zu können. Crossplane bietet eine ernsthafte Alternative zu Terraform. Warum? Dies wird im nächsten Kapitel erklärt. Wir verlassen nun dieses Thema und widmen uns Pulumi.
Ähnlich wie Terraform ist Pulumi21 ein Tool, das bei der Erstellung, dem Deployment und dem Management von Cloud-Infrastruktur hilft. Der Unterschied zu Terraform besteht darin, dass Pulumi Programmiersprachen wie TypeScript, Java oder C# zur Beschreibung der Infrastruktur verwendet. Dadurch können alle Vorteile einer bestimmten Programmiersprache wie starke Typisierung, Variablen, Schleifen und Kontrollstrukturen genutzt werden, um komplexere Infrastrukturen und Abläufe zu definieren. IDEs unterstützen zudem eine Programmiersprache oft besser als eine Konfiguration mit HCL. Entwickelnde, die zuvor bereits mit unterstützten Programmiersprachen gearbeitet haben, finden einen einfacheren Einstieg.
Um Pulumi nutzen zu können, muss die Pulumi CLI installiert werden22. Ähnlich wie bei Terraform kann durch den Befehl pulumi new ein neues Projekt samt Stack erstellt werden. Mithilfe von pulumi preview und pulumi up kann zum einen ein Plan erstellt, zum anderen Ressourcen dauerhaft bereitgestellt werden.
Ein zusätzliches Merkmal ist die Einführung der Automation API, die es erlaubt, Pulumi-Ressourcen im Quellcode einer Anwendung zu instanzieren und zu nutzen23. Anstatt die Infrastruktur über die CLI zu steuern, kann sie jetzt über den jeweiligen Quellcode gesteuert werden.
Das Programmiermodell in Pulumi
Das Beispiel in Listing 11–21 verwendet JavaScript beziehungsweise TypeScript, um einen S3-Bucket auf AWS zu erstellen. Dieser stellt außerdem sicher, dass die Inhalte des Buckets serverseitig verschlüsselt werden, indem ein KMS-Key bereitgestellt wird.
Listing 11–21
TypeScript wird eingesetzt, um S3-Ressourcen zu erzeugen
1
import * as pulumi from "@pulumi/pulumi";
2
import * as aws from "@pulumi/aws";
3
4
const bucketKey = new aws.kms.Key("mykey", {
5
description: "Encrypt bucket",
6
deletionWindowInDays: 10,
7
});
8
const mybucket = new aws.s3.Bucket("mybucket", {
9
serverSideEncryptionConfiguration: {
10
rule: {
11
applyServerSideEncryptionByDefault: {
12
kmsMasterKeyId: bucketKey.arn,
13
sseAlgorithm: "aws:kms",
14
},
15
},
16
}
17
});
Grundsätzlich basiert das Programmiermodell eines Pulumi-Projekts darauf, dass jede Ressource sowohl Übergabe- als auch Rückgabewerte besitzt, die miteinander verknüpft werden können, um komplexe Strukturen zu bilden. In diesem konkreten Fall werden sowohl ein KMS als auch ein S3-Bucket instanziert. Für die Verschlüsselung wird jedoch der KMS-Schlüssel benötigt, wodurch eine direkte Abhängigkeit zu dieser Ressource entsteht. Die S3-Bucket-Ressource verarbeitet den Rückgabewert des KMS-Schlüssels, in diesem Fall die ARN.
Deployment von Pulumi-Ressourcen
Obwohl die Definition von Pulumi-Ressourcen imperativ ist, arbeitet der Pulumi-Kern mit Zuständen. Zur Laufzeit prüft Pulumi, ob der aktuelle Zustand der Infrastruktur dem gewünschten Zustand entspricht. Falls Abweichungen festgestellt werden, werden die entsprechenden Ressourcen angelegt, verändert oder gelöscht. Eine Visualisierung der Komponenten von Pulumi findet sich in Abb. 11–9.
Abb. 11–9
Deployment von Pulumi-Ressourcen
Der gewünschte Zustand der Ressourcen wird bei der Ausführung des Pulumi-Codes durch den Language-Host ermittelt. Dabei erfolgt keine automatische Provisionierung der Ressourcen, sondern die Deployment Engine prüft, ob der gewünschte Zustand mit dem aktuellen Stand übereinstimmt. Im Falle einer Änderung wird die Ressource dauerhaft mit dem entsprechenden Resource-Provider geändert. Die Komponenten arbeiten ausschließlich asynchron, um Abhängigkeiten mit den passenden Werten aufzulösen. Für die Referenzierung der ARN im S3-Bucket muss im Falle eines KMS-Keys diese Ressource zuvor provisioniert werden.
Wie bei Terraform oder Crossplane werden die bekannten Hyperscaler wie AWS, Azure oder GCP unterstützt. Darüber hinaus können passende Provider für weitere Plattformen wie Alibaba, OpenStack, DigitalOcean und vSphere über die Pulumi Registry gefunden und eingesetzt werden24.
Das Backend für den State kann lokal, auf einem Blob-Speicher wie AWS S3 oder auf der Pulumi Cloud genutzt werden.
Pulumi-Stacks
In einem Pulumi-Projekt können mehrere Stacks definiert werden25. Die Stacks eignen sich dazu, die Infrastrukturumgebung in verschiedene Bereiche zu unterteilen oder auch für Feature-Branches zu nutzen. Jeder Stack hat seinen eigenen State.
Abb. 11–10
Aufbau eines Pulumi-Projekts
Wie im Schaubild 11–10 dargestellt, können Stacks neben dem Quellcode erstellt werden, um umgebungsspezifische Anpassungen vorzunehmen.
Neben den bereits erwähnten Programmiersprachen ermöglicht Pulumi auch das Deployment mittels YAML26. Das Beispiel aus Listing 11–21 kann in YAML wie folgt umgesetzt werden:
Listing 11–22
YAML wird eingesetzt, um S3-Ressourcen zu erzeugen
1
resources:
2
bucketKey:
3
type: aws:kms:Key
4
properties:
5
description: "EncryptBucket"
6
deletionWindowInDays: 10
7
mybucket:
8
type: aws:s3:Bucket
9
properties:
10
serverSideEncryptionConfiguration:
11
rule:
12
applyServerSideEncryptionByDefault:
13
kmsMasterKeyId: ${bucketKey.arn}
14
sseAlgorithm: aws:kms
Übergabeparameter können mit ${} definiert werden. Deployt wird ebenfalls mit pulumi up. Zusätzlich können Funktionen zur Filterung und zur Konvertierung von String-Werten genutzt werden27. Man sollte beachten, dass der Funktionsumfang einer YAML-Definition im Vergleich zu einer Programmiersprache deutlich eingeschränkt ist. Für einfache Infrastrukturen eignet sich YAML sehr gut. Sobald die Komplexität steigt, kann es unter Umständen notwendig sein, eine Programmiersprache einzusetzen.
Im Gegensatz zu Terraform und Crossplane bietet Pulumi einen eigenen GitOps-Operator als Erweiterung an28. Mit dem Pulumi Kubernetes Operator kann ein Stack als Kubernetes-Ressource bereitgestellt werden. Die CRD Stack enthält Parameter wie das Git-Repository, Secrets und Umgebungsvariablen29. Der Stack-Controller verwaltet diese Stacks und führt entsprechend die Deployments mit dem bekannten Kommando pulumi up durch. Ein Stack kann außerdem einen bestimmten Commit-SHA oder einen Verweis auf einen zu verfolgenden Branch oder Tag beinhalten. Wenn ein Branch als Parameter angegeben ist, wird der aktuelle Stand periodisch abgefragt und automatisch deployt.
Im folgenden Abschnitt werden wir sowohl den Pulumi Kubernetes Operator als auch einen Stack bereitstellen. Wie im vorherigen Beispiel beabsichtigen wir, eine AWS-EC2-Instanz zu erstellen. Dafür führen wir folgende Schritte durch:
Am Schluss wird der Pulumi-Stack von Argo CD überwacht und kontinuierlich angewandt.
Wir erstellen eine Argo CD Application, um eine konsistente Automatisierung sowohl des Pulumi Kubernetes Operators als auch der Stacks zu gewährleisten. Zur Umsetzung verwenden wir das bewährte App of Apps-Muster. Zusätzlich erzeugen wir ein neues Repository mit folgender Struktur:
Abb. 11–11
Ein neuer Ordner im Repository für den Pulumi Kubernetes Operator
Pulumi Kubernetes Operator Helm-Chart
Entgegen der offiziellen Anleitung30 bietet Pulumi ein Helm-Chart an, das als OCI-Image verfügbar ist31. Verwenden können wir dieses Helm-Chart jedoch nicht direkt als Repository. Stattdessen müssen wir in Argo CD ein Repository definieren, das mit der Argo CD CLI erstellt werden muss. Dazu verwenden wir den folgenden Befehl:
Listing 11–23
Wir erstellen ein Argo CD Repository mit der CLI für das OCI Image.
1
argocd repo add ghcr.io/pulumi/helm-charts \
2
--type helm
3
--name pulumi-helm-charts
4
--enable-oci
Der Parameter --enable-oci ist unbedingt erforderlich, damit Argo CD dieses Helm-Chart als Repository nutzen kann. Das Kommando erzeugt automatisch ein Secret, das im Argo CD Namespace erstellt wird.
Listing 11–24
Das Argo CD Repository erstellt ein neues Secret
1
apiVersion: v1
2
kind: Secret
3
metadata:
4
labels:
5
argocd.argoproj.io/secret-type: repository
6
name: pulumi-helm-charts
7
namespace: argocd
8
stringData:
9
enableOCI: "true"
10
name: pulumi-helm-charts
11
type: helm
12
url: ghcr.io/pulumi/helm-charts
Da Secrets letztendlich Deklarationen sind, können wir diese Ressource als eigene Datei zu unserem Repository hinzufügen. Durch die Automatisierung dieses Schritts beim Wiederaufsetzen des Operators sparen wir Zeit.
Nun können wir unsere Anwendung erstellen, indem wir die folgende Application-Ressource definieren:
Listing 11–25
Definition einer Application für den Pulumi Kubernetes Operator
1
apiVersion: argoproj.io/v1alpha1
2
kind: Application
3
metadata:
4
name: pulumi-kubernetes-operator
5
namespace: argocd
finalizers:
7
- resources-finalizer.argocd.argoproj.io
8
spec:
9
project: default
10
source:
11
repoURL: ghcr.io/pulumi/helm-charts
12
targetRevision: 0.3.0
13
chart: pulumi-kubernetes-operator
14
destination:
15
server: 'https://kubernetes.default.svc'
16
namespace: pulumi-operator
17
syncPolicy:
18
automated:
19
selfHeal: true
20
syncOptions:
21
- CreateNamespace=true
22
- ApplyOutOfSyncOnly=true
In Abb. 11–12 sehen wir, dass alle Ressourcen für den Pulumi Operator angelegt wurden.
Abb. 11–12
Zudem wird der Operator ebenfalls sauber angelegt.
Nach der Synchronisation durch Argo CD können wir beginnen, unser Manifest zu bauen und es als Stack zu deployen.
Nun möchten wir unsere EC2-Instanz tatsächlich erstellen. Dazu legen wir ein neues Verzeichnis an und initialisieren unseren Workspace mit dem Befehl pulumi new an32.
Listing 11–26
Wir werden durch einen Wizard geführt, wo wir die Grundeinstellungen defineren.
> pulumi new aws-yaml
2
This command will walk you through creating a new
3
Pulumi project.
4
5
Enter a value or leave blank to accept the (default),
6
and press <ENTER>.
7
Press ^C at any time to quit.
8
9
project name: gitops-book-sample-project
10
project description (A minimal AWS Pulumi YAML program):
11
"A simple EC2 instance provision"
12
Created project 'gitops-book-sample-project'
13
14
stack name (dev):
15
Created stack 'dev'
16
Enter your passphrase to protect config/secrets:
17
Re-enter your passphrase to confirm:
18
19
aws:region: The AWS region to deploy into (us-east-1):
20
Saved config
21
22
Your new project is ready to go!
23
24
To perform an initial deployment, run 'pulumi up'
In diesem Ordner werden die folgenden zwei Dateien erstellt:
Abb. 11–13
Zwei neue Dateien hat Pulumi angelegt. Nun können wir die YAML-Ressourcen erzeugen.
Dabei enthält Pulumi.dev.yaml zusätzliche Konfigurationen für den Stack. Der eigentliche Pulumi-Quellcode befindet sich in Pulumi.yaml. Dem Skeleton fügen wir die benötigte EC2-Ressource hinzu.
Listing 11–27
Eine EC2-Instanz wird als Pulumi-Ressource angelegt. Eingesetzt wird YAML.
1
name: gitops-book-sample-project
2
runtime: yaml
3
description: A simple EC2 instance provision
4
outputs:
5
ec2-instance-id: ${my-ec2-instance.id}
6
resources:
7
name: gitops-book-sample-project
runtime: yaml
9
description: A simple EC2 instance provision
10
outputs:
11
ec2-instance-id: ${my-ec2-instance.id}
12
resources:
13
my-ec2-instance:
14
type: aws:ec2:Instance
15
properties:
16
ami: ami-067d1e60475437da2
17
instanceType: t3.micro
18
tags:
19
Name: NewInstance
Wir speichern dieses Manifest im Git-Repository. In nächsten Schritt müssen wir dem Pulumi Operator dieses Manifest als Stack bereitstellen, um mittels Reconciliation die Provisionierung der Ressourcen zu initiieren.
Wir haben nun unseren Operator deployt und unser Manifest angelegt. Anschließend erstellen wir einen Stack, indem wir eine Definition für diese Ressource erstellen, die das Git-Repository und alle notwendigen Secrets enthält. Mit diesem Stack kann die Infrastruktur durch Pulumi provisioniert werden und er ermöglicht den Zugriff zur Überwachung und Aktualisierung des aktuellen Zustandes. Wir trennen die Konfiguration sowie den Quellcode voneinander und erzeugen eine neue Ressource im Verzeichnis ./stack an:
Listing 11–28
Wir definieren einen Stack als Kubernetes-Ressource.
1
apiVersion: pulumi.com/v1
2
kind: Stack
3
metadata:
4
name: gitops-book-sample-project
5
namespace: pulumi-operator
6
spec:
7
backend: "s3://gitops-book-pulumi-backend"
8
envRefs:
9
AWS_DEFAULT_REGION:
10
type: Secret
11
secret:
12
.. secret
AWS_ACCESS_KEY_ID:
14
type: Secret
15
secret:
16
.. secret
17
AWS_SECRET_ACCESS_KEY:
18
type: Secret
19
secret:
20
.. secret
21
stack: "gitops-book-sample-project"
22
projectRepo:
23
git@gitlab.com:gitops-book/
24
pulumi-infrastructure.git
25
repoDir: infra
26
branch: master
27
gitAuth:
28
sshAuth:
29
sshPrivateKey:
30
type: Secret
31
secret:
32
... secret
Durch die Spezifikation von spec.backend wird festgelegt, auf welchen AWS-S3-Bucket Pulumi zugreifen soll. Die Variablen AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID und AWS_SECRET_ACCESS_KEY verweisen auf ein Secret, das diese Daten enthält, um Pulumi bei der Erstellung der benötigten Ressourcen zu unterstützen. Um auf den versionierten Quellcode zugreifen zu können, geben wir spec.projectRepo und spec.gitAuth an, damit git clone über SSH funktioniert.
Mit der kustomization.yaml führen wir eine Zusammenführung und Bereitstellung der benötigten Ressourcen durch. Dadurch sind alle notwendigen Ressourcen in einer Einheit vereint.
Listing 11–29
Zur Zusammenführung der Ressourcen nutzen wir eine kustomization.yaml.
1
apiVersion: kustomize.config.k8s.io/v1beta1
2
kind: Kustomization
3
resources:
4
- pulumi-stack.yaml
Für die Application-Ressource erstellen wir die folgenden Ressourcen:
Listing 11–30
Wir erstellen eine neue Argo CD-Application für unsere Ressourcen.
1
apiVersion: argoproj.io/v1alpha1
2
kind: Application
3
project: default
4
spec:
5
source: repoURL: 'git@gitlab.com:gitops-book/
6
pulumi-infrastructure.git'
7
path: stack
8
targetRevision: HEAD
9
destination:
10
server: 'https://kubernetes.default.svc'
11
namespace: pulumi-operator
12
syncPolicy:
13
automated:
14
selfHeal: true
15
syncOptions:
16
- CreateNamespace=true
Argo CD synchronisiert und erstellt die benötigten Stacks gemäß Abb. 11–14.
Abb. 11–14
Pulumi-Stack wurde deployt
In der AWS-Konsole in Abb. 11–15 sehen wir eine EC2-Ressource, die mit Pulumi bereitgestellt wurde:
Abb. 11–15
Pulumi war erfolgreich und der gewünschte Zustand wurde provisioniert.
Im nächsten Abschnitt werden wir untersuchen, wie Crossplane-Ressourcen nach den GitOps-Prinzipien verwaltet werden können.
Wie bereits beschrieben, ist Crossplane ein plattformagnostisches IaC-Tool, das auf Kubernetes aufsetzt, aber auch eine Nicht-Kubernetes-Umgebung bereitstellen kann. Das Werkzeug befindet sich seit Ende September 2021 im Inkubationsstatus der CNCF. Im Gegensatz zu Terraform setzt Crossplane ausschließlich auf die Kubernetes-API und alle Infrastrukturmanifeste sind Kubernetes-Ressourcen. Analog zur Cluster API kann Crossplane auch Infrastruktur bereitstellen, die nicht auf Kubernetes basiert. Dazu werden eigene CRDs verwendet, die die Kubernetes-API um weitere Ressourcen erweitern. Solche Ressourcen können ein S3-Bucket, einen EKS-Cluster oder ein Subnetz sein. Um Multi-Cloud-Szenarien abdecken zu können, unterstützt Crossplane neben AWS, Azure und Google eine breite Palette an Providern und Plattformen, darunter IONOS, Digital Ocean, IBM Cloud, Kubernetes generell und VMware vSphere.
Der kleinste Baustein in Crossplane sind die Managed Resources (MRs), die von den Providern gebündelt werden und für die eigentliche Provisionierung zuständig sind. Diese MRs repräsentieren einzelne Komponenten der Cloud-Anbieter wie Compute-Instanzen, Storage-Container oder Netzwerkressourcen.
Crossplane CRDs
Als zentrale Anlaufstelle für die Suche nach solchen Komponenten bietet sich die Seite CRDs.dev33 an, über die alle CRDs in allen von Crossplane angebotenen Versionen durchsucht werden können. Die API-Dokumentation allein reicht nicht unbedingt aus, um das richtige Verständnis zu erlangen. Hierfür bieten die Anbieter Anleitungen an, in die sich ein Blick lohnen kann. So enthält der Crossplane Azure Provider einen Unterordner mit Beispielen für Datenbanken, Netzwerkkonfiguration und Storage. Ähnliche Beispiele bietet der AWS-Provider34.
XRs und XRDs
In Form von Composite Resources (XR) wird den Entwickelnden ein Werkzeug an die Hand gegeben, mit dem MRs nach dem Baukastenprinzip zu höherwertigen Komponenten, genauer Compositions, zusammengesetzt werden können. Eine Composite Resource Definition (XRD) legt die Struktur der Composition fest. Dazu muss die XRD ein OpenAPI-Schema enthalten und die Parameter definieren, welche die Composition für die Bereitstellung der einzelnen Komponenten benötigt. Dieses Schema ähnelt dem Variablenblock eines Modells in Terraform.
XRCs
Composite Resource Claims (XRC) ermöglichen es Entwickelnden, diese höherwertigen Dienste auf einfache Weise in Anspruch zu nehmen, ohne mit der zugrunde liegenden Komplexität in Berührung zu kommen. Der Unterschied zu XRs liegt in der Benutzergruppe, für die das jeweilige Modul bestimmt ist. XRs sollten von Infrastrukturoder Plattformteams verwendet werden, um zu definieren, welche Composition mit welchen konkreten Parametern tatsächlich bereitgestellt werden soll. Im Gegensatz dazu sind Claims für Anwendungsentwicklungsteams gedacht und tun dasselbe. So werden Claims mit einem Namespace versehen, während XRs clusterweit gültig sind. Diese Trennung der Kontexte ermöglicht letztlich eine effiziente Zusammenarbeit von Infrastruktur- und Anwendungsentwicklungsteams, da sie die gleichen Ressourcen konsumieren können. Auf diese Weise unterstützt Crossplane einen effektiven Self-Service-Ansatz für Entwickelnde. In Abb. 11–16 wird dieses Schema dargestellt.
Abb. 11–16
XRs, XRCs und Claims auf einen Blick
An dieser Stelle werden wir ebenfalls wie im vorigen Abschnitt praktisch zeigen, wie wir eine EC2-Instanz mit Argo CD verwalten können – in diesem Fall mit Crossplane statt dem TF-Controller. Insgesamt gehen wir grob betrachtet folgende Schritte durch:
Anschließend wird eine Instanz von Argo CD überwacht und kontinuierlich ausgerollt.
Um Crossplane nutzen zu können, muss ein Management-Cluster erstellt und Crossplane als Helm-Chart installiert werden. Zusätzlich muss der AWS-Provider installiert werden, um Cloud-Ressourcen provisionieren zu können. Diese Voraussetzung kann mit Managed Services wie EKS oder auch mit einem einfachen lokalen Kubernetes-Cluster wie Minikube oder KinD erfüllt werden.
Das Ziel dieses Abschnitts ist es, die entsprechenden Manifeste sowohl für das Deployment von Crossplane als auch für die Konfiguration des AWS-Providers zu definieren, sodass wir alles mit Argo CD verwalten können. Dazu verwenden wir das sogenannte App of Apps-Pattern39. Einzelne Application-Ressourcen, wie die Installation eines Deployments oder einer Abhängigkeit, können logisch als eine Application zusammengefasst werden. Bei der Synchronisation werden alle untergeordneten Applications automatisch erstellt, synchronisiert und auch gelöscht, wenn es eine Änderung in den Manifesten gibt. Entsprechend bilden Crossplane und der zugehörige AWS-Provider eine Einheit. Die zugehörigen Manifeste sind im Beispielprojekt in Git abgelegt40. Die Ordnerstruktur mit allen Manifesten für den Management-Cluster sieht wie folgt aus:
Abb. 11–17
Ordnerstruktur für das Deployment der Control Plane
In unserem Fall erstellen wir eine Application-Ressource, definieren das Crossplane Helm-Chart und speichern es als Datei crossplane-argocd-app.yaml. Dies kann durch kubectl apply -f crossplane-argocd-app.yaml angelegt werden.
Listing 11–31
Argo CD-Ressource mit dem Crossplane Helm-Chart
1
apiVersion: argoproj.io/v1alpha1
2
kind: Application
3
metadata:
4
name: crossplane
5
namespace: argocd
6
spec:
7
project: default
8
source:
9
repoURL: 'https://charts.crossplane.io/stable'
10
targetRevision: 1.13.2
11
chart: crossplane
destination:
13
server: 'https://kubernetes.default.svc'
14
namespace: default
15
syncPolicy:
16
automated: {}
Die Installation des Providers erfolgt in vier Schritten:
Ein AWS-Access-Key besteht aus der aws_access_key_id und dem aws_secret_access_key und können diesen über die CLI erzeugen42. Für das Secret benötigen wir eine Textdatei mit den beiden Key-Values und erstellen unsere Secret-Ressource.
Listing 11–32
AWS-Access-Token wird als Secret für Crossplane bereitgestellt.
1
apiVersion: v1
2
kind: Secret
3
metadata:
4
name: aws-secret
5
namespace: crossplane-system
6
data:
7
credentials: [base64 value]
Wir speichern diese Ressource als Datei mit dem Namen aws-provider-secret.yaml. Die Zugangsdaten sind lediglich Base64-codiert und sollten keinesfalls in das Git-Repository hochgeladen werden. Eine Beschreibung, wie wir Secrets verschlüsseln können, befindet sich in Kapitel 5.
Nun installieren wir den AWS-Provider. Eine Liste mit allen Providern ist im Upbound Marketplace43 zu finden. Dazu definieren wir folgendes Manifest:
Listing 11–33
AWS-Provider-Definition für Crossplane
1
apiVersion: pkg.crossplane.io/v1
2
kind: Provider
3
metadata:
4
name: provider-aws
5
spec:
6
package: xpkg.upbound.io/upbound/provider-aws:v0.40.0
Auf diese Weise werden jedoch alle CRDs von allen unterstützten AWS Managed Services installiert, was das System unnötig aufbläht. Seit Juni 2023 gibt es Provider Families als Gruppe für Provider mit eingeschränktem Kontext44. Anstelle von provider-aws können jetzt spezifische Provider wie AWS Relational Database Service (RDS) oder AWS Elastic Load Balancer (ELB) verwendet werden. Wir nutzen ebenfalls diese Unterteilung und passen unser Manifest wie folgt an:
Listing 11–34
AWS ELB-Provider für Crossplane
1
apiVersion: pkg.crossplane.io/v1
2
kind: Provider
3
metadata:
4
name: provider-aws-elb
5
spec:
6
package: xpkg.upbound.io/upbound/provider-aws-elb:v0.40.0
Wenn ein zweiter Service genutzt werden soll, kann ein zweiter Provider definiert werden.
Listing 11–35
Module in Terraform
1
apiVersion: pkg.crossplane.io/v1
2
kind: Provider
3
metadata:
4
name: provider-aws-rds
5
spec:
6
package: xpkg.upbound.io/upbound/provider-aws-rds:v0.40.0
Bei der Installation eines Providers wird die dazugehörige Provider-Family ebenfalls installiert, die sich gegenüber AWS authentifiziert und schließlich die Provisionierung initiiert:
Listing 11–36
Provider-Family auf einen Blick
1
> kubectl get providers
2
NAME INSTALLED HEALTHY PACKAGE AGE
3
provider-aws-elb True True
4
xpkg.upbound.io/upbound/provider-aws-elb:v0.40.0 26m
5
provider-aws-rds True True
6
xpkg.upbound.io/upbound/provider-aws-rds:v0.40.0 26m
7
upbound-provider-family-aws True True
8
xpkg.upbound.io/upbound/provider-family-aws:v0.40.0 26m
Es ist sinnvoll, nur die tatsächlich benötigten Dienste wie AWS RDS, S3 oder ELB als Provider zu installieren.
Nun muss eine ProviderConfig-Ressource definiert werden, um auf das Secret verweisen zu können. Dazu wird die folgende Ressource erstellt:
Listing 11–37
ProviderConfig zur Referenzierung der AWS-Credentials
1
apiVersion: aws.upbound.io/v1beta1
2
kind: ProviderConfig
3
metadata:
4
name: crossplane-system
5
spec:
6
credentials:
7
source: Secret
8
secretRef:
9
namespace: crossplane-system
10
name: aws-secret
11
key: credentials
Mit diesen Ressourcen lässt sich die Crossplane Control Plane als Einheit durch Argo CD deployen. Zusätzlich muss mit Kustomize eine kustomization.yaml erstellt werden, die die Ressourcen zusammenfasst:
Listing 11–38
Kustomization zur Anlage als Argo CD Application
1
apiVersion: kustomize.config.k8s.io/v1beta1
2
kind: Kustomization
3
resources:
4
- crossplane-argocd-app.yaml
5
- aws-rds-provider.yaml
6
- aws-elb-provider.yaml
7
- provider-config.yaml
Diese Ressourcen müssen nun in Git versioniert werden, um später für Argo CD eine Anwendung zu erstellen. Die Application können wir entweder über die Kommandozeile oder mit der Anlage der Application-Ressource wie in Listing 11–39 erstellen.
Listing 11–39
Application-Ressource für die Kustomization
1
apiVersion: argoproj.io/v1alpha1
2
kind: Application
3
metadata:
4
name: crossplane
5
namespace: argocd
6
finalizers:
7
- resources-finalizer.argocd.argoproj.io
spec:
9
project: default
10
source:
11
repoURL: 'git@gitlab.com:gitops-book/
12
crossplane-infrastructure.git'
13
path: management
14
destination:
15
server: 'https://kubernetes.default.svc'
16
namespace: crossplane-system
17
syncPolicy:
18
automated:
19
prune: true
In Abb. 11–18 sehen wir, dass alle definierten Ressourcen synchronisiert werden:
Abb. 11–18
Crossplane Application ist gesund
Zusätzlich sehen wir im Event-Status (siehe Abb. 11–19), dass der EC2-Provider erfolgreich installiert wurde.
Wir können auf diese Weise die Control Plane sehr simpel anpassen oder weitere Provider hinzufügen. Die Synchronisation übernimmt ausschließlich Argo CD.
Nach der Einrichtung der Control Plane ist es nun an der Zeit, unsere EC2-Instanz zu provisionieren. Für die Ressourcen erstellen wir ein neues Verzeichnis ./base als Wurzelelement.
Abb. 11–19
Status der Crossplane EC2-Provider
Abb. 11–20
Orderstruktur zum Anlegen der Crossplane-Ressourcen
Im nächsten Schritt erstellen wir eine Crossplane-Instance-Ressource für unsere EC2-Instanz und speichern diese als YAML-Datei in diesem neuen Verzeichnis ab:
Listing 11–40
Ausgestaltung der EC2-Instanz-Ressource
1
apiVersion: ec2.aws.upbound.io/v1beta1
2
kind: Instance
3
metadata:
4
name: gitops-book-sample-instance
5
spec:
6
forProvider:
7
ami: ami-05af0694d2e8e6df3
8
instanceType: t2.micro
9
region: us-west-1
10
tags:
11
type: ExampleTag
12
providerConfigRef:
13
name: crossplane-system
Anhand des kind-Elements bestimmen wir, welche Instanz wir bereitstellen möchten. Wir geben den Instanztyp, die Region und das Amazon-Image als AMI entsprechend unserem Terraform-Beispiel an und definieren zusätzlich einen Tag. Weitere Konfigurationsoptionen können wir aus der offiziellen Dokumentation entnehmen45. Um Ressourcen durch Crossplane auf AWS bereitzustellen, muss eine Referenz auf die ProviderConfig-Ressource definiert werden. Anschließend legen wir eine kustomization.yaml an, die diese Instance- Ressource referenziert.
Listing 11–41
Kustomize zur Nutzung im base-Verzeichnis
1
apiVersion: kustomize.config.k8s.io/v1beta1
2
kind: Kustomization
3
resources:
4
- application-set.yaml
Wir übertragen unsere Änderungen in das Git-Repository, damit wir diese Ressource später mit Argo CD synchronisieren können.
Für die Synchronisation benötigen wir eine Application-Ressource. Diese werden wie folgt in Argo CD angelegt:
Listing 11–42
Kustomize zur Nutzung im base-Verzeichnis
1
apiVersion: argoproj.io/v1alpha1
2
kind: Application
3
metadata:
4
name: crossplane-base
5
namespace: argocd
6
spec:
7
project: default
8
source:
9
repoURL: 'git@gitlab.com:gitops-book/
10
crossplane-infrastructure.git'
11
path: base
12
targetRevision: HEAD
13
destination:
14
server: 'https://kubernetes.default.svc'
15
syncPolicy:
16
syncOptions:
17
- CreateNamespace=true
18
- ApplyOutOfSyncOnly=true
Abb. 11–21
Status der Synchronisation in Argo CD
Abb. 11–22
Status der Synchronisation in Argo CD
Wie in Abb. 11–21 und Abb. 11–22 dargestellt, synchronisiert und erstellt Argo CD die definierte Instanz in AWS.
Sobald wir diese Ressource aus der Synchronisation von Argo CD entfernen, werden die Ressourcen gelöscht – und somit auch die EC2-Instanz auf AWS.