blog.kuepper.nrw

Vereinfachte GitOps-Pipeline mit FluxCD und automatischen Image-Updates

Autor: Rüdiger Küpper

Erstellt:
Letzte Aktualisierung:
Kategorie: Kubernetes
Tags: KubernetesFluxCDImageUpdateAutomationGitOps

FluxCD Image Update Automation

Traditionelle Build-Pipelines sind oft komplex und fehleranfällig. Sie müssen nicht nur den Anwendungscode verarbeiten, sondern auch Helm Charts verwalten, semantische Versionierung durchführen und das finale Deployment orchestrieren. Wenn dabei ein Schritt fehlschlägt – beispielsweise ein fehlerhafter Helm Chart – bricht die gesamte Pipeline ab, obwohl das Docker Image bereits erfolgreich erstellt wurde. Besonders bei größeren Projekten mit längeren Build-Zeiten ist das frustrierend und ineffizient.

Die Lösung liegt in der Trennung von Build- und Deployment-Prozessen durch eine GitOps-Architektur. Anstatt alles in einer monolithischen Pipeline zu erledigen, beschränke ich die Build-Pipeline auf das Wesentliche: semantische Versionierung, Docker Image-Erstellung und Push in die Registry. Das Deployment wird komplett an FluxCD delegiert, das kontinuierlich das GitOps-Repository überwacht und automatisch neue Image-Versionen erkennt und ausrollt.

Diese Architektur bietet mehrere Vorteile: Build-Pipelines werden einfacher und stabiler, Deployments erfolgen vollautomatisch ohne manuelle Eingriffe, und durch die stage-spezifische Konfiguration können verschiedene Umgebungen (dev, stage, prod) mit unterschiedlichen Image-Tags versorgt werden. FluxCD übernimmt dabei nicht nur das Deployment, sondern auch die automatische Aktualisierung der Image-Tags im GitOps-Repository.

Build Pipeline sehen oft wie folgt aus:

Build Pipeline sehen oft wie folgt aus

Diese Piplelines müssen nicht nur den Code selbst auschecken, sie müssen zusätzlich auch noch den HelmChart auschecken könne. Liegen diese wo anders, benötigt man hier wieder einen Token für Pull und Push. Tritt beim Pull oder des Push des HelmCharts ein Problem auf, oder ist der HelmChart fehlerhaft, dann bricht die komplette Pipeline ab. Gerade bei großen Projekten, bei denen ein Build etwas länger dauert ist das sehr ärgerlich. Da das Image eigentlich schon fertig für das Deployment ist, aber ein Fehler im Chart das verhindert.

Meine Pipeline macht nur noch SemVer und erstellt das Docker Image und push es in die Docker Registry. Sonst wird da nicht mehr gemacht. Der Rest wird an anderer Stelle erledigt.

Ich installiere meine Kubernetes Cluster mit Cloudinit und mache so direkt ein flux bootstrap, damit alles in den Clustern installiert wird.

# ...
# other stuff: install kubernete, helm, k9s ....
# ...
# FluxCD Bootstrap
export GITHUB_TOKEN="${github_token}"
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

# Warten bis Kubernetes API verfügbar ist
while ! kubectl cluster-info &>/dev/null; do
	echo "Waiting for Kubernetes API..."
	sleep 10
done

# FluxCD Bootstrap ausführen
flux bootstrap github \
--owner=ruedigerp \
--repository=fluxcd \
--branch=main \
--path=clusters/production \
--personal \
--components-extra=image-reflector-controller,image-automation-controller

Im Repo gitops ist für jeden Cluster ein Verzeichnis für die jeweile Konfigurationen. Damit werden Helm Chart Repos, Helm Charts, Applications und weitere Stage spezifische Konfigurationen gemacht.

