MinIO war jahrelang die Standardantwort auf die Frage nach S3-kompatiblem Object Storage im eigenen Cluster. Seit Ende 2025 gilt das nur noch eingeschränkt: Die Community-Edition ist im Dezember 2025 in den Maintenance-Modus gewechselt, das öffentliche Repository wurde Anfang 2026 archiviert. Neue Funktionen entstehen ausschließlich in der kommerziellen Variante. Für viele Self-Hosting-Setups ist das der Anlass, sich nach einer dauerhaft quelloffenen Alternative umzusehen. SeaweedFS ist dafür ein ausgereifter Kandidat: Apache-2.0-lizenziert, aktiv weiterentwickelt und S3-kompatibel.

Dieser Beitrag zeigt anhand eines praxisnahen Ablaufs, wie SeaweedFS per Helm im HA-Setup auf Kubernetes installiert und bestehende Buckets anschließend per rclone-Job migriert werden – bis hin zum Cutover.

SeaweedFS in Kürze

Wo MinIO ein einziger, symmetrischer Server-Prozess ist, teilt SeaweedFS die Aufgaben auf mehrere Rollen auf: der Master koordiniert den Cluster (Leader-Election per Raft), die Volume Server halten die eigentlichen Daten, der Filer legt den Namespace darüber, und ein zustandsloses S3-Gateway stellt die S3-API bereit. Für ein Setup, das man an SeaweedFS anbindet, spricht man am Ende nur mit dem S3-Gateway – der Rest ist Innenleben.

Zwei Dinge entscheiden über echtes HA: Der Master ist erst mit drei Replicas hochverfügbar (Raft-Quorum), und der Filer braucht einen gemeinsamen Metadaten-Store (PostgreSQL, MySQL oder Redis) – mehrere Filer mit lokalem leveldb sind kein HA.

Installation von SeaweedFS per Helm

SeaweedFS bringt ein offizielles Helm-Chart mit, das die einzelnen Rollen als getrennte StatefulSets ausrollt. Das ist genau der Modus, den wir für ein HA-Setup brauchen. Zunächst das Chart-Repository einbinden:

helm repo add seaweedfs https://seaweedfs.github.io/seaweedfs/helm
helm repo update

Die eigentliche Arbeit steckt in den Values. Für ein hochverfügbares Setup bekommt jede Rolle drei Replicas und replicationPlacement legt fest, wie SeaweedFS die Daten über die Volume Server verteilt: Der Wert 001 sorgt dafür, dass jeder Chunk zusätzlich auf einem zweiten Server im selben Cluster liegt.

# seaweedfs-ha-values.yaml
global:
  replicationPlacement: "001"   # eine zusätzliche Kopie auf anderem Volume-Server

master:
  replicas: 3                   # 3 = Raft-Quorum, erst dann HA
  data: { type: persistentVolumeClaim, size: 5Gi, storageClass: standard }

volume:
  replicas: 3
  dataDirs:
    - name: data
      type: persistentVolumeClaim
      size: 100Gi               # an Datenmenge anpassen
      storageClass: standard

filer:
  replicas: 3
  # Für echtes HA: gemeinsamen Store (Postgres/MySQL/Redis) statt lokalem leveldb.
  data: { type: persistentVolumeClaim, size: 10Gi, storageClass: standard }

s3:
  enabled: true
  replicas: 3
  enableAuth: true

Die Größenangaben sind bewusst klein gehalten: Master und Filer speichern nur Metadaten, der Platzbedarf liegt also fast vollständig bei den Volume-Servern. Mit diesen Values lässt sich das Chart ausrollen:

helm upgrade --install seaweedfs seaweedfs/seaweedfs \
  --namespace storage --create-namespace \
  -f seaweedfs-ha-values.yaml

Der erste Start dauert einen Moment, weil die Master erst untereinander ein Quorum bilden müssen, bevor sich Volume Server und Filer anmelden können. Ein Blick auf die Pods zeigt, ob alle Rollen oben sind:

kubectl -n storage get pods -l app.kubernetes.io/name=seaweedfs

Sobald alles läuft, ist das S3-Gateway clusterintern unter http://seaweedfs-s3.storage.svc:8333 erreichbar – das ist die Adresse, die später sowohl der Migrations-Job als auch die Anwendung selbst verwendet.

Migration von MinIO

Der Ansatz: Beide Object Stores laufen parallel, die Daten werden in einem Wartungsfenster per rclone synchronisiert und verifiziert, dann stellt man die Anwendung um. MinIO bleibt als Fallback stehen. rclone passt hier gut, weil es beide Seiten als reines S3 spricht und mit sync idempotent arbeitet – ein abgebrochener Lauf lässt sich risikofrei wiederholen.

