Zum Inhalt

Helm Charts

Das Kamerplanter Helm-Chart basiert auf der bjw-s common library und definiert alle Kubernetes-Ressourcen in einem einzigen Chart. Container-Images und das Chart selbst werden als OCI-Artefakte über die GitHub Container Registry bereitgestellt.


Registry-Übersicht

Artefakt OCI-URL
Helm-Chart oci://ghcr.io/nolte/kamerplanter-helm/kamerplanter
Backend-Image ghcr.io/nolte/kamerplanter-backend
Frontend-Image ghcr.io/nolte/kamerplanter-frontend

Chart-Informationen

name: kamerplanter
type: application
version: 0.2.0          # Chart-Version (Helm-spezifisch)
appVersion: "1.0.0"     # Anwendungs-Version

Abhängigkeiten

Dependency Version Quelle Zweck
common (bjw-s) 4.6.2 bjw-s-labs Helm-Charts Library-Chart für einheitliche Kubernetes-Ressourcen
valkey 0.9.3 OCI: ghcr.io/valkey-io/valkey-helm Redis-kompatibler Cache + Celery-Broker

Chart-Struktur

helm/kamerplanter/
├── Chart.yaml            # Chart-Metadaten und Abhängigkeiten
├── Chart.lock            # Pinned Dependency-Versionen
├── values.yaml           # Standard-Werte (Produktion)
├── values-dev.yaml       # Override für Entwicklung
├── templates/
│   └── common.yaml       # bjw-s Library-Loader
└── charts/
    ├── common-4.6.2.tgz  # bjw-s Common Library
    └── valkey-0.9.3.tgz  # Valkey Sub-Chart

Das Chart nutzt den bjw-s common.loader.all-Ansatz: Alle Kubernetes-Ressourcen (Deployments, StatefulSets, Services, ConfigMaps, Ingress) werden deklarativ über values.yaml definiert — es gibt keine eigenen Templates.


Konfigurationsreferenz

Controller (Deployments & StatefulSets)

Backend

controllers:
  backend:
    type: deployment
    replicas: 2                    # Anpassbar
    strategy: RollingUpdate
    containers:
      main:
        image:
          repository: ghcr.io/nolte/kamerplanter-backend
          tag: latest              # In Produktion: feste Version verwenden
        env:
          ARANGODB_HOST: "..."
          ARANGODB_PORT: "8529"
          ARANGODB_DATABASE: "kamerplanter"
          ARANGODB_USERNAME: "root"
          ARANGODB_PASSWORD: "..."
          REDIS_URL: "redis://kamerplanter-valkey:6379/0"
          CORS_ORIGINS: '["..."]'
          DEBUG: "false"
          KAMERPLANTER_MODE: "light"    # oder "standard"
        resources:
          requests:
            cpu: 250m
            memory: 256Mi
          limits:
            cpu: "1"
            memory: 512Mi

Frontend

controllers:
  frontend:
    type: deployment
    replicas: 2
    containers:
      main:
        image:
          repository: ghcr.io/nolte/kamerplanter-frontend
          tag: latest
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi

Das Frontend wird hinter nginx ausgeliefert. Die nginx-Konfiguration wird automatisch als ConfigMap gemountet und leitet /api/-Anfragen an das Backend weiter.

ArangoDB

controllers:
  arangodb:
    type: statefulset
    replicas: 1                    # Single-Node (kein Cluster)
    containers:
      main:
        image:
          repository: arangodb
          tag: "3.11"
        env:
          ARANGO_ROOT_PASSWORD: "..."
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
          limits:
            cpu: "1"
            memory: 1Gi
    statefulset:
      volumeClaimTemplates:
        - name: data
          accessMode: ReadWriteOnce
          size: 5Gi                 # Anpassbar
          globalMounts:
            - path: /var/lib/arangodb3

Services

service:
  backend:
    controller: backend
    ports:
      http:
        port: 8000
  frontend:
    controller: frontend
    ports:
      http:
        port: 80
  arangodb:
    controller: arangodb
    ports:
      http:
        port: 8529

Ingress

ingress:
  main:
    enabled: true                   # Standard: deaktiviert
    hosts:
      - host: pflanzen.example.com
        paths:
          - path: /api
            pathType: Prefix
            service:
              identifier: backend
          - path: /
            pathType: Prefix
            service:
              identifier: frontend

TLS

Für HTTPS füge eine tls-Sektion hinzu und verwende z.B. cert-manager mit Let's Encrypt:

ingress:
  main:
    enabled: true
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod
    hosts:
      - host: pflanzen.example.com
        paths: [...]
    tls:
      - secretName: kamerplanter-tls
        hosts:
          - pflanzen.example.com

