GitOps in der Praxis

Seite 2: Die Mechaniken von GitOps

Inhaltsverzeichnis

Das Konzept im Beispielprojekt sieht vor, dass Entwickler mehrere Versionen der gleichen Anwendung im Cluster installieren können, die aber vollständig voneinander isoliert sind. Dies schließt unterschiedliche Endpunkte im Cluster und eine jeweils eigene Datenbank ein. Ein solches Vorgehen erlaubt die parallele Entwicklung der Software an mehreren Feature-Branches und deren Testbarkeit für die Entwickler. Wenn ein Feature fertiggestellt ist, erfolgt der Merge auf den Master-Branch, und der GitOps-Ansatz sorgt für eine schnelle, automatische Aktualisierung der Software in den Umgebungen des Clusters. Für den Fall, dass eine fehlerhafte Software im Cluster installiert worden ist, lässt sich durch ein git revert der ursprüngliche Zustand wiederherstellen, da das Projekt dem SSOT-Prinzip folgt.

Im Weiteren werden die Terraform-Skripte für die Provisionierung der Infrastruktur und die Bash-Skripte für die Installation des Kubernetes Cluster nach dem Ansatz Infrastruktur as Code (IaC) im Git versioniert abgelegt. Dadurch ist sichergestellt, dass sich die Installationen immer nach dem gleichen Schema und mit den gleichen Konfigurationen aufsetzen lassen.

Die Grundlage für das Deployment der Anwendung im Beispielprojekt stellt Helm dar. Der im Root-Verzeichnis ausgeführte Befehl helm create refimpl bereitet die Anwendung für das Ausrollen vor. Er erstellt einen Unterordner refimpl, der ein Basis-Set an Dateien enthält, die für ein Deployment auf Kubernetes nötig sind. Die darin enthaltene Datei values.yaml kommt für das Verteilen unterschiedlicher Versionen der Anwendung zum Einsatz. Im Beispiel befindet sich im Ordner die Datei values.yaml, die auf eine feste Versionsnummer als Image-Tag verweist. Zudem existiert eine values-master.yaml, die auf das Latest-Image-Tag der Anwendung verweist. Über diesen Tag installieren Entwickler stets den letzten Master-Stand im Cluster. Beim Erstellen des Hauptzweiges schreibt die Software einen Zeitstempel in die Datei values-master.yml, der in der Datei templates/deployment.yaml als Label referenziert ist. Auf die Weise pusht das System immer ein Update des Repository und der Templates, damit der ArgoCD-Server das Deplyoment der neuen Version anstößt.

spec:  
  replicas: {{ .Values.replicaCount }}  
  selector:  
    matchLabels:  
      {{- include \
    	"refimpl.selectorLabels" . | nindent 6 }}  
  template:  
    metadata:  
      labels:  
        timestamp: '{{ .Values.timestamp }}'

Diese Referenzierung im Deployment Template stellt sicher, dass Helm jede Änderung des Sollzustands erkennt und eine neue Version erstellt.

Zum Isolieren der Anwendung injizieren Entwickler Kubernetes- beziehungsweise Helm-Variablen über Umgebungsvariablen, um die Anwendung dynamisch konfigurieren zu können. Die verwendete MongoDB installieren sie ebenfalls über ein Helm Chart von Bitnami im Kubernetes-Cluster. Um die Datenbank an die Anwendung anzubinden, konfigurieren sie den Kubernetes-Service der MongoDB in dem dazugehörigen Namespace als Kombination von Kubernetes-Variablen und festen Werten in der deployment.yaml.

containers:
image: "{{ .Values.image.repository }}:
        {{ .Values.image.version }}"  
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:  
  - name: MY_POD_NAMESPACE  
    valueFrom:  
      fieldRef:  
        fieldPath: metadata.namespace  
  - name: MONGODB  
    value: "$(MY_POD_NAMESPACE)-mongo-mongodb
            .$(MY_POD_NAMESPACE).svc"

Das Template definiert in der Containers-Sektion zuerst die Variable MY_POD_NAMESPACE mit dem Namen des Namespace. Anschließend lässt sich diese Variable für den Container weiterverwenden, um beispielsweise Service-Adressen dynamisch zusammenzubauen.

Ein weiterer wichtiger Punkt für die Isolierung von Anwendungen ist ein dynamischer und unabhängiger Endpunkt. Im Projekt erfolgt die Definition mit Helm und Umgebungsvariablen. Die Bereitstellung erfolgt anhand einer Konfiguration des Helm-Template ingress.yaml:

rules:  
  - http:  
      paths:  
        - path: /{{.Release.Namespace}} \
                 {{ .Values.ingress.path }}  
          backend:  
            serviceName: {{ $fullName }}  
            servicePort: {{ $svcPort }}

Das Template greift erneut auf eine Namespace-Variable zu. Helm stellt die Variable .Release.Namespace standardmäßig bereit. Die Variable .Values.ingress.path ist in values.yaml konfiguriert. Ein resultierender Endpunkt sieht beispielsweise folgendermaßen aus: /master-stage/refimpl/. Im Weiteren gilt es, den Kontextpfad der Anwendung im deployment.yaml-Template als Umgebungsvariable zu definieren, da sonst der Webserver der Spring-Anwendung nicht über den angepassten Pfad für Kubernetes erreichbar ist.

