lars
webmobiledatenbankendevopsarchitektur
hello (at) larskoelpin.de

GitOps PUSH und PULL-Deployment

October 23, 2022
opskubernetes

In einem vergangenen Beitrag habe ich das Konzept von GitOps und dessen Ebenen erläutert. Dabei verändert sich die Runtime/Container-Ebene wohl am meisten — Nämlich immer dann, wenn eine neue Version der Applikation deployed werden soll. Wie neue Artefakte der Applikationen auf dem System installiert werden ist über verschiedene Methoden möglich. Dabei lassen sich alle Methoden in PUSH und PULL-Prozessen einordnen.

PUSH-Flow

Ein Push-Orientierter Prozess, wie er im Bild 1 gezeigt wird, ist dabei der wohl bekannterer.

Bild 1: PUSH-Flow
Bild 1: PUSH-Flow

Ein Continious Integration System (CI-System) reagiert dabei klassischerweise auf Änderungen im Source-Code-Management-System.

Das CI-System kompiliert dabei die neuen Änderungen der Applikation wie im Schritt (1) gezeigt. Da das CI-System hat in einem Push-orientiertem Flow eine Continous Deployment-zugeordnete Aufgabe inne hält, folgen in (2) 2 Dinge:

  1. Zunächst wird das kompilierte Artefakt (im Bild in der Version 1) in eine zentrale Registry (z.B, über docker push in eine docker registry) übergeben.

  2. Da dieses System auf GitOps basiert, existiert zusätzlich ein git repository, das mit dem Deployment synchronisert wird. Im nächsten Schritt (4) muss das CI-System deshalb das Cluster-Repository auschecken und dort die Versionsänderung einpflegen sowie commiten. Danach erzeugt das CI-System in (5) einen Pull-Request am Cluster-Repository. Wenn dieses den Pull-Request in Schritt (6) akzeptiert hat, ist es eine Frage der Konfiguration des CD-Systems (z.B. FluxCD), wann dieser in Schritt (7) eine neue Synchronisations-Schleife startet. Durch die verarbeitete Änderung sieht das Operator-System, dass ein neues Artefakt in das Cluster gezogen werden muss. Es startet deshalb in Schritt (8) einen Prozess, um an dieses Artefakt, das in Schritt (4) gesetzt wurde, zu gelangen.

Dieser Flow hat einige Nachteile: Zum Einen benötigen viele CI-Systeme Zugriff auf das Cluster-Repository. Das führt dazu, dass Rechte einzelner CI-Systeme verwaltet werden müssen. Zum Anderen muss das CI-System die interne Struktur des Cluster-Repository kennen. Refactorings für die interne Struktur des Cluster-Repository werden dadurch erschwert.

Ein Deployment Script sieht dabei beispielsweise so aus:

deploy:
  steps:
    - name: Checkout gitops repository
      uses: actions/checkout@master
      with:
        ref: main
        repository: cluster-configuration
        token: ${{ TOKEN }}
        path: gitops
    - run: |
        bin/update-production-version.sh ./apps/webapp/env/prod/image-version.yaml webapp my-registry.de/:$GITHUB_SHA
        git config user.name github-actions
        git config user.email [email protected]
        git checkout -b update-webapp-version-$GITHUB_SHA
        echo "Push version to update-webapp-version-$GITHUB_SHA"
        git add .
        git commit -m "[WebApp] - 1.0.0-$GITHUB_SHA"
        git push origin update-webapp-version-$GITHUB_SHA
      working-directory: gitops

Dazu muss das Ziel Repository (Cluster-Repository) geklont werden, git konfiguriert werden und ein Script bereitgestellt werden, um z.B. YAML Dateien von Kubernetes zu editieren (in dem Beispiel: bin/update-production-version). Die durch das CI-System erstellten Pull-Requests, müssen danach durch einen automatisierten Prozess automatisch akzeptiert werden können.

Bei einem Push-basierten Prozess, muss das CI-System direkt in das Repository commiten und somit die interne Struktur kennen / oder eine API vom Cluster-Repository (“update-production-version”) bereitgestellt werden. Dies ist insbesondere Problematisch, da es nicht nur ein CI-System gibt, sondern pro Projekt eines. Eine größere Änderung des Cluster-Repository (z.B. das Einführen von neuen Ordnern) ist deshalb schwierig, ohne bestehende CI-Systeme zu brechen.

PULL-Flow

Glückerlicherweise gibt es einen weiteren Ansatz, den Prozess zu gestalten: Ein Artifact-Pull Modell, das in Bild 2 gezeigt wird.

