« On peut adopter ce pattern à toutes les apps qui pourraient avoir besoin de MinIO, et ainsi modifier les app roles / app policy générique. » (fondateur, 26/07) Bonne nouvelle : les modules centraux n'ont PAS eu à changer. `kv_read_paths` existe déjà dans `app_policy` et fait exactement ça — l'ERP s'en sert pour lire les creds GCS de Longhorn. Le motif générique était donc à moitié construit ; il manquait la moitié MinIO. CE QUI EST AJOUTÉ : `minio/iac/consumers.tf` provisionne, pour chaque app déclarée dans `var.consumers`, une politique MinIO bornée à SON bucket (GetObject/PutObject/DeleteObject sur les objets, ListBucket sur le bucket seul — ni les autres buckets, ni l'administration), un compte de service, et écrit ses clés dans `kvv2/minio/<app>`. POURQUOI LES CLÉS VIVENT CHEZ MINIO ET PAS CHEZ L'APP : seul ce pipeline possède les identifiants ROOT. Si chaque app créait son propre compte de service, il faudrait donner ce root à chaque rôle CI — c'est-à-dire à tout le monde. Ici il ne sort jamais d'ici, et l'app ne reçoit qu'une clé qui ne peut rien lire d'autre que son bucket. Un compte de service qui fuite ne donne accès qu'aux objets qu'il gérait déjà. Ajouter une app = trois lignes déclaratives, documentées au README : son bucket dans values.yaml, son entrée dans `var.consumers`, son `kv_read_paths` au tfvars central. Aucun module à toucher. Le provider parle à `s3.arcodange.fr` — l'endpoint PUBLIC, parce que le runner CI n'est pas dans le LAN et que `.lab` ne s'y résout pas. C'est l'exposition HTTPS mergée ce matin qui rend ce plan applicable. ⚠ Erreur attrapée par le validateur, et corrigée : je relisais `kvv2/minio/config` alors que `local.config` de main.tf porte déjà les identifiants root — une lecture qui aurait créé une dépendance circulaire avec l'écriture faite par ce même plan. Vérifié : `tofu fmt` propre, `tofu validate` réussi (avec TERRAFORM_VAULT_AUTH_JWT posé — sans lui, le provider Vault échoue déjà sur main, ce n'est pas mon fait). Je n'ai lancé AUCUN apply : c'est le workflow qui le fera, et la revue t'appartient. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
7.1 KiB
MinIO — stockage objet S3 du homelab
Brique partagée du namespace tools, au même titre que pgbouncer ou
clickhouse. Le serveur vit ici ; les buckets, quotas et identifiants d'une
application vivent avec cette application.
Premier consommateur : Kadans
- ADR-012 « MinIO local d'abord » — bascule vers Cloudflare R2 prévue aux seuils : 100+ utilisateurs actifs, > 10 To/mois, ou dispersion géographique.
- ADR-013 le gratuit est local-first (la vidéo ne quitte pas l'appareil) ; MinIO sert les paliers payants.
- ADR-018 ce qui transite est dérivé (aperçu 240p ~50 Ko, travail 360p ~3,4 Mo/min) — le master reste chez l'utilisateur. D'où le dimensionnement ci-dessous.
Ce que ce chart pose
| Mode | standalone (1 réplique) — la donnée est dérivée et Longhorn réplique déjà le volume ; l'erasure coding distribué coûterait de la RAM que des Pi 5 n'ont pas à dépenser pour ça |
| Volume | 50 Gi sur longhorn ≈ 250 h de cours au palier « travail ». ⚠ Longhorn réplique : compter ×3 sur la capacité du cluster avant d'augmenter |
| Ressources | requests 512 Mi / 100 m · limit 2 Gi — la limite protège les voisins de tools, pas MinIO |
| API S3 | s3.arcodange.lab (interne) et s3.arcodange.fr (public, tunnel Cloudflare → entrypoint web + crowdsec) — voir « Pourquoi une exposition publique » |
| Console | minio.arcodange.lab (Traefik) |
| Bucket | kadans-videos, privé — l'accès passe par des URL signées (ADR-0002 du dossier produit) |
| Identifiants | jamais dans le dépôt : iac/ les génère dans Vault (kvv2/minio/config), le Vault Secrets Operator les matérialise en secret minio-config, le chart les lit via existingSecret |
Le ServiceAccount du pod est nommé minio (et non le minio-sa par défaut du
chart amont) parce que le module Vault app_roles borne l'authentification au
SA portant le nom de l'app : un seul SA, rien à réconcilier.
Première mise en service
L'ordre compte, et il compte deux fois :
- Workflow
Hashicorp Vault— MinIO doit d'abord figurer danshashicorp-vault/iac/terraform.tfvars(c'est fait) : c'est là que naît le rôle CIgitea_cicd_minio, et non dansminio/iac. Sans cette étape, le workflow MinIO échoue surrole "gitea_cicd_minio" could not be found— il essaie de s'authentifier avec un rôle que personne n'a encore créé. - Workflow
MinIO— appliqueminio/iac: rôle Kubernetes pour le Vault Secrets Operator, et génération du mot de passe root danskvv2/minio/config. - ArgoCD synchronise l'application (déclarée dans
chart/values.yaml). - Vérifier :
kubectl -n tools get vaultstaticsecret minio(secret matérialisé) puiskubectl -n tools get pods -l app=minio.
Note
Sans le secret
minio-config, le pod ne démarre pas. C'est voulu — mieux vaut un pod en attente qu'un MinIO ouvert avec des identifiants par défaut.
Pourquoi une exposition publique (s3.arcodange.fr)
La PWA Kadans est servie en https://kadans.arcodange.fr et téléverse ses vidéos
directement vers MinIO, avec des URL présignées émises par kadans-api
(kadans-api#23) : les octets ne passent jamais par l'API.
Deux raisons rendent le .lab inutilisable pour ça, et ce sont des faits du
navigateur, pas des préférences :
- Contenu mixte — une page servie en
httpsne peut pas émettre une requête vershttp://. L'ingress.labest en entrypointwebsans TLS. .labn'est pas résolvable hors du LAN — la synchronisation ne marcherait qu'à la maison, ce qui vide de son sens « retrouver mes vidéos sur mon autre appareil ».
Pas de basic-auth sur cet ingress, contrairement à kadans-public : une
requête S3 porte sa propre signature (SigV4). Un défi HTTP Basic casserait le PUT
présigné, auquel le navigateur ne peut pas répondre. L'autorisation vient de
l'URL signée et de sa durée de vie courte (15 min pour déposer, 1 h pour lire).
CORS (MINIO_API_CORS_ALLOW_ORIGIN) liste les origines EXACTES de la PWA —
jamais * : une URL présignée qui fuiterait serait sinon rejouable depuis
n'importe quel site.
⚠ À vérifier avant de s'y fier : la taille maximale d'une requête
Le trafic public passe par un tunnel Cloudflare. Les offres gratuites de Cloudflare plafonnent la taille du corps d'une requête proxifiée (de l'ordre de 100 Mo) — ce plafond n'a pas été mesuré ici, il doit l'être avec un vrai téléversement avant d'annoncer une limite aux utilisateurs.
Ce qu'on sait, en revanche, et qui rend le sujet peu urgent : sur le corpus réel du fondateur (707 vidéos, ~2 ans), la durée moyenne est de 53 secondes et deux vidéos seulement dépassent 5 minutes. Au palier « travail » de l'ADR-018 (360p ≈ 3,4 Mo/min), 100 Mo représentent ~29 minutes de cours : le corpus entier passe très largement. Si la limite se confirme, le plafond de 200 Mio annoncé côté API mérite d'être ramené sous celle du tunnel — mieux vaut refuser tôt, avec une phrase claire, qu'échouer au milieu d'un téléversement.
Donner à une app l'accès au stockage
Le motif tient en trois pièces, et il est générique — ajouter une app ne demande aucune modification des modules Vault centraux.
- Le bucket — dans
values.yamlde ce chart (blocbuckets), privé. - Le compte de service — une entrée dans
var.consumersdeiac/consumers.tf:Le plan crée une politique MinIO bornée à ce bucket, un compte de service, et écrit ses clés dans{ app = "mon-app", bucket = "mon-app-fichiers" }kvv2/minio/mon-app. - La lecture — dans
hashicorp-vault/iac/terraform.tfvars, l'app déclare :{ name = "mon-app", kv_read_paths = ["kvv2/data/minio/mon-app"] }kv_read_pathsexistait déjà (l'ERP s'en sert pour les creds GCS de Longhorn) : rien à changer dansapp_policyniapp_roles.
Puis, côté app : une VaultStaticSecret sur kvv2/minio/<app> et l'injection
des variables dans le Deployment.
Pourquoi les clés vivent ICI et pas chez l'app
Seul ce pipeline possède les identifiants root de MinIO. Si chaque app créait son propre compte de service, il faudrait donner ce root à chaque rôle CI — c'est-à-dire à tout le monde. Ici il ne sort jamais, et l'app ne reçoit qu'une clé qui ne peut rien lire d'autre que son bucket : ni les autres buckets, ni l'administration. Un compte de service qui fuite ne donne accès qu'aux objets qu'il gérait déjà.
Rotation
Détruire random_password.app["<app>"] et relancer le plan suffit : la clé
change, force_destroy = false garde le compte et les objets déjà déposés
conservent leur propriétaire.