Remplacer MinIO par SeaweedFS — cadrage : trois bancs mesurés sur le homelab, SeaweedFS contre MinIO à charge égale, et le MinIO de prod est EN PANNE #48

Open
opened 2026-09-14 18:53:12 +02:00 by arcodange · 5 comments
Owner

Cadrage seulement. Aucun code ni aucune configuration de prod n'ont été modifiés : pas de Tofu distant, pas de Helm, pas d'ArgoCD. Le MinIO de prod n'a été qu'observé. Trois bancs jetables ont tourné dans le namespace essai-seaweedfs, puis ont été supprimés, PV vérifiés :

  • B1 (2026-09-14) : l'API S3 de SeaweedFS ;
  • B2 (2026-09-15) : provisionnement, charge, bascule et purge ;
  • B3 (2026-09-15) : MinIO de prod rejoué à charge égale, après l'arbitrage « mesurer MinIO d'abord ».

⚠⚠ Constat hors banc : le MinIO de prod est EN PANNE

Relevé le 2026-09-15 vers 09:24, en lecture seule (rien n'a été touché) :

  • Pod tools/minio-56975fd45-s8j8k sur pi2 : CrashLoopBackOff, 311 puis 312 redémarrages en 26 h.
  • Dernière sortie : code 1, reason: Error.
  • Journal du conteneur précédent : unable to rename (/export/.minio.sys/tmp -> /export/.minio.sys/tmp-old/…) drive is faulty, puis FATAL Unable to initialize backend: drive is faulty.
  • Volume Longhorn pvc-ebb2605f-0597-43ef-a3d0-6b3c7c8fe3b2 : attached, healthy, 3 réplicas running (pi1, pi2, pi3). Longhorn dit le volume sain ; c'est MinIO qui refuse son disque.
  • pi2 à 109-110 % de mémoire pendant tous les relevés.

Conséquence : aucune vidéo ne peut être stockée ni relue depuis au moins 26 h. C'est la même famille de symptômes que tools#31 et kadans-api#107 (Input/output error). Pour la même raison, la référence prod « au repos » n'existe pas : kubectl top pod → « Metrics not available », et le compte d'objets et d'octets est inaccessible (serveur éteint). Le diagnostic n'était pas dans le périmètre de ce cadrage (Q1).

TL;DR

  • MinIO communautaire est mort (archivé, plus d'images, plus de correctifs), et notre instance l'est aussi depuis au moins 26 h.
  • À charge égale sur le même Pi, SeaweedFS 4.46 et MinIO de prod font 0 échec tous les deux. SeaweedFS envoie un peu plus vite (24,8 contre 20,9 Mio/s), mais consomme ×2,1 de mémoire (1188 contre 567 Mi) et ×2 de CPU (547 contre 271 m). Détail en §4.
  • Le critère « < 512 Mi » écrit en §5.4 était trop sévère : MinIO lui-même le dépasse (567 Mi). L'écart réel est ×2,1, et SeaweedFS reste sous la limite de prod de MinIO (2 Gi).
  • L'API S3, le provisionnement Tofu idempotent, le parcours de bout en bout et la bascule sont mesurés verts pour SeaweedFS (§3, §5).
  • Recommandation révisée : SeaweedFS, avec quatre conditions de configuration et une limite mémoire de 1,5 Gi, hors pi2 (§4).
  • Arbitré : renommer MINIO_* en S3_* pendant la migration (lot M3).

1. Pourquoi

1.1 L'état réel de MinIO communautaire

Date Fait Source
mai 2025 Console d'administration retirée de l'édition communautaire vonng, « MinIO is Dead », 2025-12-04
2025-10-15 Plus d'images sur Docker Hub ni Quay, au moment d'un avis de sécurité (GHSA-jjjj-jwhf-8rgr) même source ; lobehub#9845
2025-12-03 « maintenance mode » minio/minio#21714
2026-02 README : « THIS REPOSITORY IS NO LONGER MAINTAINED », renvoi vers AIStor github.com/minio/minio
2026-04-25 Dépôt archivé, en lecture seule bandeau GitHub

1.2 Ce que coûte de rester figé

  • Aucun correctif de sécurité, alors que s3.arcodange.fr est public.
  • Des stubs jamais corrigés : PutBucketCors en 501, d'où un CORS global partagé entre toutes les apps.
  • Plus de version à suivre : le tag est à notre charge depuis que le chart est autonome (tools#31).
  • Aucun recours amont pour nos pannes : tools#31, le 403 ListObjectParts du 2026-09-07, kadans-api#107, et la panne « drive is faulty » d'aujourd'hui.

2. Inventaire de ce qui dépend de MinIO

Relevé par git grep -i minio : front 87de2fb2, kadans-api 7354087, kadans-jobs b7fe972, dossier 4f0c9d0, kadans-admin 218614e, tools 6913e5d.

2.1 Ce qui SURVIT : le client S3 standard

kadans-api garde minio-go v7 : les trois bancs l'utilisent, contre SeaweedFS comme contre MinIO.

Dépôt Fichier Usage
kadans-api stockage.go, internal/solo/stockage*.go PresignedPutObject, Presign (PUT de part), NewMultipartUpload, ListObjectParts, CompleteMultipartUpload, AbortMultipartUpload, PresignedGetObject, StatObject, GetObject, RemoveObject, ToErrorResponse
kadans-api outils/import-direct/main.go, outils/verifier-import/main.go StatObject, PutObject, FGetObject
front app/services/depot-video.ts PUT présigné par XHR, withCredentials = false
kadans-jobs cmd/worker/main.go lit par URL signée par l'API : aucun identifiant du magasin
kadans-admin aucune occurrence

2.2 Ce qui est SPÉCIFIQUE à MinIO et doit être réécrit ou renommé

  • tools
    • minio/ en entier : chart, values.yaml (MINIO_API_CORS_ALLOW_ORIGIN, MINIO_PROMETHEUS_AUTH_TYPE), sondes /minio/health/*.
    • minio/iac/* : provider aminueza/minio 3.3.0, root kvv2/minio/config, provisionneur admin:*.
    • minio/iac/modules/minio_app/* : clés MINIO_* dans kvv2/minio/<app>.
    • Workflows .gitea/workflows/minio.yaml et helmcharts.yaml.
    • hashicorp-vault/iac/modules/app_policy/main.tf et terraform.tfvars.
    • chart/values.yaml et doc/ce-que-traefik-voit.md.
  • kadans-api
    • Variables MINIO_ENDPOINT, MINIO_ENDPOINT_INTERNE, MINIO_BUCKET, MINIO_REGION, MINIO_ACCESS_KEY, MINIO_SECRET_KEY : stockage.go, main.go, outils/*, stockage_test.go.
    • Chart : deployment.yaml l.195-214, vaultstaticsecret-stockage.yaml (Secret kadans-api-minio, chemin minio/kadans), values.yaml l.208-285.
    • Textes et tests qui disent « MinIO » : AGENTS.md, README.md, features/stockage.feature, openapi.yaml, magasin_memoire*.go, garde_erreur_du_magasin_test.go, magasin_dit_son_erreur_test.go, rgpd.go, recette_octets*.go, vignette_rattrapage.go, dimensions.go, environnement.go, migrations 0011 et 0014 (commentaires), ADR-026, outils/import-collection/*, outils/collection/ffmpeg.go, les autres vaultstaticsecret-*.yaml.
  • front
    • iac/main.tf et iac/providers.tf.
    • scripts/check-cors-magasin.mjs : son explication et le piège mc cors get deviennent faux ; le préflight reste.
    • Commentaires et messages : app/services/depot-video.ts, app/lib/sauvegarde/{contrat-stockage,type-contenu}.ts, app/lib/video-storage.ts, app/composables/useReconciliation.ts, scripts/generer-contrat-stockage.mjs, tests/e2e/parcours-{import-reel,nouvel-appareil,televersement}.spec.ts, tests/e2e/parcours.config.ts, tests/fixtures/api-reelle.mjs, trois tests unitaires.
    • Doctrine : README.md, docs/STORAGE_STRATEGY.md, ADR 012, 013, 014, 018, 021, 022, docs/memoire/stockage/{sauvegarde,transport}.md, docs/cadrage/*, docs/notes/2026-07-13-*, CLAUDE.md.
  • dossier
    • 04-architecture/composants/{socle-homelab,api-coeur,README}.md, adr/0002-pipeline-video-protege.md, runbooks/mvp-bloc4-extrait-protege.md, 02-prd/{choix-anticipes,seuil-du-gratuit-mesure}.md, 02-prd/detail/24-sauvegarde-et-compte.md, 05-roadmap/{03,04,05}-*.md, 05-roadmap/plan/etat.json.

3. Matrice de compatibilité SeaweedFS 4.46

Légende : mesuré (B1, B2, B3) ; doc ; incertain ; absent.

Exigence Statut Preuve
PUT et GET présignés SigV4 mesuré B1 : 200, ETag, Content-Type enregistré, octets identiques ; URL altérée → 403 banc
PUT de part présigné, par client HTTP nu avec Origin de la PWA mesuré B1 et B2 : 200, ETag, ACAO exact banc
Multipart complet, dont ListObjectParts et Abort mesuré B1 et B2 : recollé dans l'ordre (sha256) ; terminé ou abandonné → NoSuchUpload 404 banc
StatObject / RemoveObject mesuré : taille et type ; puis 404 NoSuchKey banc
CORS par bucket mesuré B1 (préflight .fr et .lab, ETag exposé, origine étrangère → 403) ; B2 (posé par Tofu) banc ; wiki S3 CORS
⚠ CORS d'un bucket sans règle mesuré B2 : le défaut -s3.allowedOrigins=* accepte toute origine → à poser explicitement banc ; weed/command/server.go l.172
Droits par identité mesuré B1 et B2 : autre bucket → AccessDenied ; créer un bucket → AccessDenied ; GET anonyme → 403 banc
Granularité source : toutes les actions multipart se ramènent à Write (weed/iam/helpers.go) source
⚠ Noms d'actions IAM mesuré B2 : s3:ListMultipartUploadParts refusé (MalformedPolicyDocument) ; il faut s3:ListParts banc
API IAM en écriture mesuré : refusée avec -s3.config ; acceptée avec -s3.iam.readOnly=false ; admin amorcé par AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY banc ; s3api_embedded_iam.go l.2596
Provisionnement Tofu mesuré B2 : applyplan -detailed-exitcode = 0 « No changes »apply = 0 changement → encore No changes après redémarrage. ⚠ JonasKop/seaweedfs 0.2.0 absent du registre OpenTofu : registry.terraform.io/… obligatoire. hashicorp/aws pour CORS et cycle de vie (55 s pour la ressource cycle de vie) banc
Cycle de vie AbortIncompleteMultipartUpload mesuré en partie : règle acceptée et persistante ; run-shard avant l'échéance ne purge pas, ce qui est correct. Exécution à J+1 non observée (délai de 24 h en dur) banc ; constants_lifecycle_interval_*.go
Purge manuelle mesuré B2 : s3.clean.uploads -timeAgo 1m → envois inachevés 3 → 0 banc
⚠ Volumes et capacité mesuré B2 : volumes pré-alloués par collection. Deux épisodes de 500 « No writable volumes » (disque à 11 % puis 64 %). Les suppressions ne rendent rien avant volume.vacuum (Free 0 → 13) banc
Empreinte à charge égale mesuré B2 et B3, voir §4 : 1188 Mi, contre 567 Mi pour MinIO banc
Persistance Longhorn mesuré B1 (objet, CORS, cycle de vie) et B2 (identités dynamiques) après redémarrage banc
Métriques et sondes mesuré : 607 séries SeaweedFS_* ; /healthz → 200 banc
Chart Helm doc : officiel ; chart autonome probable (leçon de tools#31) k8s/charts
Maturité doc : une version par semaine environ ; Apache-2.0 releases

4. SeaweedFS contre MinIO à charge égale, alternatives et recommandation

4.1 À charge égale (B2 contre B3)

Même nœud serveur (pi3), même chargeur (pod alpine:3.20 sur pi1, même binaire), même Longhorn 5 Gi. Même charge : 8 envois de 320 Mio, 4 en parallèle, parts de 16 Mio, séquence de kadans-api (part présignée, PUT nu avec Origin, ListObjectParts, Complete, Stat, Remove). kubectl top toutes les 3 s.

SeaweedFS 4.46 (B2) MinIO RELEASE.2024-12-18T13-15-44Z (B3, image et config de prod)
Code de sortie / échecs 0 / 0 0 / 0
Octets envoyés, durée 2560 Mio en 103,4 s 2560 Mio en 122,3 s
Débit 24,8 Mio/s 20,9 Mio/s
PUT de part 16 Mio, p50 / p95 / max 2,18 s / 5,82 s / 8,03 s 2,89 s / 3,99 s / 5,10 s
NewMultipartUpload p50 / p95 2 ms / 6 ms 55 ms / 141 ms
ListObjectParts p50 / p95 / max 18 ms / 29 ms / 2,34 s 117 ms / 283 ms / 304 ms
CompleteMultipartUpload p50 / p95 / max 6 ms / 1,56 s / 4,54 s 23 ms / 30 ms / 31 ms
StatObject p50 / p95 2 ms / 17 ms 1 ms / 3 ms
RemoveObject p50 / p95 2 ms / 13 ms 6 ms / 11 ms
Mémoire du pod : repos → pic 130 Mi → 1188 Mi 75 Mi → 567 Mi
CPU du pod, pic 547 m 271 m
CPU de pi3, pic 1767 m 1194 m
Mémoire de pi3 pendant l'essai 62 % → 71 % 60 % → 61 %
Redémarrages 0 0
Parcours stockage.go (PUT nu, Origin, Complete, GET présigné, Remove) code 0, 7 OK code 0, 7 OK
Limite mémoire du banc 1536 Mi 2 Gi (celle de prod)

Lecture :

  • Aucun des deux ne décroche. SeaweedFS est plus rapide en médiane et en débit. MinIO est plus régulier : p95 et max plus bas sur les PUT de part et sur Complete.
  • SeaweedFS paie ×2,1 en mémoire et ×2 en CPU. Son pic (1188 Mi) reste sous la limite de prod de MinIO (2 Gi).
  • Le critère « < 512 Mi » était mal posé : écrit sans référence, MinIO lui-même le dépasse.
  • Écart de banc : MinIO a été chargé avec l'identité root ; SeaweedFS avec une identité applicative bornée (B2). L'évaluation d'une politique pèse peu face au transfert, mais l'écart est écrit.

4.2 La référence prod

Ce qui était demandé Relevé
kubectl top pod au repos, plusieurs fois impossible : 5 relevés à 15 s d'écart → Metrics not available (pod en CrashLoopBackOff)
Requêtes et limites image quay.io/minio/minio:RELEASE.2024-12-18T13-15-44Z ; requests cpu: 100m, memory: 512Mi ; limits memory: 2Gi (aucune limite CPU) ; PVC 50 Gi
Objets et octets de kadans-videos inaccessibles : serveur éteint. Dernier chiffre connu : ~7,7 Go le 2026-08-07 (kadans-api#107)

4.3 Les alternatives

Garage RustFS Cloudflare R2
PUT de part présigné (notre chemin principal) incertain incertain incertain : la doc ne cite pas UploadPart
Multipart, CORS par bucket, cycle de vie Abort doc incertain doc
Droits une clé × un bucket IAM et politiques jetons d'API
Maturité v2.4.1 stable 1.0.0-rc.6 (2026-09-11), prerelease commercial ; ADR-012 : aucun seuil atteint
Mesuré ici non non non

4.4 Recommandation révisée

SeaweedFS. À charge égale, il fait le même travail sans échec, un peu plus vite. Il règle deux dettes : le CORS global partagé et les stubs 501. Il remplace un logiciel mort dont l'instance est tombée. Son surcoût mémoire est réel (×2,1), mais tient sous la limite qu'on donnait déjà à MinIO. Conditions, toutes mesurées comme nécessaires :

  1. -s3.allowedOrigins explicite, jamais *, plus un CORS par bucket posé par Tofu.
  2. -s3.iam.readOnly=false et admin amorcé depuis Vault. Politiques en noms SeaweedFS (s3:ListParts), provider épinglé registry.terraform.io/….
  3. Capacité : -volume.max et -master.volumeSizeLimitMB dimensionnés sur le PVC, plus un CronJob volume.vacuum.
  4. Filet de purge : un CronJob s3.clean.uploads -timeAgo 24h.
  5. Ressources : requests 512Mi, limit 1536Mi à 2Gi, et nodeSelector sur pi1 ou pi3, jamais pi2 (109-111 % de mémoire à chaque relevé, et nœud du MinIO en panne).

5. Les bancs

5.1 B1 (2026-09-14) : l'API S3

weed server tout-en-un sur pi3, Longhorn 2 Gi, identités en fichier, accès par port-forward.

Passage Code Résultat
Nominal (minio-go v7.2.1) 0 38 OK
Sabotage : faux secret 28 403 et SignatureDoesNotMatch ; ⚠ le port-forward meurt quand le serveur refuse un corps en cours d'envoi (KO suivants = harnais)
Persistance / témoin 0 / 1 relu / KO attendu
IAM en écriture avec -s3.config 254 écritures désactivées

Créé puis supprimé : ns essai-seaweedfs, Secret seaweedfs-s3, PVC seaweedfs-banc (pvc-59a2afd6-…), Deployment. delete namespace → 0 ; PV et volume Longhorn → NotFound ; diff des PV → 0.

5.2 B2 (2026-09-15) : provisionnement, charge, bascule, purge

Montage : weed server -s3 -s3.iam.readOnly=false, Longhorn 5 Gi, pi3 ; chargeur sur pi1.

Provisionnement (OpenTofu 1.10.5, état local) :

Étape Code Sortie
init JonasKop/seaweedfs 1 absent de registry.opentofu.org
init registry.terraform.io/JonasKop/seaweedfs + hashicorp/aws 5.100.0 0 installés, signés
apply n° 1 1 MalformedPolicyDocument: not a valid action: 'ListMultipartUploadParts'
apply n° 2 (s3:ListParts) 0 1 ajoutée
plan -detailed-exitcode 0 No changes
apply n° 3 0 0 changement
plan après redémarrage 0 No changes

Charge. Essai n° 1, code 146 : 146 PUT en 500, dus au dimensionnement de volumes du harnais (-volume.max=8). Essai n° 2, code 0 : chiffres au §4.1.

Parcours et sabotage. Nominal : code 0. Faux secret : code 8 (403 SignatureDoesNotMatch, puis port-forward mort). Cloison : AccessDenied ×2. Bucket sans règle CORS : code 1 (origine étrangère acceptée).

Bascule.

  • URL de l'ancien magasin : clé inconnue → 403 InvalidAccessKeyId ; même clé, autre secret → 403 SignatureDoesNotMatch. Tous portent l'ACAO, donc le XHR lit bien 403.
  • uploadId de l'ancien magasin → NoSuchUpload 404.

Côté front (lecture de code, ea56aac5) :

  • depot-video.ts l.120-128 transforme ce 403 en ErreurApiCoeur(403), que classerEchec (moteur-sauvegarde.ts l.169) range en serveur, donc rejoué en backoff.
  • Au rejeu, televersementId est renvoyé (l.322-326). L'API reçoit NoSuchUpload et rouvre un envoi, et le front remet les parts à zéro (l.332-337).
  • Rattrapage en un essai, à une condition : l'API signe avec les nouvelles clés dans le même geste que la bascule de l'hôte.

Purge.

  • Envois posés : code 1 (le 3ᵉ en 500, volumes pleins) ; ListMultipartUploads = 3.
  • s3.lifecycle.run-shard avant l'échéance : 0, toujours 3.
  • s3.clean.uploads -timeAgo 1m : 0, puis 0 envoi inachevé.
  • volume.vacuum + volume.deleteEmpty : Free 0 → 13, disque 64 % → 0 %.

Créé puis supprimé : ns, Secret seaweedfs-admin, PVC seaweedfs-banc 5 Gi (pvc-87fa5604-…), Deployment et Service seaweedfs, Pod chargeur (deux fois), buckets et identités internes au banc. delete namespace → 0 ; PV et volume Longhorn → NotFound ; diff des PV → 0 ; 0 pod restant.

5.3 B3 (2026-09-15) : la référence MinIO

Montage, calqué sur tools/minio/templates/deployment.yaml et values.yaml (6913e5d, lus en lecture seule) :

  • Identique à la prod : image, commande minio server /export -S /etc/minio/certs/ --address :9000 --console-address :9001, MINIO_PROMETHEUS_AUTH_TYPE=public, MINIO_API_CORS_ALLOW_ORIGIN (les deux origines), sondes startup, readiness et liveness, securityContext 1000:1000, requests 512Mi/100m, limit 2Gi.
  • Écarts : nodeSelector pi3 (comme SeaweedFS) ; PVC Longhorn 5 Gi au lieu de 50 ; Secret root local (sans Vault ni ServiceAccount).
Passage Code Résultat
create-bucket kadans-videos 0
Charge (même binaire, mêmes paramètres, depuis pi1) 0 0 échec ; chiffres au §4.1
Parcours stockage.go 0 7 OK, dont PUT nus 200 avec ACAO, GET présigné sha identique, Stat 404 après Remove

Créé puis supprimé :

kubectl --context=default apply -f minio-banc.yaml   # ns essai-seaweedfs, PVC minio-banc (longhorn 5Gi), Deployment minio (pi3), Service minio
kubectl --context=default -n essai-seaweedfs create secret generic minio-banc --from-literal=rootUser=… --from-literal=rootPassword=…
kubectl --context=default apply -f chargeur.yaml     # Pod chargeur (alpine:3.20, pi1)
kubectl --context=default -n essai-seaweedfs port-forward svc/minio 39000:9000
kubectl --context=default delete namespace essai-seaweedfs --wait=true --timeout=300s     # code 0
kubectl --context=default get ns essai-seaweedfs                                          # NotFound
kubectl --context=default get pv pvc-439e7a80-ebf1-4ba4-bfe0-f4d6637f3293                 # NotFound
kubectl --context=default -n longhorn-system get volumes.longhorn.io pvc-439e7a80-ebf1-4ba4-bfe0-f4d6637f3293  # NotFound
diff pv-avant.txt pv-apres.txt                                                            # code 0

Prod, lecture seule : get deploy, get pod, top pod (×5), get events, logs --previous, get pvc, get volumes.longhorn.io et replicas.longhorn.io. Aucune écriture, aucun redémarrage, aucun port-forward vers la prod. ArgoCD, Vault, DNS et Cloudflare intacts.

5.4 Ce qui n'a pas été mesuré

  • L'exécution réelle de l'Abort multipart par le worker de cycle de vie, à J+1.
  • La copie rclone et son débit.
  • kadans-api elle-même déployée contre l'essai (seule sa séquence d'appels a été rejouée).
  • La référence prod au repos (serveur en panne).

6. Plan de migration en lots

👤 = exige un humain.

Lot Contenu Humain
M0 — la panne de prod (hors migration, préalable) Décider : réparer MinIO, ou accélérer M1-M5 (Q1). Tant que MinIO ne démarre pas, les objets existants (~7,7 Go) ne sont lisibles par aucune voie S3 : M4 en dépend 👤 fondateur
M1 — le serveur à côté Chart tools/seaweedfs/ autonome, avec toutes les conditions du §4.4 (allowedOrigins, iam.readOnly, volumes, vacuum, clean.uploads, 512Mi/1536Mi-2Gi, hors pi2). Service interne seulement 👤 synchronisation ArgoCD si elle n'est pas automatique
M2 — le provisionnement Pipeline seaweedfs : admin dans kvv2/seaweedfs/config. Module s3_app : registry.terraform.io/JonasKop/seaweedfs pour bucket, identité, clé et politique (s3:ListParts) ; hashicorp/aws pour CORS et cycle de vie. Secret écrit dans kvv2/s3/<app> avec des clés S3_*. app_policy : nouveaux chemins 👤 apply Vault par OIDC : hashicorp-vault, seaweedfs, front/iac
M3renommage MINIO_*S3_* (arbitré le 2026-09-15) kadans-api lit S3_ENDPOINT, S3_ENDPOINT_INTERNE, S3_BUCKET, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEY avec repli sur MINIO_* le temps de la bascule, retiré en M6. Idem pour outils/import-direct et outils/verifier-import. Chart : Secret kadans-api-s3, VaultStaticSecrets3/kadans, endpointInterne → SeaweedFS. Tests (stockage_test.go, TestMagasinMemoireNEcrasePasMinio), textes et doctrine renommés dans les quatre dépôts, dont check-cors-magasin.mjs et CLAUDE.md du front
M4 — la copie Job rclone sync puis rclone check, comptes d'objets et d'octets des deux côtés. Exige un MinIO qui démarre (M0) 👤 lancer et lire le rapport
M5 — la bascule Ingress s3.arcodange.fr et .lab vers SeaweedFS dans le même geste que le redéploiement de kadans-api sur les nouvelles clés. Contrôles : bun run check:cors, bun run test:import-reel 👤 Cloudflare si le tunnel ou le DNS change
M6 — le retrait Suppression du repli MINIO_*, du chart, du module, du provider, du workflow et de kvv2/minio/* 👤 secrets Vault ; PVC MinIO (Prune=false) supprimé à la main

Retour arrière, jusqu'à M6 : ingress et VaultStaticSecret vers MinIO (s'il démarre), plus rclone copy inverse. Les masters restent sur les appareils, et la file ne retire aucune tâche : au pire, le moteur redépose.


7. Questions ouvertes au fondateur

  1. Q1 — la panne de prod. MinIO ne démarre plus (« drive is faulty »), alors que Longhorn dit le volume sain. Ouvrir un incident à part pour le diagnostiquer et récupérer les ~7,7 Go, ou traiter la migration comme la réparation ?
  2. Q2 — la limite mémoire de SeaweedFS : 1536 Mi (pic mesuré + 30 %) ou 2 Gi (la limite actuelle de MinIO) ?

(Tranchés le 2026-09-15 : « mesurer MinIO d'abord » → §4.1 ; renommer MINIO_* en S3_* → M3.)


Renvoi : tools#31, tools#37, kadans-api#107, kadans-api#217, incident du 2026-09-07 (front/docs/memoire/stockage/sauvegarde.md#droit-manque-magasin).

🤖 Generated with Claude Code

https://claude.ai/code/session_01FDDndoBCcpef952NdHDgaQ

> **Cadrage seulement.** Aucun code ni aucune configuration de prod n'ont été modifiés : pas de Tofu distant, pas de Helm, pas d'ArgoCD. Le MinIO de prod n'a été **qu'observé**. Trois bancs **jetables** ont tourné dans le namespace `essai-seaweedfs`, puis ont été **supprimés**, PV vérifiés : > - B1 (2026-09-14) : l'API S3 de SeaweedFS ; > - B2 (2026-09-15) : provisionnement, charge, bascule et purge ; > - B3 (2026-09-15) : MinIO de prod rejoué à charge égale, après l'arbitrage « mesurer MinIO d'abord ». ## ⚠⚠ Constat hors banc : le MinIO de prod est EN PANNE Relevé le 2026-09-15 vers 09:24, **en lecture seule** (rien n'a été touché) : - **Pod** `tools/minio-56975fd45-s8j8k` sur **pi2** : **`CrashLoopBackOff`, 311 puis 312 redémarrages en 26 h**. - **Dernière sortie** : code 1, `reason: Error`. - **Journal du conteneur précédent** : `unable to rename (/export/.minio.sys/tmp -> /export/.minio.sys/tmp-old/…) drive is faulty`, puis **`FATAL Unable to initialize backend: drive is faulty`**. - **Volume Longhorn** `pvc-ebb2605f-0597-43ef-a3d0-6b3c7c8fe3b2` : `attached`, **`healthy`**, 3 réplicas `running` (pi1, pi2, pi3). **Longhorn dit le volume sain ; c'est MinIO qui refuse son disque.** - **pi2** à **109-110 % de mémoire** pendant tous les relevés. **Conséquence :** aucune vidéo ne peut être stockée ni relue depuis au moins 26 h. C'est la même famille de symptômes que tools#31 et kadans-api#107 (`Input/output error`). Pour la même raison, **la référence prod « au repos » n'existe pas** : `kubectl top pod` → « Metrics not available », et le compte d'objets et d'octets est inaccessible (serveur éteint). Le diagnostic n'était pas dans le périmètre de ce cadrage (Q1). ## TL;DR - **MinIO communautaire est mort** (archivé, plus d'images, plus de correctifs), **et notre instance l'est aussi** depuis au moins 26 h. - **À charge égale sur le même Pi, SeaweedFS 4.46 et MinIO de prod font 0 échec tous les deux.** SeaweedFS envoie un peu plus vite (24,8 contre 20,9 Mio/s), mais consomme **×2,1 de mémoire** (1188 contre 567 Mi) et **×2 de CPU** (547 contre 271 m). Détail en §4. - **Le critère « < 512 Mi » écrit en §5.4 était trop sévère** : MinIO lui-même le dépasse (567 Mi). L'écart réel est ×2,1, et SeaweedFS reste **sous la limite de prod de MinIO (2 Gi)**. - **L'API S3, le provisionnement Tofu idempotent, le parcours de bout en bout et la bascule** sont mesurés verts pour SeaweedFS (§3, §5). - **Recommandation révisée : SeaweedFS**, avec quatre conditions de configuration et une limite mémoire de 1,5 Gi, hors pi2 (§4). - **Arbitré** : renommer `MINIO_*` en `S3_*` pendant la migration (lot M3). --- ## 1. Pourquoi ### 1.1 L'état réel de MinIO communautaire | Date | Fait | Source | |---|---|---| | mai 2025 | Console d'administration retirée de l'édition communautaire | [vonng, « MinIO is Dead », 2025-12-04](https://blog.vonng.com/en/db/minio-is-dead/) | | 2025-10-15 | Plus d'images sur Docker Hub ni Quay, au moment d'un avis de sécurité (GHSA-jjjj-jwhf-8rgr) | même source ; [lobehub#9845](https://github.com/lobehub/lobehub/issues/9845) | | 2025-12-03 | « maintenance mode » | [minio/minio#21714](https://github.com/minio/minio/issues/21714) | | 2026-02 | README : « THIS REPOSITORY IS NO LONGER MAINTAINED », renvoi vers AIStor | [github.com/minio/minio](https://github.com/minio/minio) | | 2026-04-25 | Dépôt **archivé**, en lecture seule | bandeau GitHub | ### 1.2 Ce que coûte de rester figé - **Aucun correctif de sécurité**, alors que `s3.arcodange.fr` est **public**. - **Des stubs jamais corrigés** : `PutBucketCors` en 501, d'où un CORS global partagé entre toutes les apps. - **Plus de version à suivre** : le tag est à notre charge depuis que le chart est autonome (tools#31). - **Aucun recours amont** pour nos pannes : tools#31, le 403 `ListObjectParts` du 2026-09-07, kadans-api#107, et la panne « drive is faulty » d'aujourd'hui. --- ## 2. Inventaire de ce qui dépend de MinIO Relevé par `git grep -i minio` : front `87de2fb2`, kadans-api `7354087`, kadans-jobs `b7fe972`, dossier `4f0c9d0`, kadans-admin `218614e`, tools `6913e5d`. ### 2.1 Ce qui SURVIT : le client S3 standard **`kadans-api` garde minio-go v7** : les trois bancs l'utilisent, contre SeaweedFS comme contre MinIO. | Dépôt | Fichier | Usage | |---|---|---| | kadans-api | `stockage.go`, `internal/solo/stockage*.go` | `PresignedPutObject`, `Presign` (PUT de part), `NewMultipartUpload`, `ListObjectParts`, `CompleteMultipartUpload`, `AbortMultipartUpload`, `PresignedGetObject`, `StatObject`, `GetObject`, `RemoveObject`, `ToErrorResponse` | | kadans-api | `outils/import-direct/main.go`, `outils/verifier-import/main.go` | `StatObject`, `PutObject`, `FGetObject` | | front | `app/services/depot-video.ts` | PUT présigné par XHR, `withCredentials = false` | | kadans-jobs | `cmd/worker/main.go` | lit par URL signée par l'API : aucun identifiant du magasin | | kadans-admin | — | aucune occurrence | ### 2.2 Ce qui est SPÉCIFIQUE à MinIO et doit être réécrit ou renommé - **tools** - `minio/` en entier : chart, `values.yaml` (`MINIO_API_CORS_ALLOW_ORIGIN`, `MINIO_PROMETHEUS_AUTH_TYPE`), sondes `/minio/health/*`. - `minio/iac/*` : provider `aminueza/minio` 3.3.0, root `kvv2/minio/config`, provisionneur `admin:*`. - `minio/iac/modules/minio_app/*` : clés `MINIO_*` dans `kvv2/minio/<app>`. - Workflows `.gitea/workflows/minio.yaml` et `helmcharts.yaml`. - `hashicorp-vault/iac/modules/app_policy/main.tf` et `terraform.tfvars`. - `chart/values.yaml` et `doc/ce-que-traefik-voit.md`. - **kadans-api** - Variables `MINIO_ENDPOINT`, `MINIO_ENDPOINT_INTERNE`, `MINIO_BUCKET`, `MINIO_REGION`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY` : `stockage.go`, `main.go`, `outils/*`, `stockage_test.go`. - Chart : `deployment.yaml` l.195-214, `vaultstaticsecret-stockage.yaml` (Secret `kadans-api-minio`, chemin `minio/kadans`), `values.yaml` l.208-285. - Textes et tests qui disent « MinIO » : `AGENTS.md`, `README.md`, `features/stockage.feature`, `openapi.yaml`, `magasin_memoire*.go`, `garde_erreur_du_magasin_test.go`, `magasin_dit_son_erreur_test.go`, `rgpd.go`, `recette_octets*.go`, `vignette_rattrapage.go`, `dimensions.go`, `environnement.go`, migrations 0011 et 0014 (commentaires), ADR-026, `outils/import-collection/*`, `outils/collection/ffmpeg.go`, les autres `vaultstaticsecret-*.yaml`. - **front** - `iac/main.tf` et `iac/providers.tf`. - `scripts/check-cors-magasin.mjs` : son explication et le piège `mc cors get` deviennent faux ; le préflight reste. - Commentaires et messages : `app/services/depot-video.ts`, `app/lib/sauvegarde/{contrat-stockage,type-contenu}.ts`, `app/lib/video-storage.ts`, `app/composables/useReconciliation.ts`, `scripts/generer-contrat-stockage.mjs`, `tests/e2e/parcours-{import-reel,nouvel-appareil,televersement}.spec.ts`, `tests/e2e/parcours.config.ts`, `tests/fixtures/api-reelle.mjs`, trois tests unitaires. - Doctrine : `README.md`, `docs/STORAGE_STRATEGY.md`, ADR 012, 013, 014, 018, 021, 022, `docs/memoire/stockage/{sauvegarde,transport}.md`, `docs/cadrage/*`, `docs/notes/2026-07-13-*`, `CLAUDE.md`. - **dossier** - `04-architecture/composants/{socle-homelab,api-coeur,README}.md`, `adr/0002-pipeline-video-protege.md`, `runbooks/mvp-bloc4-extrait-protege.md`, `02-prd/{choix-anticipes,seuil-du-gratuit-mesure}.md`, `02-prd/detail/24-sauvegarde-et-compte.md`, `05-roadmap/{03,04,05}-*.md`, `05-roadmap/plan/etat.json`. --- ## 3. Matrice de compatibilité SeaweedFS 4.46 Légende : **mesuré** (B1, B2, B3) ; **doc** ; **incertain** ; **absent**. | Exigence | Statut | Preuve | |---|---|---| | PUT et GET présignés SigV4 | **mesuré** B1 : 200, ETag, `Content-Type` enregistré, octets identiques ; URL altérée → 403 | banc | | PUT de part présigné, par client HTTP nu avec `Origin` de la PWA | **mesuré** B1 et B2 : 200, ETag, ACAO exact | banc | | Multipart complet, dont `ListObjectParts` et `Abort` | **mesuré** B1 et B2 : recollé dans l'ordre (sha256) ; terminé ou abandonné → `NoSuchUpload` 404 | banc | | `StatObject` / `RemoveObject` | **mesuré** : taille et type ; puis 404 `NoSuchKey` | banc | | CORS par bucket | **mesuré** B1 (préflight `.fr` et `.lab`, ETag exposé, origine étrangère → 403) ; B2 (posé par Tofu) | banc ; [wiki S3 CORS](https://github.com/seaweedfs/seaweedfs/wiki/S3-CORS) | | ⚠ CORS d'un bucket **sans** règle | **mesuré** B2 : le défaut `-s3.allowedOrigins=*` **accepte toute origine** → à poser explicitement | banc ; `weed/command/server.go` l.172 | | Droits par identité | **mesuré** B1 et B2 : autre bucket → `AccessDenied` ; créer un bucket → `AccessDenied` ; GET anonyme → 403 | banc | | Granularité | **source** : toutes les actions multipart se ramènent à `Write` (`weed/iam/helpers.go`) | source | | ⚠ Noms d'actions IAM | **mesuré** B2 : `s3:ListMultipartUploadParts` refusé (`MalformedPolicyDocument`) ; il faut **`s3:ListParts`** | banc | | API IAM en écriture | **mesuré** : refusée avec `-s3.config` ; acceptée avec **`-s3.iam.readOnly=false`** ; admin amorcé par `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | banc ; `s3api_embedded_iam.go` l.2596 | | Provisionnement Tofu | **mesuré** B2 : `apply` → `plan -detailed-exitcode` = **0 « No changes »** → `apply` = 0 changement → encore No changes après redémarrage. ⚠ [`JonasKop/seaweedfs` 0.2.0](https://github.com/JonasKop/terraform-provider-seaweedfs) absent du registre OpenTofu : `registry.terraform.io/…` obligatoire. `hashicorp/aws` pour CORS et cycle de vie (55 s pour la ressource cycle de vie) | banc | | Cycle de vie `AbortIncompleteMultipartUpload` | **mesuré en partie** : règle acceptée et persistante ; `run-shard` avant l'échéance ne purge pas, ce qui est correct. **Exécution à J+1 non observée** (délai de 24 h en dur) | banc ; `constants_lifecycle_interval_*.go` | | Purge manuelle | **mesuré** B2 : `s3.clean.uploads -timeAgo 1m` → envois inachevés 3 → 0 | banc | | ⚠ Volumes et capacité | **mesuré** B2 : volumes pré-alloués par collection. Deux épisodes de 500 « No writable volumes » (disque à 11 % puis 64 %). Les suppressions ne rendent rien avant `volume.vacuum` (Free 0 → 13) | banc | | **Empreinte à charge égale** | **mesuré** B2 et B3, voir §4 : **1188 Mi, contre 567 Mi pour MinIO** | banc | | Persistance Longhorn | **mesuré** B1 (objet, CORS, cycle de vie) et B2 (identités dynamiques) après redémarrage | banc | | Métriques et sondes | **mesuré** : 607 séries `SeaweedFS_*` ; `/healthz` → 200 | banc | | Chart Helm | **doc** : officiel ; chart autonome probable (leçon de tools#31) | [k8s/charts](https://github.com/seaweedfs/seaweedfs/tree/master/k8s/charts/seaweedfs) | | Maturité | **doc** : une version par semaine environ ; Apache-2.0 | [releases](https://api.github.com/repos/seaweedfs/seaweedfs/releases) | --- ## 4. SeaweedFS contre MinIO à charge égale, alternatives et recommandation ### 4.1 À charge égale (B2 contre B3) Même nœud serveur (**pi3**), même chargeur (pod `alpine:3.20` sur **pi1**, même binaire), même Longhorn 5 Gi. Même charge : **8 envois de 320 Mio, 4 en parallèle, parts de 16 Mio**, séquence de kadans-api (part présignée, PUT nu avec `Origin`, `ListObjectParts`, `Complete`, `Stat`, `Remove`). `kubectl top` toutes les 3 s. | | **SeaweedFS 4.46** (B2) | **MinIO `RELEASE.2024-12-18T13-15-44Z`** (B3, image et config de prod) | |---|---|---| | Code de sortie / échecs | **0 / 0** | **0 / 0** | | Octets envoyés, durée | 2560 Mio en 103,4 s | 2560 Mio en 122,3 s | | **Débit** | **24,8 Mio/s** | 20,9 Mio/s | | PUT de part 16 Mio, p50 / p95 / max | **2,18 s** / 5,82 s / 8,03 s | 2,89 s / **3,99 s** / 5,10 s | | `NewMultipartUpload` p50 / p95 | **2 ms / 6 ms** | 55 ms / 141 ms | | `ListObjectParts` p50 / p95 / max | **18 ms / 29 ms** / 2,34 s | 117 ms / 283 ms / **304 ms** | | `CompleteMultipartUpload` p50 / p95 / max | **6 ms** / 1,56 s / 4,54 s | 23 ms / **30 ms / 31 ms** | | `StatObject` p50 / p95 | 2 ms / 17 ms | **1 ms / 3 ms** | | `RemoveObject` p50 / p95 | **2 ms** / 13 ms | 6 ms / **11 ms** | | Mémoire du pod : repos → **pic** | 130 Mi → **1188 Mi** | 75 Mi → **567 Mi** | | CPU du pod, pic | 547 m | **271 m** | | CPU de pi3, pic | 1767 m | **1194 m** | | Mémoire de pi3 pendant l'essai | 62 % → 71 % | **60 % → 61 %** | | Redémarrages | 0 | 0 | | Parcours `stockage.go` (PUT nu, `Origin`, `Complete`, GET présigné, `Remove`) | code 0, 7 OK | code 0, 7 OK | | Limite mémoire du banc | 1536 Mi | **2 Gi** (celle de prod) | **Lecture :** - **Aucun des deux ne décroche.** SeaweedFS est plus rapide en médiane et en débit. MinIO est plus régulier : p95 et max plus bas sur les PUT de part et sur `Complete`. - **SeaweedFS paie ×2,1 en mémoire et ×2 en CPU.** Son pic (1188 Mi) reste **sous la limite de prod de MinIO (2 Gi)**. - **Le critère « < 512 Mi » était mal posé** : écrit sans référence, MinIO lui-même le dépasse. - ⚠ **Écart de banc** : MinIO a été chargé avec l'identité **root** ; SeaweedFS avec une identité applicative bornée (B2). L'évaluation d'une politique pèse peu face au transfert, mais l'écart est écrit. ### 4.2 La référence prod | Ce qui était demandé | Relevé | |---|---| | `kubectl top pod` au repos, plusieurs fois | **impossible** : 5 relevés à 15 s d'écart → `Metrics not available` (pod en `CrashLoopBackOff`) | | Requêtes et limites | image `quay.io/minio/minio:RELEASE.2024-12-18T13-15-44Z` ; requests `cpu: 100m`, `memory: 512Mi` ; limits `memory: 2Gi` (aucune limite CPU) ; PVC 50 Gi | | Objets et octets de `kadans-videos` | **inaccessibles** : serveur éteint. Dernier chiffre connu : ~7,7 Go le 2026-08-07 (kadans-api#107) | ### 4.3 Les alternatives | | **Garage** | **RustFS** | **Cloudflare R2** | |---|---|---|---| | PUT de part présigné (notre chemin principal) | incertain | incertain | incertain : [la doc](https://developers.cloudflare.com/r2/api/s3/presigned-urls/) ne cite pas UploadPart | | Multipart, CORS par bucket, cycle de vie Abort | doc ✅ | incertain | doc ✅ | | Droits | une clé × un bucket | IAM et politiques | jetons d'API | | Maturité | v2.4.1 stable | 1.0.0-rc.6 (2026-09-11), prerelease | commercial ; ADR-012 : aucun seuil atteint | | Mesuré ici | non | non | non | ### 4.4 Recommandation révisée **SeaweedFS.** À charge égale, il fait le même travail sans échec, un peu plus vite. Il règle deux dettes : le CORS global partagé et les stubs 501. Il remplace un logiciel mort **dont l'instance est tombée**. Son surcoût mémoire est réel (×2,1), mais tient sous la limite qu'on donnait déjà à MinIO. Conditions, toutes mesurées comme nécessaires : 1. **`-s3.allowedOrigins` explicite**, jamais `*`, plus un CORS par bucket posé par Tofu. 2. **`-s3.iam.readOnly=false`** et admin amorcé depuis Vault. Politiques en noms SeaweedFS (`s3:ListParts`), provider épinglé `registry.terraform.io/…`. 3. **Capacité** : `-volume.max` et `-master.volumeSizeLimitMB` dimensionnés sur le PVC, plus un **CronJob `volume.vacuum`**. 4. **Filet de purge** : un CronJob `s3.clean.uploads -timeAgo 24h`. 5. **Ressources** : requests `512Mi`, **limit `1536Mi` à `2Gi`**, et `nodeSelector` sur **pi1 ou pi3, jamais pi2** (109-111 % de mémoire à chaque relevé, et nœud du MinIO en panne). --- ## 5. Les bancs ### 5.1 B1 (2026-09-14) : l'API S3 `weed server` tout-en-un sur pi3, Longhorn 2 Gi, identités en fichier, accès par `port-forward`. | Passage | Code | Résultat | |---|---|---| | Nominal (minio-go v7.2.1) | **0** | 38 OK | | Sabotage : faux secret | **28** | 403 et `SignatureDoesNotMatch` ; ⚠ le `port-forward` meurt quand le serveur refuse un corps en cours d'envoi (KO suivants = harnais) | | Persistance / témoin | **0** / **1** | relu / KO attendu | | IAM en écriture avec `-s3.config` | **254** | écritures désactivées | Créé puis supprimé : ns `essai-seaweedfs`, Secret `seaweedfs-s3`, PVC `seaweedfs-banc` (`pvc-59a2afd6-…`), Deployment. `delete namespace` → 0 ; PV et volume Longhorn → NotFound ; `diff` des PV → 0. ### 5.2 B2 (2026-09-15) : provisionnement, charge, bascule, purge **Montage** : `weed server -s3 -s3.iam.readOnly=false`, Longhorn 5 Gi, pi3 ; chargeur sur pi1. **Provisionnement** (OpenTofu 1.10.5, état local) : | Étape | Code | Sortie | |---|---|---| | `init` `JonasKop/seaweedfs` | 1 | absent de `registry.opentofu.org` | | `init` `registry.terraform.io/JonasKop/seaweedfs` + `hashicorp/aws` 5.100.0 | 0 | installés, signés | | `apply` n° 1 | 1 | `MalformedPolicyDocument: not a valid action: 'ListMultipartUploadParts'` | | `apply` n° 2 (`s3:ListParts`) | 0 | 1 ajoutée | | `plan -detailed-exitcode` | **0** | No changes | | `apply` n° 3 | 0 | 0 changement | | `plan` après redémarrage | **0** | No changes | **Charge.** Essai n° 1, code **146** : 146 PUT en 500, dus au dimensionnement de volumes du harnais (`-volume.max=8`). Essai n° 2, code **0** : chiffres au §4.1. **Parcours et sabotage.** Nominal : code **0**. Faux secret : code **8** (403 `SignatureDoesNotMatch`, puis `port-forward` mort). Cloison : `AccessDenied` ×2. Bucket sans règle CORS : code **1** (origine étrangère acceptée). **Bascule.** - URL de l'ancien magasin : clé inconnue → 403 `InvalidAccessKeyId` ; même clé, autre secret → 403 `SignatureDoesNotMatch`. Tous portent l'ACAO, donc le XHR lit bien 403. - `uploadId` de l'ancien magasin → `NoSuchUpload` 404. **Côté front** (lecture de code, `ea56aac5`) : - `depot-video.ts` l.120-128 transforme ce 403 en `ErreurApiCoeur(403)`, que `classerEchec` (`moteur-sauvegarde.ts` l.169) range en **`serveur`**, donc rejoué en backoff. - Au rejeu, `televersementId` est renvoyé (l.322-326). L'API reçoit `NoSuchUpload` et rouvre un envoi, et le front remet les parts à zéro (l.332-337). - **Rattrapage en un essai**, à une condition : l'API signe avec les nouvelles clés **dans le même geste** que la bascule de l'hôte. **Purge.** - Envois posés : code 1 (le 3ᵉ en 500, volumes pleins) ; `ListMultipartUploads` = 3. - `s3.lifecycle.run-shard` avant l'échéance : 0, toujours 3. - `s3.clean.uploads -timeAgo 1m` : 0, puis **0 envoi inachevé**. - `volume.vacuum` + `volume.deleteEmpty` : Free 0 → 13, disque 64 % → 0 %. Créé puis supprimé : ns, Secret `seaweedfs-admin`, PVC `seaweedfs-banc` 5 Gi (`pvc-87fa5604-…`), Deployment et Service `seaweedfs`, Pod `chargeur` (deux fois), buckets et identités internes au banc. `delete namespace` → 0 ; PV et volume Longhorn → NotFound ; `diff` des PV → 0 ; 0 pod restant. ### 5.3 B3 (2026-09-15) : la référence MinIO **Montage**, calqué sur `tools/minio/templates/deployment.yaml` et `values.yaml` (`6913e5d`, lus en lecture seule) : - **Identique à la prod** : image, commande `minio server /export -S /etc/minio/certs/ --address :9000 --console-address :9001`, `MINIO_PROMETHEUS_AUTH_TYPE=public`, `MINIO_API_CORS_ALLOW_ORIGIN` (les deux origines), sondes startup, readiness et liveness, `securityContext` 1000:1000, requests 512Mi/100m, limit 2Gi. - **Écarts** : `nodeSelector` pi3 (comme SeaweedFS) ; PVC Longhorn 5 Gi au lieu de 50 ; Secret root local (sans Vault ni ServiceAccount). | Passage | Code | Résultat | |---|---|---| | `create-bucket kadans-videos` | 0 | — | | Charge (même binaire, mêmes paramètres, depuis pi1) | **0** | 0 échec ; chiffres au §4.1 | | Parcours `stockage.go` | **0** | 7 OK, dont PUT nus 200 avec ACAO, GET présigné sha identique, Stat 404 après `Remove` | **Créé puis supprimé :** ``` kubectl --context=default apply -f minio-banc.yaml # ns essai-seaweedfs, PVC minio-banc (longhorn 5Gi), Deployment minio (pi3), Service minio kubectl --context=default -n essai-seaweedfs create secret generic minio-banc --from-literal=rootUser=… --from-literal=rootPassword=… kubectl --context=default apply -f chargeur.yaml # Pod chargeur (alpine:3.20, pi1) kubectl --context=default -n essai-seaweedfs port-forward svc/minio 39000:9000 kubectl --context=default delete namespace essai-seaweedfs --wait=true --timeout=300s # code 0 kubectl --context=default get ns essai-seaweedfs # NotFound kubectl --context=default get pv pvc-439e7a80-ebf1-4ba4-bfe0-f4d6637f3293 # NotFound kubectl --context=default -n longhorn-system get volumes.longhorn.io pvc-439e7a80-ebf1-4ba4-bfe0-f4d6637f3293 # NotFound diff pv-avant.txt pv-apres.txt # code 0 ``` **Prod, lecture seule** : `get deploy`, `get pod`, `top pod` (×5), `get events`, `logs --previous`, `get pvc`, `get volumes.longhorn.io` et `replicas.longhorn.io`. **Aucune** écriture, **aucun** redémarrage, aucun `port-forward` vers la prod. ArgoCD, Vault, DNS et Cloudflare intacts. ### 5.4 Ce qui n'a pas été mesuré - L'exécution réelle de l'Abort multipart par le worker de cycle de vie, à J+1. - La copie `rclone` et son débit. - kadans-api elle-même déployée contre l'essai (seule sa séquence d'appels a été rejouée). - La référence prod au repos (serveur en panne). --- ## 6. Plan de migration en lots 👤 = exige un humain. | Lot | Contenu | Humain | |---|---|---| | **M0** — la panne de prod (hors migration, préalable) | Décider : réparer MinIO, ou accélérer M1-M5 (Q1). Tant que MinIO ne démarre pas, les objets existants (~7,7 Go) ne sont lisibles par aucune voie S3 : **M4 en dépend** | 👤 fondateur | | **M1** — le serveur à côté | Chart `tools/seaweedfs/` autonome, avec toutes les conditions du §4.4 (allowedOrigins, iam.readOnly, volumes, vacuum, clean.uploads, 512Mi/1536Mi-2Gi, hors pi2). Service interne seulement | 👤 synchronisation ArgoCD si elle n'est pas automatique | | **M2** — le provisionnement | Pipeline `seaweedfs` : admin dans `kvv2/seaweedfs/config`. Module `s3_app` : `registry.terraform.io/JonasKop/seaweedfs` pour bucket, identité, clé et politique (`s3:ListParts`) ; `hashicorp/aws` pour CORS et cycle de vie. Secret écrit dans **`kvv2/s3/<app>`** avec des clés **`S3_*`**. `app_policy` : nouveaux chemins | 👤 **apply Vault par OIDC** : `hashicorp-vault`, `seaweedfs`, `front/iac` | | **M3** — **renommage `MINIO_*` → `S3_*`** (arbitré le 2026-09-15) | kadans-api lit `S3_ENDPOINT`, `S3_ENDPOINT_INTERNE`, `S3_BUCKET`, `S3_REGION`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` **avec repli sur `MINIO_*`** le temps de la bascule, retiré en M6. Idem pour `outils/import-direct` et `outils/verifier-import`. Chart : Secret `kadans-api-s3`, `VaultStaticSecret` → `s3/kadans`, `endpointInterne` → SeaweedFS. Tests (`stockage_test.go`, `TestMagasinMemoireNEcrasePasMinio`), textes et doctrine renommés dans les quatre dépôts, dont `check-cors-magasin.mjs` et `CLAUDE.md` du front | — | | **M4** — la copie | Job `rclone sync` puis `rclone check`, comptes d'objets et d'octets des deux côtés. **Exige un MinIO qui démarre** (M0) | 👤 lancer et lire le rapport | | **M5** — la bascule | Ingress `s3.arcodange.fr` et `.lab` vers SeaweedFS **dans le même geste** que le redéploiement de kadans-api sur les nouvelles clés. Contrôles : `bun run check:cors`, `bun run test:import-reel` | 👤 Cloudflare si le tunnel ou le DNS change | | **M6** — le retrait | Suppression du repli `MINIO_*`, du chart, du module, du provider, du workflow et de `kvv2/minio/*` | 👤 secrets Vault ; PVC MinIO (`Prune=false`) supprimé à la main | **Retour arrière**, jusqu'à M6 : ingress et `VaultStaticSecret` vers MinIO (s'il démarre), plus `rclone copy` inverse. Les masters restent sur les appareils, et la file ne retire aucune tâche : au pire, le moteur redépose. --- ## 7. Questions ouvertes au fondateur 1. **Q1 — la panne de prod.** MinIO ne démarre plus (« drive is faulty »), alors que Longhorn dit le volume sain. Ouvrir un incident à part pour le diagnostiquer et récupérer les ~7,7 Go, ou traiter la migration comme la réparation ? 2. **Q2 — la limite mémoire de SeaweedFS** : 1536 Mi (pic mesuré + 30 %) ou 2 Gi (la limite actuelle de MinIO) ? *(Tranchés le 2026-09-15 : « mesurer MinIO d'abord » → §4.1 ; renommer `MINIO_*` en `S3_*` → M3.)* --- Renvoi : tools#31, tools#37, kadans-api#107, kadans-api#217, incident du 2026-09-07 (`front/docs/memoire/stockage/sauvegarde.md#droit-manque-magasin`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FDDndoBCcpef952NdHDgaQ
Author
Owner

Arbitrage fondateur — 2026-09-15 (outil question) : FINIR LE BANC D'ABORD

La seconde moitié du banc (§5.4) est lancée : provisionnement déclaratif et identités dynamiques, purge réelle des envois multipart abandonnés, charge réaliste depuis le cluster, parcours de bout en bout façon kadans-api, et comportement d'une URL présignée à la bascule. Mêmes règles qu'au premier banc : contexte default explicite, namespace jetable, prod intacte, tout supprimé à la fin. La décision de migrer revient au fondateur après ces mesures.

## Arbitrage fondateur — 2026-09-15 (outil question) : **FINIR LE BANC D'ABORD** La seconde moitié du banc (§5.4) est lancée : provisionnement déclaratif et identités dynamiques, purge réelle des envois multipart abandonnés, charge réaliste depuis le cluster, parcours de bout en bout façon kadans-api, et comportement d'une URL présignée à la bascule. Mêmes règles qu'au premier banc : contexte `default` explicite, namespace jetable, prod intacte, tout supprimé à la fin. La décision de migrer revient au fondateur après ces mesures.
arcodange changed title from Remplacer MinIO par SeaweedFS — cadrage : banc mesuré sur le homelab (nos opérations S3 passent toutes), et ce qui reste à trancher avant de migrer to Remplacer MinIO par SeaweedFS — cadrage : banc complet mesuré sur le homelab, quatre conditions de configuration et un critère mémoire manqué 2026-09-15 00:31:53 +02:00
Author
Owner

Second banc joué (2026-09-15), corps de l'issue mis à jour : matrice §3, recommandation §4, mesures §5.4-5.6, plan §6, questions §7.

  • Provisionnement par Tofu idempotent, y compris après redémarrage. Il faut deux providers, s3:ListParts au lieu du nom AWS, et registry.terraform.io/… écrit en toutes lettres.
  • Parcours de bout en bout avec l'Origin de la PWA. Le sabotage rougit (code 8).
  • Bascule : les 403 sont lisibles par le XHR, et NoSuchUpload fait rouvrir un téléversement. Le front se rattrape en un essai, si l'API change de clés en même temps que l'hôte.
  • Charge depuis pi1 : 0 échec, 24,8 Mio/s. Pic mémoire 1188 Mi, pour un critère écrit à < 512 Mi → Q1.
  • Purge : la voie manuelle s3.clean.uploads fonctionne. L'exécution réelle de la règle de cycle de vie reste non observée (délai de 24 h en dur).
  • ⚠ Deux pièges trouvés : le défaut -s3.allowedOrigins=* accepte toute origine sur un bucket sans règle, et les suppressions ne rendent pas la place avant volume.vacuum (deux épisodes de 500).

Tout ce qui a été créé est supprimé : namespace, PVC, PV et volume Longhorn en NotFound, liste des PV identique (§5.6). Décision au fondateur.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FDDndoBCcpef952NdHDgaQ

**Second banc joué (2026-09-15), corps de l'issue mis à jour** : matrice §3, recommandation §4, mesures §5.4-5.6, plan §6, questions §7. - **✅ Provisionnement par Tofu idempotent**, y compris après redémarrage. Il faut deux providers, `s3:ListParts` au lieu du nom AWS, et `registry.terraform.io/…` écrit en toutes lettres. - **✅ Parcours de bout en bout** avec l'`Origin` de la PWA. Le sabotage rougit (code 8). - **✅ Bascule** : les 403 sont lisibles par le XHR, et `NoSuchUpload` fait rouvrir un téléversement. Le front se rattrape en un essai, **si** l'API change de clés en même temps que l'hôte. - **✅ Charge** depuis pi1 : 0 échec, 24,8 Mio/s. **❌ Pic mémoire 1188 Mi**, pour un critère écrit à < 512 Mi → Q1. - **Purge** : la voie manuelle `s3.clean.uploads` fonctionne. L'exécution réelle de la règle de cycle de vie reste non observée (délai de 24 h en dur). - ⚠ Deux pièges trouvés : le défaut **`-s3.allowedOrigins=*`** accepte toute origine sur un bucket sans règle, et **les suppressions ne rendent pas la place avant `volume.vacuum`** (deux épisodes de 500). Tout ce qui a été créé est supprimé : namespace, PVC, PV et volume Longhorn en NotFound, liste des PV identique (§5.6). Décision au fondateur. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FDDndoBCcpef952NdHDgaQ
Author
Owner

Arbitrages fondateur — 2026-09-15 (outil question), après le second banc

  1. Mémoire (pic SeaweedFS 1188 Mi contre un critère écrit de 512 Mi) → MESURER MINIO D'ABORD. Référence à charge égale : même image MinIO que la prod, montée dans le namespace jetable sur le même nœud, même charge. Prod observée en lecture seule (mémoire au repos, objets, octets). Décision ensuite.
  2. Noms → MINIO_* devient S3_* pendant la migration, dans le même lot : chart, secrets Vault, API, jobs.
## Arbitrages fondateur — 2026-09-15 (outil question), après le second banc 1. **Mémoire (pic SeaweedFS 1188 Mi contre un critère écrit de 512 Mi) → MESURER MINIO D'ABORD.** Référence à charge égale : même image MinIO que la prod, montée dans le namespace jetable sur le même nœud, même charge. Prod observée en lecture seule (mémoire au repos, objets, octets). Décision ensuite. 2. **Noms → `MINIO_*` devient `S3_*`** pendant la migration, dans le même lot : chart, secrets Vault, API, jobs.
arcodange changed title from Remplacer MinIO par SeaweedFS — cadrage : banc complet mesuré sur le homelab, quatre conditions de configuration et un critère mémoire manqué to Remplacer MinIO par SeaweedFS — cadrage : trois bancs mesurés sur le homelab, SeaweedFS contre MinIO à charge égale, et le MinIO de prod est EN PANNE 2026-09-15 09:33:34 +02:00
Author
Owner

Référence MinIO mesurée (B3, 2026-09-15), corps de l'issue mis à jour (§4.1, §4.2, §4.4, §5.3, §6 M0 et M3, §7).

⚠⚠ Le MinIO de prod est en panne. Pod tools/minio-… en CrashLoopBackOff sur pi2, 312 redémarrages en 26 h, avec FATAL Unable to initialize backend: drive is faulty. Le volume Longhorn est pourtant healthy avec 3 réplicas. Relevé en lecture seule, rien touché. Aucune vidéo ne se stocke ni ne se relit → Q1.

À charge égale (image et configuration de prod, même Pi, 8 × 320 Mio, 4 en parallèle) :

SeaweedFS MinIO
échecs 0 0
débit 24,8 Mio/s 20,9 Mio/s
PUT de part p50 / p95 2,18 s / 5,82 s 2,89 s / 3,99 s
pic mémoire 1188 Mi 567 Mi
pic CPU du pod 547 m 271 m

Le critère « < 512 Mi » était mal posé : MinIO le dépasse lui-même. La recommandation reste SeaweedFS, avec une limite de 1536 Mi à 2 Gi, hors pi2. Le renommage MINIO_*S3_* est inscrit en M3.

Ménage fait : namespace, PV et volume Longhorn en NotFound, liste des PV identique.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FDDndoBCcpef952NdHDgaQ

**Référence MinIO mesurée (B3, 2026-09-15), corps de l'issue mis à jour** (§4.1, §4.2, §4.4, §5.3, §6 M0 et M3, §7). **⚠⚠ Le MinIO de prod est en panne.** Pod `tools/minio-…` en `CrashLoopBackOff` sur pi2, 312 redémarrages en 26 h, avec `FATAL Unable to initialize backend: drive is faulty`. Le volume Longhorn est pourtant `healthy` avec 3 réplicas. Relevé en lecture seule, rien touché. Aucune vidéo ne se stocke ni ne se relit → Q1. **À charge égale** (image et configuration de prod, même Pi, 8 × 320 Mio, 4 en parallèle) : | | SeaweedFS | MinIO | |---|---|---| | échecs | 0 | 0 | | débit | 24,8 Mio/s | 20,9 Mio/s | | PUT de part p50 / p95 | 2,18 s / 5,82 s | 2,89 s / 3,99 s | | pic mémoire | 1188 Mi | 567 Mi | | pic CPU du pod | 547 m | 271 m | Le critère « < 512 Mi » était mal posé : MinIO le dépasse lui-même. **La recommandation reste SeaweedFS**, avec une limite de 1536 Mi à 2 Gi, hors pi2. Le renommage `MINIO_*` → `S3_*` est inscrit en M3. Ménage fait : namespace, PV et volume Longhorn en NotFound, liste des PV identique. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FDDndoBCcpef952NdHDgaQ
Author
Owner

Relais : réduire l'empreinte mémoire de SeaweedFS (Q2). Arrêté à la demande du fondateur, forfait épuisé. AUCUNE mesure valide.

Les leviers trouvés, et leur prix

  • -volume.index=leveldb (ou leveldbMedium, leveldbLarge) à la place de l'index en mémoire, qui coûte ~20 octets par fichier. L'index passe à 4, 8 ou 12 Mo au total. Prix : accès un peu plus lent. Optimization, Production Setup. Gain probablement faible ici : peu d'objets.
  • -s3.concurrentUploadLimitMB, -filer.concurrentUploadLimitMB, -volume.concurrentUploadLimitMB (défaut 0, illimité). Une écriture ATTEND tant que les octets en vol dépassent la limite (source 4.46 : weed/s3api/s3api_circuit_breaker.go). Prix : débit et latence sous charge. Le mainteneur conseille plutôt un filer store partagé et plusieurs instances filer/S3 (discussion #6012).
  • Taille des chunks -filer.maxMB (4 Mio sous weed server). Au plus 4 tampons par requête (weed/operation/upload_chunked.go). Plus petit = moins de mémoire en vol, mais plus de chunks et de métadonnées. Optimization : « 1000 x 32MB memory is needed » pour 1000 lectures concurrentes.
  • -s3.cacheCapacityMB : cache de chunks pour les GET, désactivé par défaut (0). Rien à gagner.
  • GOMEMLIMIT (~90-95 % de la limite du conteneur) et GOGC plus bas. Prix : plus de CPU consacré au GC, et un risque d'emballement près de la limite (Go GC guide). Hypothèse non mesurée : c'est le levier le plus probable, le pic de 1188 Mi ressemblant à du tas Go non rendu.
  • -volume.readBufferSizeMB : plus grand = moins de verrous, mais plus de mémoire en concurrence (Optimization).

Mesures faites : aucune. Trois configurations avaient été déclarées avant d'être jouées :

  • C1 = index leveldb + concurrentUploadLimitMB=64 ;
  • C2 = C1 + GOMEMLIMIT=512MiB et GOGC=50 ;
  • C3 = C2 sous une limite dure de 768 Mi, avec GOMEMLIMIT=690MiB.

La première tentative (C1) n'est pas un verdict. Le PVC a été refusé à cause de son nom en majuscules (seaweedfs-C1, invalide en RFC 1123), donc le pod n'a jamais démarré (apply 1, rollout 1, charge code 200).

Reste à mesurer : C1, C2 et C3 sous la même charge (8 × 320 Mio, 4 en parallèle, parts de 16 Mio ; serveur sur pi3, chargeur sur pi1). Il faudra corriger le nom du PVC en minuscules dans jouer.sh. Ensuite, le parcours stockage.go sur la configuration retenue, puis le choix de la limite. La comparaison reste : SeaweedFS B2 à 1188 Mi, MinIO B3 à 567 Mi.

Ménage fait : delete namespace essai-seaweedfs → code 0 ; namespace NotFound ; aucun PVC n'avait été créé ; liste des PV identique à l'avant-banc (diff code 0). Prod et pi2 non touchés.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FDDndoBCcpef952NdHDgaQ

**Relais : réduire l'empreinte mémoire de SeaweedFS (Q2). Arrêté à la demande du fondateur, forfait épuisé. AUCUNE mesure valide.** **Les leviers trouvés, et leur prix** - **`-volume.index=leveldb`** (ou `leveldbMedium`, `leveldbLarge`) à la place de l'index en mémoire, qui coûte ~20 octets par fichier. L'index passe à 4, 8 ou 12 Mo au total. Prix : accès un peu plus lent. [Optimization](https://github.com/seaweedfs/seaweedfs/wiki/Optimization), [Production Setup](https://github.com/seaweedfs/seaweedfs/wiki/Production-Setup). Gain probablement faible ici : peu d'objets. - **`-s3.concurrentUploadLimitMB`**, `-filer.concurrentUploadLimitMB`, `-volume.concurrentUploadLimitMB` (défaut 0, illimité). Une écriture ATTEND tant que les octets en vol dépassent la limite (source 4.46 : `weed/s3api/s3api_circuit_breaker.go`). Prix : débit et latence sous charge. Le mainteneur conseille plutôt un filer store partagé et plusieurs instances filer/S3 ([discussion #6012](https://github.com/seaweedfs/seaweedfs/discussions/6012)). - **Taille des chunks `-filer.maxMB`** (4 Mio sous `weed server`). Au plus 4 tampons par requête (`weed/operation/upload_chunked.go`). Plus petit = moins de mémoire en vol, mais plus de chunks et de métadonnées. [Optimization](https://github.com/seaweedfs/seaweedfs/wiki/Optimization) : « 1000 x 32MB memory is needed » pour 1000 lectures concurrentes. - **`-s3.cacheCapacityMB`** : cache de chunks pour les GET, désactivé par défaut (0). Rien à gagner. - **`GOMEMLIMIT`** (~90-95 % de la limite du conteneur) et **`GOGC`** plus bas. Prix : plus de CPU consacré au GC, et un risque d'emballement près de la limite ([Go GC guide](https://go.dev/doc/gc-guide)). **Hypothèse non mesurée : c'est le levier le plus probable**, le pic de 1188 Mi ressemblant à du tas Go non rendu. - **`-volume.readBufferSizeMB`** : plus grand = moins de verrous, mais plus de mémoire en concurrence ([Optimization](https://github.com/seaweedfs/seaweedfs/wiki/Optimization)). **Mesures faites : aucune.** Trois configurations avaient été déclarées avant d'être jouées : - C1 = index leveldb + `concurrentUploadLimitMB=64` ; - C2 = C1 + `GOMEMLIMIT=512MiB` et `GOGC=50` ; - C3 = C2 sous une limite dure de 768 Mi, avec `GOMEMLIMIT=690MiB`. La première tentative (C1) **n'est pas un verdict**. Le PVC a été refusé à cause de son nom en majuscules (`seaweedfs-C1`, invalide en RFC 1123), donc le pod n'a jamais démarré (apply 1, rollout 1, charge code 200). **Reste à mesurer** : C1, C2 et C3 sous la même charge (8 × 320 Mio, 4 en parallèle, parts de 16 Mio ; serveur sur pi3, chargeur sur pi1). Il faudra corriger le nom du PVC en minuscules dans `jouer.sh`. Ensuite, le parcours `stockage.go` sur la configuration retenue, puis le choix de la limite. La comparaison reste : SeaweedFS B2 à 1188 Mi, MinIO B3 à 567 Mi. **Ménage fait** : `delete namespace essai-seaweedfs` → code 0 ; namespace NotFound ; aucun PVC n'avait été créé ; liste des PV identique à l'avant-banc (`diff` code 0). Prod et pi2 non touchés. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FDDndoBCcpef952NdHDgaQ
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange-org/tools#48