- name: SERVER_CONTEXTPATH  
  value: /{{ .Release.Namespace }}/ \
          {{ .Values.ingress.path }}

In diesem Fall überschreibt Spring automatisch den Wert, der in der application.yml konfiguriert ist.

Bei der benötigten ersten Installation der Anwendung im Kubernetes-Cluster über den ArgoCD-Server, erfolgt die Einrichtung der zu überwachenden Repositories mit dem Argo-CLI. Die Installation steht für Entwickler automatisiert als GitLab-Job bereit. Das Rollout-Repository enthält ausschließlich die Datei gitlab-ci.yml und ein Skript. Um den Job zu starten, müssen Entwickler lediglich eine neue Pipeline eines Rollout-Job-Repository anstoßen. Vor dem Start der Pipeline definieren sie die benötigten Werte in GitLab-Job-Variablen und verwenden sie anschließend im Skript. Folgende Abbildung zeigt die Konfiguration des Jobs.

Rollout-Job-Konfiguration

Schließlich gilt es die Skripte für die Rollout-Jobs zu erstellen. gitlab-ci.yml enthält die nachfolgende Konfiguration:

variables:
 KUBECONFIG: /kubeconfig
stages:
    - rollout

create-application-namespace:
 stage: rollout
 image: registry.gitlab.com/artikel-gitops/docker-rollout
 only:
     refs:
         - master
    variables:
         - $NAMESPACE
 script:
     - source prepareEnv
     - ./createApps.sh -n $NAMESPACE -v $VALUES -d $DATABASE

Zu Beginn des Rollout-Jobs wird im Abschnitt variables der Pfad zu der dem Cluster zugehörigen kubeconfig angegeben. Diese Konfiguration ist bereits im referenzierten Docker-Image hinterlegt. Der Rollout besteht ausschließlich aus einer Stage und dem Job create-application-namespace. Die Pipeline des Jobs lässt sich in dieser Konfiguration ausschließlich vom Hauptzweig und mit der GitLab-Variable NAMESPACE starten. Der Job ruft zunächst das Skript prepareEnv auf.

#!/usr/bin/env bash
echo  "[INFO] Create port forward to argocd server"
kubectl -n argocd port-forward svc/argocd-server 8080:443 &

echo  "[INFO] Wait for port forward"
sleep 5

echo  "[INFO] Login to argocd server"
argocd --plaintext --username <username> \
       --password <password> login localhost:8080

Zunächst stößt das Skript einen Port-Forward auf den ArgoCD-Service als Hintergrundprozess an. Dieser mappt den Port 443 für die Kubernetes-Services auf localhost:8080. Nach einer Wartezeit von 5 Sekunden erfolgt ein Log-in über das Argo-CLI auf den ArgoCD-Server. Durch den Aufruf des Skripts mit dem Parameter source sind der Log-in und der Port-Forward in der Job Shell aktiv. Zum Schluss erfolgt der Aufruf des Skripts createApp.sh, das die Anwendung im Cluster installiert.

echo  "[INFO] Create Databases for app"
argocd app create ${NAMESPACE}-mongo \
--sync-policy automated \
--sync-option CreateNamespace=true \
--repo https://charts.bitnami.com/bitnami \
--helm-chart mongodb \
--revision 10.1.3 \
--dest-server https://kubernetes.default.svc \
--dest-namespace ${NAMESPACE} \
-p auth.username="test" -p auth.password="test" \ 
-p auth.database="testDatabase" -p auth.rootPassword="test"

echo "[INFO] Deploy app to kubenetes Namespace: ${NAMESPACE}"
argocd app create ${NAMESPACE}-refimpl \
--sync-policy automated \
--repo https://gitlab.com/artikel-gitops/refimpl.git \
--path refimpl 
--dest-server https://kubernetes.default.svc \
--dest-namespace ${NAMESPACE} \
--sync-option CreateNamespace=true \
--values ${VALUES}

Das Auslesen der Parameter an das Skript ist über Bash-Mechanismen realisiert. Der erste Aufruf setzt die MongoDB-Datenbank der Anwendung auf. Es handelt sich um das Helm-Chart-Repository von Bitnami. Die Angabe der Parameter --helm-chart mongodb --revision 10.1.3 ist notwendig, da Helm die Version benötigt. Im Weiteren überschreibt das Skript mit den Parametern

-p auth.username="test"
-p auth.password="test" 
-p auth.database="testDatabase" 
-p auth.rootPassword="test" 

die Standardwerte des Helm-Charts der MongoDB und konfiguriert die Datenbank. Die im Beispielprojekt als Klartext angegebenen Credentials verbieten sich freilich in realen Anwendungen. Der letzte Aufruf installiert die Anwendung im Kubernetes-Cluster. Der Parameter --path refimpl gibt in diesem Fall den Ordner an, unter dem die mit dem Parameter --values ${VALUES} konfigurierte Datei values.yaml zu finden ist.