Im repo ist auch ein Verzeichnis `/apps/, in dem sind Applications die installiert werden sollen. Wie zum Beispiel mein Blog.

❯ tree apps/blog
apps/blog
├── base
│   ├── deployment.yaml
│   ├── ingress-http.yaml
│   ├── ingress-https.yaml
│   ├── kustomization.yaml
│   └── service.yaml
├── dev
│   ├── deployment-patch.yaml
│   └── kustomization.yaml
├── prod
│   ├── deployment-patch.yaml
│   └── kustomization.yaml
└── stage
    ├── deployment-patch.yaml
    └── kustomization.yaml

Im Base Verzeichnis sind alle Kubernetes YAML-Files die für die Application benötigt werden. Das Deployment enthält wie üblich ein Docker Image:

grep image apps/blog/base/deployment.yaml
        image: ghcr.io/ruedigerp/ink-blog.kuepper.nrw:v0.0.1-develop.1

Hier ist es egal welche Version eingetragen ist, da der Image Name und Tag je nach Stage ersetzt werden.

Denn im Apps Verzeichnis sind für jeden Cluster/Stage ein weiteres Verzeichnis, welches dann die Konfigurationen enthält, die auf dem Ziel Cluster genutzt werden sollen. Wie auch das Patch für die Images.

❯ grep image apps/blog/prod/deployment-patch.yaml
        image: ghcr.io/ruedigerp/ink-blog.kuepper.nrw:v1.9.31 # {"$imagepolicy": "blog:blog-policy"}
❯ grep image apps/blog/stage/deployment-patch.yaml
        image: ghcr.io/ruedigerp/ink-blog.kuepper.nrw:v1.9.32-stage.1 # {"$imagepolicy": "blog:blog-policy"}
❯ grep image apps/blog/dev/deployment-patch.yaml
        image: ghcr.io/ruedigerp/ink-blog.kuepper.nrw:v1.9.32-develop.2 # {"$imagepolicy": "blog:blog-policy"}

Aufgerufen werden sie durch die jeweiligen kustomization.yaml Files in den Cluster/Stage Verzeichnissen. Hier das Beispiel von prod:

❯ cat apps/blog/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../base

patches:
  - path: deployment-patch.yaml

namespace: blog

Das sagt Flux an der Stelle es soll alles aus dem Verzeichnis ../base anwenden, und das Patch-File für das Deployment anwenden. Hier können auch noch weitere Patches für alles mögliche, wie beispielsweise Änderungen am Ingress, Service, Secrets usw.

Der Patch für das Deployment sieht wie folgt aus:

❯ cat apps/blog/prod/deployment-patch.yaml
# apps/blog/stage/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: blog
  namespace: blog
spec:
  template:
    spec:
      containers:
      - name: blog
        image: ghcr.io/ruedigerp/ink-blog.kuepper.nrw:v1.9.31 # {"$imagepolicy": "blog:blog-policy"}

Der Patch nimmt das Deployment blog im Namespace blog und ersetzt das image welches in base/deployment.yaml gesetzt ist.

Wichtig ist in der deployment.yaml und deployment-patch-yaml der Kommentar hinter dem Image: # {"$imagepolicy": "blog:blog-policy"}.

Damit weiß ImageUpdateAutomoation von FluxCD welches Image er ersetzten soll und mit welcher Policy. In diesem Fall blog-policy im Namespace blog.

# clusters/production/flux-system/kustomizations/private/blog.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: blog
  namespace: flux-system
spec:
  dependsOn:
    - name: infrastructure
  interval: 5m
  prune: false
  path: ./apps/blog/prod
  sourceRef:
    kind: GitRepository
    name: flux-system
  targetNamespace: blog
  decryption:
    provider: sops
    secretRef:
      name: sops-age
# weitere Sachen wie Patch der Replica, ingress Domains usw. 
# ... 

Über weitere Kustomizations lade ich im Verzeichnis clusters/production noch viele weitere Sachen wie Helm Repos, Helm Charts, weitere Apps und auch die Konfigurationen der imageUpdates.

tree clusters/production/infrastructure/image-updater/
clusters/production/infrastructure/image-updater/
├── blog
│   ├── image-policy.yaml
│   ├── image-repository.yaml
│   ├── image-update-automation.yaml
│   └── kustomization.yaml
├── image-write-repo.yaml
├── kustomization.yaml
├── other-app
│   ├── image-policy.yaml
│   ├── image-repository.yaml
│   ├── image-update-automation.yaml
│   └── kustomization.yaml
└── second-other-app
    ├── image-policy.yaml
    ├── image-repository.yaml
    ├── image-update-automation.yaml
    └── kustomization.yaml

Die Image-Update-Automation benötigt für das prüfen auf neue Docker Images, das prüfen ob der Image-Tag für die Stage passt und die Automation selbst drei Konfigurationen. Das Image Repo konfigurieren:

# clusters/production/infrastructure/image-updater/blog/image-repository.yaml
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
  name: blog-repo
  namespace: blog
spec:
  image: ghcr.io/ruedigerp/ink-blog.kuepper.nrw
  interval: 10m
  provider: generic

Damit wird alle 10 Minuten geprüft, ob es ein neues Image gibt. Und die Policy kann da überprüfen welcher Tag auf die entsprechende Stage passt.

# clusters/production/infrastructure/image-updater/blog/image-policy.yaml
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: blog-policy
  namespace: blog  # Gleicher Namespace
spec:
  imageRepositoryRef:
    name: blog-repo  # Muss mit ImageRepository.name übereinstimmen
  filterTags:
    pattern: '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
  policy:
    semver:
      range: '>=1.0.0'    

Der filterTag ist entsprechend der Stages jeweils angepasst. Dafür liegt entsprechend des Clusters, bzw. der Stage, in /cluster/{prod,stage,dev}/infrastructure/image-updater/blog/image-policy.yaml die passende Policy.

Stage:

...
  filterTags:
    pattern: '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
  policy:
    semver:
      range: '>=1.0.0-stage' 

...

Dev:

  filterTags:
    pattern: '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
  policy:
    semver:
      range: '>=1.0.0-dev' 

Die Dateien sind ansonsten identisch.

Wenn es einen neuen Tag gibt, wird diese Änderung mit der ImageUpdateAutomation vorgenommen und ins GitOps Repo commited.

# clusters/production/infrastructure/image-updater/blog/image-update-automation.yaml
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
  name: blog-automation
  namespace: blog  # Gleicher Namespace
spec:
  sourceRef:
    kind: GitRepository
    name: flux-system-write
    namespace: flux-system
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: fluxcdbot@users.noreply.github.com
        name: fluxcdbot
      messageTemplate: |
        Automated image update

        Automation name: {{ .AutomationObject }}

        Images:
        {{- range .Updated.Images }}
        - {{.}}
        {{- end }}
    push:
      branch: main
  interval: 1m
  update:
    path: "./apps/blog/prod"  # Anpassen an deinen Pfad
    strategy: Setters

Damit wird dann in ./apps/blog/prod das image im deployment-patch.yaml geändert und ins Git Repo gepusched. Der Patch ändert dann beim reconcile der Kustomization das Deployment und applied es in den Cluster.

Ich muss daher keine Änderungen im GitOps Repo machen. Das passiert alles automatisch. Wenn man nach einem Docker Image Build nicht automatisch ausrollen möchte, kann man auch über einen PR die Änderungen machen lassen. So kann man neue Versionen erst ausrollen wenn man es möchte und hat mehr Kontrolle.

Rüdiger Küpper
DevOps Engineer & CISO @ Mogenius