Zum Inhalt springen

GitOps

GitOps mit Argo CD und Helm: Struktur und Image-Promotion

Wie du ein GitOps-Repository für viele Umgebungen strukturierst und neue Docker-Images automatisch in die richtige Umgebung bringst, mit Argo CD, Helm und GitHub Actions.

Christopher CEO · IT Architect

Aktualisiert am · 6 Min. Lesezeit

Bei GitOps beschreibt ein Git-Repository den gewünschten Zustand deiner Umgebungen, und ein Werkzeug wie Argo CD sorgt dafür, dass der Cluster diesem Zustand entspricht. Das klingt einfach, in der Praxis stellen sich aber schnell zwei Fragen: Wie strukturierst du das Repository für mehrere Umgebungen? Und wie kommt ein neues Docker-Image automatisch in die richtige Umgebung? In diesem Leitfaden zeigen wir, wie wir beides mit Argo CD und Helm lösen.

Eine Umgebung pro Repository, Branch oder Verzeichnis?

GitOps-Projekte mit Argo CD lassen sich auf drei Arten aufsetzen:

Strategie Vorteile Nachteile
Eine Umgebung pro Repository Saubere Trennung, eigene Berechtigungen pro Umgebung Änderungen über mehrere Umgebungen sind mühsam
Eine Umgebung pro Branch Vertrautes Git-Modell Branches driften auseinander, Merges zwischen Umgebungen werden fehleranfällig
Eine Umgebung pro Verzeichnis Änderungen an mehreren Umgebungen schnell möglich, alles auf einen Blick Berechtigungen müssen anders gelöst werden

Für die meisten Anwendungsfälle ist eine Umgebung pro Verzeichnis der beste Kompromiss. Darauf konzentrieren wir uns im Folgenden.

Die Verzeichnisstruktur

Für jede Umgebung gibt es im Repository ein eigenes Verzeichnis:

.
├── argocd
│   ├── project.yaml
│   └── repo-secrets.yaml
├── Chart.yaml
├── environments
│   ├── asia
│   │   ├── dev
│   │   ├── test
│   │   └── prod
│   └── eu
│       ├── dev
│       ├── test
│       │   ├── argocd
│       │   │   └── application.yaml
│       │   └── values.yaml
│       └── prod
└── templates
    └── _helpers.tpl

Arbeitest du mit Kustomize statt Helm, übernehmen overlays-Verzeichnisse dieselbe Rolle.

Sollen bestimmte Umgebungen aus Sicherheitsgründen getrennt sein, etwa Produktion vom Rest, verschiebst du deren Verzeichnisse in ein zweites Repository:

myapp-gitops-prod.git
myapp-gitops-dev-test.git

Mehrere Umgebungen in einem Commit

Mit einer Umgebung pro Verzeichnis kannst du mehrere Umgebungen in einem Commit ändern, etwa DEV und TEST gleichzeitig auf ein neues Image heben. Jede Umgebung ist eine eigene Application: Schlägt die Synchronisierung einer Umgebung fehl, bleiben die anderen davon unberührt, und ein Rollback ist ein einfaches git revert. Grundsätzlich spricht also nichts gegen solche Commits.

Willst du sie aus organisatorischen Gründen verhindern, geht das nur über einen Pre-Receive-Hook, der Änderungen auf ein Verzeichnis pro Commit beschränkt. GitHub Enterprise, Bitbucket Data Center und GitLab unterstützen solche Hooks, GitHub in der Cloud nicht.

Die Argo-CD-Application mit Helm

Helm kennt zwar Subcharts, in der Praxis ist es aber einfacher, ein einziges Helm-Chart für die Anwendung zu nutzen und pro Umgebung nur die values.yaml anzupassen. Diese liegt im Verzeichnis der jeweiligen Umgebung:

environments
└── eu
    └── prod
        ├── argocd
        │   └── application.yaml
        └── values.yaml

Das Application-Manifest verweist auf das Chart im Wurzelverzeichnis und auf die values.yaml der Umgebung:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-eu-prod
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: myapp
  source:
    repoURL: git@github.com:my-org/myapp-gitops.git
    targetRevision: HEAD
    # das Helm-Chart liegt im Wurzelverzeichnis des Repositorys
    path: .
    helm:
      # relativ zu spec.source.path
      valueFiles:
        - environments/eu/prod/values.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: myapp-prod

Zusätzliche Parameter über .argocd-source.yaml

Beim Deployment prüft Argo CD, ob im Verzeichnis spec.source.path die Dateien .argocd-source.yaml und .argocd-source-<appname>.yaml liegen. Ihre Parameter überschreiben die Werte aus der values.yaml, wobei die anwendungsspezifische Datei Vorrang hat:

helm:
  parameters:
    - name: image.tag
      value: 2025110713512263c634
      forceString: true

Diese Dateien nutzt zum Beispiel der Argo CD Image Updater, um den Image-Tag zu aktualisieren. Beachte: Sie liegen im Verzeichnis aus spec.source.path, in unserem Beispiel also im Wurzelverzeichnis, nicht im Verzeichnis der Umgebung.

Webhooks: nur die geänderte Umgebung aktualisieren

Löst GitHub den Webhook von Argo CD aus, prüft Argo CD standardmäßig alle Anwendungen, die auf das Repository zeigen. Mit der Annotation argocd.argoproj.io/manifest-generate-paths beschränkst du das auf die Pfade, die für eine Anwendung relevant sind:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-eu-prod
  annotations:
    argocd.argoproj.io/manifest-generate-paths: ./Chart.yaml;./templates;./environments/eu/prod;./.argocd-source-myapp-eu-prod.yaml