Bild 2: PULL-Flow
Bild 2: PULL-Flow
 Bei diesem Modell ist schon in (1), (2) und (3) klar der Vorteil abgegolten: Das CI-System hat nach den drei trivialen Schritte Bauen und Pushen mit dem Prozess abgeschlossen.

Wo vorher programmtisch ein Pull-Request erstellt wurde, übernimmt nun stattdessen der Operator diese Aufgabe. In (4) aktualisert dieser kontinuierlich alle verfügbaren Versionen in der zentralen Registry (z.B: Docker).

Findet dieser ein bestimmtes Tag, z.B. Tags die mit RELEASE- starten, aktualisert dieser in (5) die Version automatisch im Repository. Er synchronisiert also Docker-Registry mit Konfigurationsrepository. Nach der Aktualisierung in (5) pullt der Operator in (6) in seinem nächsten Zyklus die neue Version und deployed diese.

Dieser Ansatz führt dieselben Schritte aus wie der Push-Ansatz — nur die Aufgaben sind besser verteilt. Statt, dass das CI-System von jedem Projekt Git-Operationen und File-System Änderungen macht, übernimmt das nun das CD-System. Die Schnittstelle zwischen den Systemen ist die Registry.

Implementierung von Image-Automation

In der Praxis bietet FluxCD beispielsweise eine Image-Automation an. Dazu braucht der Operator, der die Registry überwacht, zwei Informationen:

  • Welches Image soll ich überwachen?
  • Welches Tag-Pattern soll ich regelmäßig deployen

Diese Information wird wird zuvor (also bevor der ganze deployment Prozess überhaupt startet) über eine Kubernetes-API (CRD) konfiguriert. Konkret sieht das für FluxCD dann so aus:

# Wohin soll commited werden?
kind: ImageUpdateAutomation
spec:
  interval: 1m0s
  sourceRef:
    kind: GitRepository
    name: flux-system
    namespace: flux-system
  git: ...
   push:
      branch: main
  update:
    strategy: Setters
---
# Welches Image soll aktualisert werden?
kind: ImageRepository
metadata:
  name: blog
spec:
  image: myimage
---
# Welche Tags sind relevant?
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImagePolicy
metadata:
  name: blog
spec:
  imageRepositoryRef:
    name: blog
  filterTags:
    pattern: '^RELEASE\.(?P<timestamp>.*)Z$'
    extract: '$timestamp'
  policy:
    alphabetical:
      order: asc

In diesem Fall bedeutet das also, dass alle Tags die mit RELEASE.$TIMESTAMP getaggt werden, vom Operator erfasst werden.

╰─❯ k get imagepolicy -A
NAMESPACE    NAME          LATESTIMAGE
production   blog          myrepository/blog:RELEASE.2022-10-22T19-06-08Z

Gleichzeitig wird in der entsprechenden Stelle im Deployment die zuvor erstellte Policy über einen speziellen Kommentar referenziert. Das ist genau die Schnittstelle, die im Push-Modell durch das Skipt implementiert wurde, das die YAML-Datei aktualisiert.

kind: Deployment
spec:
  template:
    spec:
      containers:
      - image: myrepository/blog:RELEASE.2022-10-22T19-06-08Z # {"$imagepolicy": "production:blog"}

Wird nun ein neues Docker Image in die Registry gepusht (von lokal oder vom CI-System) ist nach kurzer Zeit in neuer Commit im Cluster-Repeository zu finden und die Änderung deployed.

commit 3e894ef61770e917db39aa980297c69e875b8a59
Author: fluxcdbot <[email protected]>
Date:   Sat Oct 22 19:24:51 2022 +0000

    myrepository/blog:RELEASE.2022-10-22T19-06-08Z

Zusammenfassung

In diesem Beitrag habe ich mir die beiden Ansätze vom Push-Modell und Pull-Modell für Deployment-Akutalisierungen angeschaut. Obwohl beide Ansätze dieselben Aufgaben haben, werden diese an unterschiedlichen Stellen gelöst. Der Push Ansatz sieht mehr Logik im CI-System vor, während ein Pull-Ansatz die Deployment-Logik als eine Schnittstelle zwischen Artefakt-Registry und Überwachungsmechanismus reaktiv implementiert. Obwohl der Push-Ansatz klarer in der Implementierung ist, hat der PULL-Ansatz erhebliche Vorteile, da keine Interna der Cluster-Konfiguration im CI-System bekannt sein müssen.