Valkey (Redis-kompatibler Cache)

valkey:
  dataStorage:
    enabled: true
    size: 1Gi

Umgebungsvariablen

Variable Pflicht Standard Beschreibung
ARANGODB_HOST Ja Hostname des ArangoDB-Service
ARANGODB_PORT Ja 8529 Port des ArangoDB-Service
ARANGODB_DATABASE Ja kamerplanter Datenbankname
ARANGODB_USERNAME Ja root Datenbank-Benutzer
ARANGODB_PASSWORD Ja Datenbank-Passwort
ARANGO_ROOT_PASSWORD Ja ArangoDB Root-Passwort (muss identisch mit ARANGODB_PASSWORD sein)
REDIS_URL Ja Valkey/Redis-Verbindungs-URL
CORS_ORIGINS Ja Erlaubte Origins als JSON-Array
DEBUG Nein false Debug-Modus aktivieren
KAMERPLANTER_MODE Nein light light (ohne Auth) oder standard (mit Auth)
REQUIRE_EMAIL_VERIFICATION Nein false E-Mail-Verifikation bei Registrierung

Entwicklungs-Overrides (values-dev.yaml)

Für die lokale Entwicklung existiert eine separate Values-Datei, die Skaffold automatisch verwendet:

Einstellung Produktion Entwicklung
Replicas (Backend/Frontend) 2 1
Update-Strategie RollingUpdate Recreate
DEBUG false true
Resource Limits Streng Großzügig
Frontend-Port 80 (nginx) 5173 (Vite Dev Server)
ArangoDB PVC 5 Gi 2 Gi
Ingress-Host (konfigurierbar) kamerplanter.local

Häufige Anpassungen

Ressourcen reduzieren (kleiner Cluster / Raspberry Pi)

controllers:
  backend:
    replicas: 1
    containers:
      main:
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
  frontend:
    replicas: 1
    containers:
      main:
        resources:
          requests:
            cpu: 50m
            memory: 64Mi
          limits:
            cpu: 250m
            memory: 128Mi
  arangodb:
    containers:
      main:
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi

Bestimmte Image-Version pinnen

controllers:
  backend:
    containers:
      main:
        image:
          tag: "1.2.3"    # Statt "latest"
  frontend:
    containers:
      main:
        image:
          tag: "1.2.3"

Image-Tags

Verwende in Produktion immer feste Versions-Tags statt latest. So stellst du sicher, dass ein helm upgrade die erwartete Version deployt.


Storage-Konfiguration (NFR-013)

Kamerplanter speichert alle Binärdaten (Fotos, Importe, Exporte) über einen austauschbaren Storage-Adapter. Die Wahl des Backends und die zugehörige Kubernetes-Persistenz werden vollständig über values.yaml gesteuert.

Local Filesystem (Standard)

Im Default-Betrieb legt das Chart automatisch das PVC backend-attachments an und mountet es in den Backend- und Celery-Worker-Pods unter /data/attachments.

storage:
  backend: local-fs                  # Standard; kein externes Storage nötig
  maxFileSizeMb: 25
  presignTtlSeconds: 900
  virusScan:
    enabled: false
    endpoint: ""

  localFs:
    root: /data/attachments           # Container-interner Mount-Pfad
    pvc:
      size: 100Gi
      accessMode: ReadWriteOnce       # Für Single-Replica (Standard)
      storageClass: ""                # Leer = Cluster-Default

Multi-Replica-Betrieb (Backend-Replicas > 1):

storage:
  localFs:
    pvc:
      accessMode: ReadWriteMany       # RWX-fähige StorageClass erforderlich
      storageClass: longhorn          # Oder: nfs, cephfs, etc.

Signing-Secret bei RWX zwingend

Bei mehr als einer Backend-Replica muss STORAGE_LOCALFS_SIGNING_SECRET als stabiles Kubernetes-Secret gesetzt sein. Ohne dieses Secret generiert jeder Pod ein eigenes ephemeres Signing-Secret — Token-Downloads schlagen fehl, wenn die Validierungsanfrage einen anderen Pod erreicht als die Signierung.

kubectl create secret generic kamerplanter-storage-signing \
  --from-literal=STORAGE_LOCALFS_SIGNING_SECRET="$(openssl rand -hex 32)" \
  --namespace kamerplanter

Im Chart über envFrom referenzieren:

controllers:
  backend:
    containers:
      main:
        envFrom:
          - secretRef:
              name: kamerplanter-storage-signing

S3-kompatibel (Production)

