Nach dem vorherigen Kapitel haben wir eine konkrete Vorstellung davon, welchen Unterschied GitOps im Entwicklungsalltag macht. In diesem Kapitel wollen wir erste praktische Schritte in Richtung GitOps unternehmen. Am Ende dieses Kapitels werden wir einen lokalen Kubernetes-Cluster haben, in dem ein GitOps-Operator läuft, der eine Beispielanwendung kontinuierlich aus einem Config-Repo deployt.
Bevor wir in den Code und die Praxis eintauchen, möchten wir aber zuerst ein paar Werkzeuge an die Hand geben, die für jede Situation nützlich sind, in der man GitOps implementieren will: eine grundsätzliche Empfehlung und eine Orientierungshilfe.
Die vier Prinzipien können einschüchternd wirken, weil sie absolute Standards setzen, die auf den ersten Blick keinen Spielraum lassen. Glücklicherweise ist GitOps aber nicht zuallererst ein himmelhohes Ideal, das kein gewöhnlicher Mensch erreichen kann. Stattdessen reden wir hier von einer Reise und einem schönen Reifeprozess. Sehr empfehlenswert zu diesem Thema ist auch der Vortrag »GitOps as a Journey« von Dan Garfield (Codefresh), Scott Rigby (Weaveworks) und Chris Short (AWS) auf der GitOpsCon 20221.
Wir empfehlen deshalb grundsätzlich ein agiles Vorgehen: Versuche nicht, sequenziell Prinzip für Prinzip lückenlos zu erfüllen. Ziele stattdessen auf einen Durchstich ab, quasi ein Minimum Viable Product (MVP), mit dem du alle vier Prinzipien minimal erfüllst. Danach bringe iterativ immer mehr Ressourcen deines Systems unter GitOps-Kontrolle.
In aller Regel hat man nämlich bereits deklarative Manifeste, die man nicht erst noch erzeugen muss. Das sind beispielsweise Kubernetes-Manifeste, Docker-Compose-Manifeste oder Terraform-Dateien. Damit ist Prinzip 1 grundlegend abgedeckt.
Git ist sehr weit verbreitet als Versionierungstechnologie. Wenn man seine Manifeste in Git speichert, hat man Prinzip 2 bereits zu einem großen Teil erfüllt.
Den GitOps-Operator installieren
Was GitOps am meisten von anderen Deployment- und Betriebsansätzen differenziert, sind Prinzip 3 und 4: der Operator im Zielsystem, der Deklarationen kontinuierlich überwacht und anwendet. An diesem Punkt ist die erste praktische Änderung nötig. Im Kubernetes-Umfeld haben wir mit Flux CD2 und Argo CD 3 zwei reife und etablierte Alternativen an GitOps-Tools. Wir verwenden in diesem Kapitel Argo CD als Startpunkt, weil die eingebaute UI sich gut zur Visualisierung eignet und weil Argo CD meist mehr Anklang bei Entwickelnden findet, während Flux bei Plattformbetreibern beliebter ist.
In Kapitel 4 auf Seite 69 gehen wir tiefer auf die Unterschiede zwischen Argo CD und Flux ein und welches Tool sich für welche Anforderungen besser eignet.
Config-Repos mit dem GitOps-Operator verknüpfen
Nach der Installation muss Argo CD so konfiguriert werden, dass es die Manifeste im Git-Repo lesen kann. Dafür müssen wir wiederum ein Manifest erstellen, das dieses Git-Repo referenziert. Bei Argo CD nennt man solche Referenzen Applications.
Analog zu den Applications bei Argo CD gibt es bei Flux verschiedene Ressourcentypen wie Kustomizations und HelmReleases.
Sobald wir dieses Referenzmanifest deployt haben, werden die vorhandenen Manifeste von Argo CD überwacht und in GitOps-Manier kontinuierlich angewandt. Ab hier sind auch alle vier Prinzipien erstmals grundlegend erfüllt.Vollständig erfüllt sind sie noch nicht, denn folgende Kriterien erfüllt unser Aufbau an diesem Punkt meistens noch nicht:
Prinzip 1 sagt nichts aus über den Umfang des Systems.
Wenn wir von Vollständigkeit reden, sollten wir noch einmal im Detail Prinzip 1 untersuchen: Dieses Prinzip besagt, dass der Zustand des von GitOps verwalteten Systems deklarativ beschrieben sein muss. Es besagt nicht, dass die Gesamtheit des Systems, innerhalb dessen ein GitOps-Operator läuft, vollständig deklarativ beschrieben sein muss.
Es ist mit Sicherheit gut, wenn so viele Komponenten unseres Softwaresystems wie möglich von GitOps verwaltet werden, und unserer Erfahrung nach entwickelt GitOps schnell eine Sogwirkung, sodass Teams von alleine versuchen, immer mehr Teile des Systems unter GitOps-Kontrolle zu bringen. Dazu kann beispielsweise die zugrunde liegende Infrastruktur gehören (siehe Kapitel 11 auf Seite 291).
Aber Prinzip 1 legt umgekehrt fest, dass es dann erfüllt ist, wenn das System, das von GitOps verwaltet wird, deklarativ beschrieben wird. Nicht alle Bestandteile eines Systems werden wir immer deklarativ ausdrücken können oder wollen. In Kapitel 9 auf Seite 247 betrachten wir Entitäten und Aktionen, die wir möglicherweise bewusst außerhalb der GitOps-Überwachung behandeln möchten.
Als weiteres Hilfsmittel vor der praktischen Umsetzung wollen wir eine Orientierungshilfe geben, mit der man GitOps-Implementierungen einschätzen und zueinander in Bezug setzen kann.
Zum Thema GitOps finden sich Unmengen an Tutorials, Blogposts, Konferenzbeiträgen und dergleichen, die sehr detailliert eine konkrete Implementierung von GitOps vorstellen. Viele davon sind wertvoll für die Umsetzung, auch wenn manche sich leider auf veraltete Definitionen von GitOps stützen und damit den Begriff »GitOps« verwischen. Ihnen allen ist aber gemein, dass sie denjenigen nicht wirklich helfen, die GitOps und seine Prinzipien zuerst noch grundlegend kennenlernen müssen, bevor sie anfangen können mit einer Implementierung.
Mit den folgenden Fragen wollen wir Verwirrung verringern und dabei helfen, GitOps-Lösungen einzusortieren, damit wir – um das geläufige Sprichwort positiv umzuformulieren – wieder »den Wald trotz lauter Bäumen« sehen können. Sie beschränken sich auf die für GitOps relevanteren Aspekte und lassen damit bewusst manche Dimensionen aus, die für die ganzheitliche Beurteilung eines Softwaresystems relevant sein können.
In Kapitel 2 auf Seite 25 haben wir ein beispielhaftes Szenario mit mehreren Anwendungen und Clustern ausgemalt. Im Rest dieses Kapitels gehen wir erste Schritte und üben ein, wie das Arbeiten im GitOps-Stil praktisch aussieht. Die dabei entstehende Implementierung ist sehr vereinfacht und nicht produktionsreif. Wir favorisieren an dieser Stelle einen zügigen Aufbau und geringe Komplexität vor zu frühen Abstraktionen.
Diese Implementierung ist nur eine Variante von vielen Wegen, wie man GitOps umsetzen kann. Wir beschränken uns an dieser Stelle bewusst auf eine einzige Variante, um zuerst den ganzen Pfad durchzugehen und eine Implementierung vollständig von Anfang bis Ende abzudecken. Dabei werden wir viele Themen nur streifen und erst das ganze Spektrum sehen, bevor wir uns dann in den nachfolgenden Kapiteln den einzelnen Themen in der Tiefe widmen.
Anfangs wollen wir die gerade formulierten Orientierungsfragen nutzen, um den endgültigen Zielzustand zu beschreiben:
Die vollständige Architektur versuchen wir in Abb. 3–1 übersichtsartig darzustellen. Die Beispielimplementierung fängt in diesem Kapitel erst noch in einem vereinfachten Zustand an und wir bauen sie stellenweise im Lauf des Buches aus. (Konkret werden wir uns beispielsweise mit HashiCorp Vault erst in Kapitel 5 auf Seite 97 befassen.)
Abb. 3–1
Vollständige Architektur der Beispielimplementierung
Den reduzierten Zielzustand für dieses Kapitel versuchen wir in Abb. 3–2 genauer darzustellen: Wir werden in GitLab ein Config-Repo haben, aus dem das ganze Environment aufgebaut wird. Als einzigen Workload wollen wir eine simple Beispielanwendung ausführen (podinfo4 von Weaveworks). Das Container-Image dafür liegt in der GitHub Container Registry.
Wir werden den Argo CD Autopilot5 verwenden, um eine Repository-Struktur aufzubauen und Argo CD in den Cluster zu installieren. Mithilfe eines Argo CD ApplicationSet werden wir die Beispielanwendung als Application deployen, und dadurch werden unsere Anwendungen auf GitOps-Weise in den Cluster betrieben.
Abb. 3–2 sieht womöglich erst einmal erschreckend komplex aus. Wir arbeiten uns von oben nach unten vor und werden Schritt für Schritt die einzelnen Bereiche erhellen. Es mag hilfreich sein, im Verlauf dieses Kapitels immer wieder auf dieses Diagramm zurückzugreifen.
Abb. 3–2
Zielzustand der Beispielimplementierung am Ende dieses Kapitels
Wir führen konkret folgende Schritte aus:
Schauen wir uns zuerst an, welche Voraussetzungen wir für die Implementierung benötigen:
Um die Implementierung selbst nachspielen zu können, benötigst du
Ganz zu Beginn starten wir einen lokalen Cluster mit minikube. Dafür genügt der Befehl minikube start.
Bevor wir Argo CD mithilfe des Autopiloten installieren, versuchen wir einige grundlegende Dinge über Argo zu verstehen: Argo ist eine Familie an Projekten, zu denen offiziell die folgenden gehören:
Der Argo CD Autopilot gehört noch nicht offiziell zur Argo-Familie, bietet uns aber dennoch einige Vorteile:
Config-Repo erstellen und Autopilot autorisieren
Wir werden jetzt zuerst Argo CD aufsetzen mithilfe des Autopiloten und anschließend nachvollziehen, was alles in diesen wenigen Schritten passiert ist. Im Zuge dessen werden wir auch ein Config-Repo befüllen. Dafür erstellen wir ein neues Repo in GitLab unter https://gitlab.com/projects/new. (Die HTTPS-URL dieses Repositorys bezeichnen wir im Folgenden mit GITLAB_REPO_URL.) Dieses Repo lassen wir gleich vom Autopiloten befüllen.
Als Beispiel stellen wir auf GitLab auch ein fertig befülltes Referenz-Config-Repo zur Verfügung14. Der Unterordner ch03/ in diesem Repo enthält die Inhalte, wie sie am Ende dieses Kapitels aussehen können. Ein eigenes Repo zu erstellen ist dennoch nötig, weil du andernfalls keine Schreibrechte auf das Config-Repo hast.
Damit auch der Autopilot in unser neues Repo committen kann, benötigt er Credentials. Dafür erzeugen wir in GitLab einen Personal Access Token (PAT) mit Schreibzugriff auf unser neues Repo. Unter https://gitlab.com/-/profile/personal_access_tokens wählen wir unter »Select scopes« nur den Scope write_repository aus. Den Wert, der anschließend unter »Your new personal access token« angezeigt wird, referenzieren wir im Folgenden mit GITLAB_PAT.
Dann exportieren wir folgende Variablen und starten die initiale Provisionierung:
Listing 3–1
Initiales Provisionieren mit Argo CD Autopilot
1
export GIT_USER=YOUR_GITLAB_USERNAME
2
export GIT_TOKEN=GITLAB_PAT
3
# Example repo and directory:
4
# GIT_REPO=https://gitlab.com/gitops-book/erp-gitops/ch03
5
export GIT_REPO=GITLAB_REPO_URL
6
argocd-autopilot repo bootstrap
Argo CD wird gestartet, und es dauert einige Minuten, bis der Cluster die Images heruntergeladen hat. Nach wenigen Minuten sollte am Ende der Terminal-Ausgabe ein Text ähnlich wie der folgende erscheinen:
Listing 3–2
Ende der initialen Ausgabe
1
INFO argocd initialized. password: 59NKDxtX-uvK0zwn
2
INFO run:
3
4
kubectl port-forward -n argocd \
5
svc/argocd-server 8080:80
Wir starten diesen Port-Forward und öffnen https://localhost:8080 im Browser. Dort melden wir uns mit dem Usernamen »admin« und dem Passwort aus der Terminal-Ausgabe an.
Argo CD leitet uns direkt zu HTTPS weiter. Die meisten Browser sind damit bei localhost nicht unmittelbar einverstanden und müssen entsprechend konfiguriert werden. In Chrome und verwandten Browsern (auch Edge) kannst du dem Vertrauen von selbstsignierten Zertifikaten unter localhost zustimmen unter einer bestimmten URL15. Andere Browser ermöglichen das Öffnen der Seite, indem man auf der Warnseite einer Ausnahmeoption zustimmt.
Nach dem Login sollten wir vier Applications zu sehen bekommen wie in Abb. 3–3.
Mit diesen wenigen Befehlen sind mehrere Dinge auf einmal geschehen: Der Autopilot hat Argo CD direkt in den Cluster installiert und anschließend einige Manifeste ins Config-Repo committet. Abb. 3–4 zeigt die Ordnerstruktur im Repository.
Wir werden in diesem Kapitel nur mit den Ordnern apps und projects umgehen. Die genaue Struktur, die der Autopilot erzeugt, werden wir in 6.7.1 auf Seite 170 genauer betrachten.
Abb. 3–3
Die UI von Argo CD nach der initialen Provisionierung durch den Autopiloten
Abb. 3–4
Ordnerstruktur wie von Argo CD Autopilot erzeugt
Damit wir besser verstehen, wie Argo CD funktioniert, betrachten wir drei Custom Resource Definitions (CRDs), die Argo CD von Haus aus mitbringt:
Der untere Teil von Abb. 3–2 auf Seite 53 kann in diesem Abschnitt sehr hilfreich sein, um visuell besser zu verstehen, wie die verschiedenen Argo-CD-Ressourcentypen miteinander zusammenhängen.
Wir betrachten an dieser Stelle noch jeweils ein Beispiel für eine Application und ein ApplicationSet unter den Dateien, die vom Autopilot erzeugt wurden. Eine der Applications, die erzeugt wurden, ist die Application argo-cd im Namespace argocd:
Listing 3–3
Eine Application in bootstrap/argo-cd.yaml
1
apiVersion: argoproj.io/v1alpha1
2
kind: Application
3
metadata:
4
creationTimestamp: null
5
labels:
6
app.kubernetes.io/managed-by: argocd-autopilot
7
app.kubernetes.io/name: argo-cd
8
name: argo-cd
9
namespace: argocd
10
spec:
11
destination:
12
namespace: argocd
13
server: https://kubernetes.default.svc
14
ignoreDifferences:
15
- group: argoproj.io
16
jsonPointers:
17
- /status
18
kind: Application
19
project: default
20
source:
21
path: bootstrap/argo-cd
22
repoURL: https://gitlab.com/gitops-book/erp-infra.git
23
syncPolicy:
24
automated:
25
allowEmpty: true
26
prune: true
27
selfHeal: true
28
syncOptions:
29
- allowEmpty=true
Eine Application hat ein Ziel (ein Kubernetes-Cluster, in diesem Fall der Cluster, in dem Argo CD selbst läuft) und eine Quelle (in diesem Fall das Config-Repo, mit dem wir gerade arbeiten). Wenn eine Application ein Git-Repo referenziert, können in diesem Repo auch weitere Applications liegen; eine Application ist schließlich selbst nur ein gewöhnliches Kubernetes-Manifest.
Auf diese Weise kann man Applications auch hierarchisch verschachteln. Diese Herangehensweise wird als »App of Apps« bezeichnet (mehr dazu in Abschnitt 6.6.2 auf Seite 168). In der UI von Argo CD können wir dieses Pattern direkt bei der Application autopilot-bootstrap sehen: Sie bindet zwei Applications und ein ApplicationSet ein (siehe Abb. 3–5).
Abb. 3–5
App of Apps: Die Application autopilot-bootstrap bindet weitere Applications ein.
ApplicationSets wiederum sind Templates, aus denen Applications erzeugt werden durch Variablen und Generatoren. Die Funktionsweise lässt sich mit Deployments und Pods vergleichen: Ein ApplicationSet und die Applications, die daraus erzeugt werden, sind analog zu einem Deployment, aus dem Pods (genauer gesagt ReplicaSets) erzeugt werden. Das Template im ApplicationSet ist dann analog zum Pod-Template im Deployment.
Ein ApplicationSet lässt sich auch in gewisser Weise mit einem Helm-Chart vergleichen: Das Template im ApplicationSet entspricht den Templates im Chart. Die Generatoren im ApplicationSet entsprechen ungefähr den Values-Files, aus denen konkrete Helm-Releases instanziiert werden können.
Ein Beispiel für ein ApplicationSet unter den vom Autopilot generierten Dateien ist das ApplicationSet cluster-resources im Namespace argocd. Wir stellen es aus Platzgründen hier nur verkürzt dar:
Listing 3–4
Ein ApplicationSet in bootstrap/cluster-resources.yaml
1
apiVersion: argoproj.io/v1alpha1
2
kind: ApplicationSet
3
metadata:
4
# ...
5
name: cluster-resources
6
namespace: argocd
7
spec:
8
generators:
9
- git:
10
files:
11
- path: bootstrap/cluster-resources/*.json
12
repoURL: https://gitlab.com/gitops-book/erp-infra.git
13
# ...
14
template:
15
metadata:
16
labels:
17
app.kubernetes.io/managed-by: argocd-autopilot
18
app.kubernetes.io/name: cluster-resources-{{name}}
19
name: cluster-resources-{{name}}
20
namespace: argocd
21
spec:
22
destination:
23
server: '{{server}}'
24
# ...
25
source:
26
path: bootstrap/cluster-resources/{{name}}
27
repoURL: https://gitlab.com/gitops-book/erp-infra.git
28
syncPolicy:
29
automated:
30
allowEmpty: true
31
selfHeal: true
In diesem Fall werden Variablen in doppelten geschwungenen Klammern wie {{name}} ersetzt durch ihre Werte in den Dateien unter bootstrap/cluster-resources/*.json, und aus den resultierenden ausgefüllten Templates erzeugt das ApplicationSet dynamisch Applications.
Wir werden jetzt ein AppProject, eine Application und ein Kubernetes-Deployment erzeugen. Das Deployment wird von der Application verwaltet werden, und die Application wird wiederum dem AppProject zugeordnet sein.
Die Zuordnung von Applications in ein AppProject ist optional (es gibt standardmäßig ein AppProject namens »default«), doch es bietet Vorteile: Man kann Berechtigungen auf den zugehörigen Applications zentral steuern und beispielsweise festlegen, in welchen Namespaces diese Applications Ressourcen erzeugen dürfen.
Wir erstellen in diesem Fall ein AppProject namens podinfo-dev, welches das gesamte Environment für unseren Workload enthalten wird:
Listing 3–5
Erstellen des AppProjects
1
argocd-autopilot project create podinfo-dev
Der Autopilot erzeugt daraufhin eine Datei namens projects/podinfodev.yaml, welche das AppProject und das ApplicationSet enthält.
Anschließend erzeugen wir eine (leere) Application:
Listing 3–6
Erstellen der Application
1
argocd-autopilot app create podinfo \
2
--project podinfo-dev --type kustomize \
3
--app deployment.yaml
Der Autopilot committet dabei eine Kustomization in das Repository, die auf die Datei apps/podinfo/base/deployment.yaml verweist, die aber noch nicht existiert. Erzeugen wir sie, um unsere Beispielanwendung zu deployen!
Bevor wir das tun können, müssen wir noch den neuesten Stand des Repositorys pullen, weil der Autopilot alle Commits im Hintergrund (und nicht in der Working Directory unserer Shell-Session) macht. Anschließend können wir unser Manifest erzeugen:
Listing 3–7
Erstellen des Deployments
1
git pull
2
kubectl create deployment podinfo \
3
--dry-run=client -o=yaml --port=9898 \
4
--image=ghcr.io/stefanprodan/podinfo:6.4.0 \
5
> apps/podinfo/base/deployment.yaml
Dieses Manifest könnten wir an diesem Punkt auch imperativ deployen mit kubectl apply, um das Deployment im Cluster zu installieren. Wenn wir aber mit GitOps arbeiten, wollen wir stattdessen die Datei committen und die Arbeit des Ausrollens dem GitOps-Operator überlassen. Committen wir also die Datei:
Listing 3–8
Erstellen des Deployments
1
git add apps/podinfo/base
2
git commit -m "feat: add podinfo deployment"
3
git push
Wenn wir ein paar Sekunden warten, sollte die UI von Argo CD uns eine neue Application mit einem gesunden Deployment zeigen wie in Abb. 3–6 und Abb. 3–7.
Abb. 3–6
Eine neue Application in der Übersicht
Wir können uns auch noch konkreter von der gesunden Ausführung der Anwendung überzeugen, indem wir einen Port-Forward starten und anschließend die URL http://localhost:9898 im Browser öffnen.
Listing 3–9
Port-Forward des Deployments
1
kubectl port-forward deploy/podinfo 9898:9898
2
# Open http://localhost:9898
Eine Ansicht wie in Abb. 3–8 sollte uns begegnen.
Abb. 3–7
Ein gesundes Deployment in der neuen Application
Abb. 3–8
Podinfo im Browser
Nach diesem erfolgreichen Setup sind wir in der Lage, Änderungen an unserer Beispielanwendung voll kontinuierlich von Argo CD deployen zu lassen. Als einfaches Beispiel nehmen wir eine Skalierung: Sagen wir, wir wollen das Deployment auf zwei Replicas hochskalieren.
Ohne GitOps würden wir das vielleicht imperativ machen, indem wir folgenden Befehl ausführen:
Listing 3–10
Imperatives Skalieren des Backends
1
kubectl scale deploy/podinfo --replicas=2
Wenn wir diesen Befehl ausführen, startet zwar ein zweiter Pod, aber dieser fährt fast sofort wieder herunter. Das liegt daran, dass unsere manuelle Änderung am Deployment sofort von Argo CD überschrieben wird.
Statt imperativ zu arbeiten, können wir komplett GitOps-konform arbeiten, indem wir diese Änderung stattdessen deklarativ im Manifest vornehmen: Wir bearbeiten also die Deployment-Datei, die wir in Schritt 3 erzeugt haben, und setzen .spec.replicas=2. Nachdem wir diese Änderungen committen und pushen, startet eine zweite Replica erfolgreich (siehe Abb. 3–9).
Abb. 3–9
Eine zweite Replica startet
In diesem Kapitel haben wir Hilfsmittel kennengelernt, um GitOps-Lösungen einschätzen und miteinander vergleichen zu können. Diese Hilfsmittel haben wir direkt angewandt bei einer Beispielimplementierung.
Im Rahmen dieser Implementierung haben wir Argo CD mithilfe des Argo CD Autopilot in einem lokalen Cluster installiert und unsere beiden Beispielapplikationen dorthin deployt. Wir haben Argo CD und seine Ressourcentypen kennengelernt, insbesondere Application-Sets mit ihren Templating-Möglichkeiten.
Ebenso haben wir den Unterschied erlebt zwischen imperativem und deklarativem Deployen, indem wir eine der Anwendungen zuerst imperativ und dann deklarativ skaliert haben. Das greifbarste Ergebnis dieses Kapitels ist das Config-Repo, das wir erstellt und mit Manifesten befüllt haben.