Jetzt wird myapp-eu-prod nur aktualisiert, wenn sich das Chart, die Umgebung oder ihre .argocd-source-Datei geändert hat.

Zwei Stolperfallen: Wenn du die Annotation nutzt, musst du die .argocd-source-Datei ausdrücklich mit angeben. Und die Annotation wirkt nur beim Webhook. Ein Sync über Oberfläche oder CLI ignoriert sie und aktualisiert auf den neuesten Commit.

Neue Images automatisch ausrollen

Bleibt die zweite Frage: Wie landet ein neu gebautes Image in der richtigen Umgebung? Wir nutzen zwei Wege.

Weg 1: Argo CD Image Updater

Der Argo CD Image Updater beobachtet deine Container-Registry und aktualisiert den Image-Tag, sobald ein neues Image erscheint, entweder direkt in der Application oder, besser nachvollziehbar, per Commit zurück ins Git-Repository.

Dafür braucht er einen eigenen Argo-CD-Account mit API-Token, RBAC-Rechte zum Lesen und Aktualisieren von Applications, Lesezugriff auf die Registry und zum Zurückschreiben Schreibzugriff auf das GitOps-Repository.

Hinweis: Die Konfiguration des Image Updaters hat sich zwischen den Versionen deutlich verändert. Halte dich an die Dokumentation genau der Version, die du einsetzt.

Der Image Updater passt gut, wenn du Image-Tags nach festen Regeln übernehmen willst, etwa „immer die neueste Patch-Version“.

Weg 2: Promotion aus der CI-Pipeline

Oft willst du mehr Kontrolle: Ein Image mit dem Tag *-dev soll nur DEV aktualisieren, eines mit *-stable nur PROD. Dann lässt du die CI-Pipeline des Anwendungsrepositorys das GitOps-Repository benachrichtigen. Auf GitHub geht das mit dem Event repository_dispatch.

1. Zugriff einrichten. Das Anwendungsrepository braucht Schreibzugriff auf genau ein Repository: das GitOps-Repository. Nutze dafür einen Fine-grained Personal Access Token oder eine GitHub App, beschränkt auf dieses eine Repository mit den Rechten Contents: Read and write und Metadata: Read-only. Hinterlege Token und Repository-Namen als Secrets, zum Beispiel GITOPS_REPO und GITOPS_REPO_TOKEN.

2. Event auslösen. Nachdem die Pipeline das Image gebaut und in die Registry geschoben hat, schickt sie das Event mit Branch und Image-Tag:

- name: Notify GitOps repository
  env:
    GH_TOKEN: ${{ secrets.GITOPS_REPO_TOKEN }}
  run: |
    gh api repos/${{ secrets.GITOPS_REPO }}/dispatches \
      -f event_type=update-gitops-repo \
      -f "client_payload[git_branch]=${GITHUB_REF_NAME}" \
      -f "client_payload[docker_image_tag]=${{ steps.publish.outputs.tag }}"

3. Im GitOps-Repository reagieren. Ein Workflow lauscht auf das Event, aktualisiert die passende values.yaml und committet die Änderung. Für die Zuordnung „welches Tag aktualisiert welche Umgebung“ haben wir die GitHub Action dreitier/conditional-regex-search-and-replace-action gebaut:

name: Update environment from upstream image

on:
  repository_dispatch:
    types: [update-gitops-repo]

jobs:
  update-values:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dreitier/conditional-regex-search-and-replace-action@main
        id: replace
        with:
          # -dev aktualisiert nur dev, -stable nur prod
          mappings: "docker_image_tag==.*-dev {THEN_UPDATE_FILES} **dev/values.yaml=docker_image_tag_regex&git_branch_regex {NEXT_MAPPING} docker_image_tag==.*-stable {THEN_UPDATE_FILES} **prod/values.yaml=docker_image_tag_regex&git_branch_regex"
          docker_image_tag: ${{ github.event.client_payload.docker_image_tag }}
          docker_image_tag_regex: 'imageTag: "(?<docker_image_tag>.*)"'
          git_branch: ${{ github.event.client_payload.git_branch }}
          git_branch_regex: 'git_branch: "(?<git_branch>.*)"'

      - name: Commit updated environment
        if: ${{ steps.replace.outputs.total_modified_files >= 1 }}
        uses: EndBug/add-and-commit@v9
        with:
          add: environments/*
          message: "Update to ${{ github.event.client_payload.docker_image_tag }} from ${{ github.event.client_payload.git_branch }}"

Argo CD übernimmt die Änderung anschließend automatisch oder, wenn GitHub den Webhook von Argo CD auslöst, sofort.

Fazit

Mit einer Umgebung pro Verzeichnis, einem gemeinsamen Helm-Chart und gezielten Webhooks bleibt dein GitOps-Repository übersichtlich, auch wenn die Zahl der Umgebungen wächst. Ob neue Images über den Image Updater oder über die CI-Pipeline kommen, hängt davon ab, wie viel Kontrolle du über die Promotion brauchst.

Du baust gerade eine Plattform mit Kubernetes und GitOps auf oder willst eine bestehende Struktur prüfen lassen? Genau das machen wir in unserer DevOps- und Kubernetes-Beratung.

Ursprünglich veröffentlicht am 9. November 2022.

Lass uns über dein Projekt sprechen.

Schreib uns kurz, wo es gerade hakt. Du bekommst eine ehrliche Antwort von einem echten Menschen, keinen Chatbot und keinen Vertriebsanruf.

Du bekommst innerhalb eines Werktags eine Antwort. Das Erstgespräch ist kostenlos und unverbindlich.