Nicht-geheime S3-Parameter werden direkt in values.yaml gesetzt. Die Credentials kommen ausschließlich aus dem External Secrets Operator (ESO) — nie als Klartext in Git.

storage:
  backend: s3
  maxFileSizeMb: 25
  presignTtlSeconds: 900

  s3:
    endpointUrl: https://s3.eu-central-1.amazonaws.com
    region: eu-central-1
    bucket: kamerplanter-prod
    usePathStyle: false               # true für MinIO und Nicht-AWS-Anbieter
    forceTls: true
    kmsKeyId: ""                      # Optional: Customer-Managed Key (SSE-KMS)

    # S3-Credentials via External Secrets Operator (NIEMALS Klartext)
    credentialsRef:
      secretName: storage-s3-credentials
      accessKeyIdKey: STORAGE_S3_ACCESS_KEY_ID
      secretAccessKeyKey: STORAGE_S3_SECRET_ACCESS_KEY

External Secrets Operator — ESO-Secret:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: storage-s3-credentials
  namespace: kamerplanter
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend              # Oder AWS Secrets Manager, etc.
    kind: ClusterSecretStore
  target:
    name: storage-s3-credentials
    creationPolicy: Owner
  data:
    - secretKey: STORAGE_S3_ACCESS_KEY_ID
      remoteRef:
        key: kamerplanter/storage
        property: access_key_id
    - secretKey: STORAGE_S3_SECRET_ACCESS_KEY
      remoteRef:
        key: kamerplanter/storage
        property: secret_access_key

Ohne ESO: manuelles Kubernetes-Secret

Wenn kein External Secrets Operator verfügbar ist, lege das Secret manuell an:

kubectl create secret generic storage-s3-credentials \
  --from-literal=STORAGE_S3_ACCESS_KEY_ID="dein-access-key" \
  --from-literal=STORAGE_S3_SECRET_ACCESS_KEY="dein-secret-key" \
  --namespace kamerplanter
Das Secret sollte aus einem sicheren Vault kommen und niemals in Git gespeichert werden.

NetworkPolicy für S3-Endpoints

Das Chart enthält eine NetworkPolicy, die ausgehende Verbindungen auf den konfigurierten S3-Endpunkt beschränkt und den Zugriff auf die Cloud-Metadata-IP (169.254.169.254) blockiert (SSRF-Schutz):

networkPolicies:
  storage:
    enabled: true
    blockMetadataEndpoint: true      # Blockiert 169.254.169.254 (Default: true)

MinIO im Cluster

storage:
  backend: s3
  s3:
    endpointUrl: http://minio.kamerplanter.svc:9000
    region: us-east-1
    bucket: kamerplanter
    usePathStyle: true
    forceTls: false
    allowPrivateEndpoint: true       # Erlaubt nicht öffentlich erreichbaren Endpunkt
    credentialsRef:
      secretName: storage-s3-credentials
      accessKeyIdKey: STORAGE_S3_ACCESS_KEY_ID
      secretAccessKeyKey: STORAGE_S3_SECRET_ACCESS_KEY

Virenscan (optional)

storage:
  virusScan:
    enabled: true
    endpoint: http://clamav-rest.kamerplanter.svc:9000

ClamAV muss als separates Deployment im Cluster laufen. Das Backend blockiert einen Upload, wenn der Scanner einen Fund meldet.

Häufige Provider-Konfigurationen

storage:
  backend: s3
  s3:
    endpointUrl: https://fsn1.your-objectstorage.com
    region: eu-central
    bucket: mein-kamerplanter-bucket
    usePathStyle: false
    forceTls: true
    credentialsRef:
      secretName: storage-s3-credentials
      accessKeyIdKey: STORAGE_S3_ACCESS_KEY_ID
      secretAccessKeyKey: STORAGE_S3_SECRET_ACCESS_KEY
storage:
  backend: s3
  s3:
    endpointUrl: https://<account-id>.r2.cloudflarestorage.com
    region: auto
    bucket: kamerplanter
    usePathStyle: false
    forceTls: true
    credentialsRef:
      secretName: storage-s3-credentials
      accessKeyIdKey: STORAGE_S3_ACCESS_KEY_ID
      secretAccessKeyKey: STORAGE_S3_SECRET_ACCESS_KEY
storage:
  backend: s3
  s3:
    endpointUrl: https://s3.eu-central-003.backblazeb2.com
    region: eu-central-003
    bucket: kamerplanter
    usePathStyle: false
    forceTls: true
    credentialsRef:
      secretName: storage-s3-credentials
      accessKeyIdKey: STORAGE_S3_ACCESS_KEY_ID
      secretAccessKeyKey: STORAGE_S3_SECRET_ACCESS_KEY

Siehe auch