Vorab das Ziel-Bucket in SeaweedFS anlegen und die Anwendung read-only schalten, damit während der Migration nichts mehr nach MinIO geschrieben wird.

Das Manifest besteht aus zwei Teilen: Eine ConfigMap hält die rclone.conf mit beiden Endpunkten sowie das Migrationsskript, ein Job mountet beides und führt es aus. Beide Stores sind darin schlicht zwei S3-Remotes – der eigentliche Wechsel reduziert sich damit auf einen Kopiervorgang zwischen zwei Adressen:

# rclone-migration.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: rclone-migration-config
  namespace: storage
data:
  rclone.conf: |
    [minio]
    type = s3
    provider = Minio
    access_key_id = MINIO_ACCESS_KEY
    secret_access_key = MINIO_SECRET_KEY
    endpoint = http://minio.storage.svc:9000
    region = us-east-1

    [seaweedfs]
    type = s3
    provider = Other
    access_key_id = SEAWEEDFS_ACCESS_KEY
    secret_access_key = SEAWEEDFS_SECRET_KEY
    endpoint = http://seaweedfs-s3.storage.svc:8333
    region = us-east-1
    force_path_style = true
  migrate.sh: |
    #!/bin/sh
    set -e
    BUCKET="my-app-data"
    FLAGS="--transfers=16 --checkers=8 --checksum --retries=3 --stats=10s --stats-one-line"

    rclone sync  minio:$BUCKET seaweedfs:$BUCKET $FLAGS
    rclone check minio:$BUCKET seaweedfs:$BUCKET --one-way $FLAGS
    echo "Migration OK"
---
apiVersion: batch/v1
kind: Job
metadata:
  name: rclone-minio-to-seaweedfs
  namespace: storage
spec:
  backoffLimit: 2
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: rclone
          image: rclone/rclone:latest
          command: ["/bin/sh", "/scripts/migrate.sh"]
          volumeMounts:
            - { name: config,  mountPath: /config/rclone, readOnly: true }
            - { name: scripts, mountPath: /scripts,       readOnly: true }
      volumes:
        - name: config
          configMap:
            name: rclone-migration-config
            items: [{ key: rclone.conf, path: rclone.conf }]
        - name: scripts
          configMap:
            name: rclone-migration-config
            items: [{ key: migrate.sh, path: migrate.sh, mode: 0755 }]

Im Produktivbetrieb gehören die Access-Keys in ein Secret, nicht in die ConfigMap. Hier stehen sie zur besseren Lesbarkeit inline 😉

Ausgerollt wird der Job wie jedes andere Manifest; die Logs zeigen den Fortschritt live mit:

kubectl apply -f rclone-migration.yaml
kubectl -n storage logs -f job/rclone-minio-to-seaweedfs

rclone check vergleicht Quelle und Ziel und lässt den Job fehlschlagen, falls etwas nicht übertragen wurde – die Verifikation ist also direkt eingebaut.

Cutover: Anwendungskonfiguration auf den SeaweedFS-Endpoint umstellen (endpoint: http://seaweedfs-s3.storage.svc:8333, forcePathStyle: true – SeaweedFS erwartet Path-Style), Wartungsmodus deaktivieren, Smoke-Test fahren. MinIO noch ein bis zwei Wochen als Fallback stehen lassen, dann abbauen.

Worauf ist zu achten?

  • Path-Style: SeaweedFS adressiert per Path-Style. In rclone (force_path_style = true) und in der App (forcePathStyle: true) setzen, sonst laufen Requests ins Leere.
  • Filer-HA: Mehrere Filer-Replicas brauchen einen gemeinsamen Metadaten-Store – sonst driften die Namespaces auseinander.
  • S3-Randfälle: Multipart-Uploads und Presigned URLs verhalten sich zwischen Implementierungen leicht unterschiedlich. Vor dem produktiven Cutover im Staging testen.

Fazit

Die Migration ist kein Big-Bang, sondern ein planbarer Zwei-Schritt: HA-Setup per Helm, dann eine idempotente rclone-Migration mit eingebauter Prüfung. Weil beide Systeme reines S3 sprechen, bleibt der Eingriff auf Infrastrukturebene – die Anwendungslogik bleibt unangetastet. Und mit MinIO im Fallback verliert der Cutover seinen Schrecken.

Alle Beiträge von Philipp Kürsten

Philipp Kürsten arbeitet als Lead Consultant bei der OPITZ CONSULTING Deutschland GmbH. Er hat mehrjährige Projekterfahrung in der Applikationsentwicklung innerhalb von Microservice-Architekturen und beschäftigt sich mit modernen Architekturen für die gestiegenen Anforderungen im Zeitalter der Digitalisierung.

Schreibe einen Kommentar