## Le plafond, mesuré — la section du README qui disait « à vérifier » ne le dit plus
`minio/README.md` portait : « 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 ».
Sonde par PUT NON SIGNÉ vers le bucket : rien ne s'écrit, et les deux réponses se
distinguent proprement — un 403 vient de MinIO (le corps a donc traversé le
tunnel), un 413 vient du tunnel.
100 Mio → 403 corps passé
101 Mio → 413 refusé par le tunnel
⚠ Le plafond est EXACTEMENT 100 Mio, alors que kadans-api annonçait 200 Mio.
⚠ Et la parade retenue n'est PAS celle que le README recommandait. Ramener le
gabarit sous le plafond aurait aussi fermé les cours longs (~29 min au palier
« travail »). C'est le téléversement en PARTS qui a été livré (kadans-api #188).
## Ce que ça crée comme déchet, et à qui il appartient
Un téléversement en parts jamais refermé laisse ses parts dans le bucket :
`mc ls` n'en dit rien, la console non plus, aucun objet ne les montre. Une fuite
qui ne se voit qu'à la facture — ou à la saturation d'un volume de 50 Gi.
L'app abandonne ce qu'elle ouvre quand elle échoue en route. Elle ne peut PAS
rattraper le navigateur qui ferme l'onglet.
⚠ LE README DU MODULE DISAIT « il ne pose ni quota, ni règle de cycle de vie » —
et cette règle-ci ne le contredit pas, elle en précise la frontière. La question
qui tranche est « à QUOI cette connaissance appartient-elle ? » :
- une EXPIRATION DE CONTENU (« ces vidéos se purgent à 90 jours ») demande de
connaître le produit. Elle est chez l'app ;
- un téléversement incomplet n'est le contenu de PERSONNE. Aucune app ne veut
le garder, aucune ne peut le voir depuis son code. C'est un déchet de
PROTOCOLE, produit par le mécanisme même du bucket : il est chez celui qui
crée les buckets.
Test pratique écrit au README : si répondre à « combien de temps ? » exige de
connaître le produit, c'est chez l'app. Ici la réponse n'exige que de connaître
S3 — passé l'expiration des URL signées (2 h côté kadans-api), un téléversement
ne peut plus RIEN recevoir. D'où 1 jour, douze fois la marge, et pas 7.
⚠ Non paramétrable (YAGNI) : un seul cas. Le déclencheur pour en faire une
variable est écrit — une app qui signerait des parts au-delà de 24 h.
## ⚠ Ce réglage-ci fonctionne, contrairement au CORS par bucket
MinIO communautaire stubbe `PutBucketCors` en 501 (`cmd/dummy-handlers.go`), ce
qui avait déjà coûté une tentative d'IaC — c'est écrit dans le `iac/main.tf` du
dépôt front. Les handlers de CYCLE DE VIE, eux, n'y figurent PAS : vérifié à la
source avant d'écrire une ligne, pour ne pas répéter exactement cette erreur.
## Le plancher de version, et pourquoi il est là
`abort_incomplete_multipart_upload` est apparu en **3.10.0** — mesuré en
interrogeant le schéma du provider version par version (3.9.0 ne l'a pas,
3.10.0 l'a). Le module déclare donc `>= 3.10.0` LUI-MÊME.
⚠ Sans ce plancher, un appelant resté sur 3.3.0 échouerait au plan sur un
« unsupported block type » qui ne dit pas qu'il faut monter de version. Avec, il
lit dès `tofu init` : « no available releases match the given constraints 3.3.0,
>= 3.10.0 ».
## ⚠ ORDRE DE FUSION
Cette PR fait passer tout appelant du module sous le plancher 3.10.0. Le dépôt
`kadans` épingle encore 3.3.0 : **son bump doit atterrir AVANT celle-ci**, sinon
son apply casse entre les deux fusions.
## Preuve
`tofu validate` contre le schéma RÉEL du provider 3.10.0, module instancié depuis
un bac à sable (aucun backend, aucun appel à MinIO ni Vault) : « Success! The
configuration is valid. »
Et le plancher a été éprouvé plutôt que relu : épinglé à 3.3.0, `tofu init` rend
bien le refus cité ci-dessus.
Co-Authored-By: Claude Opus 5 <[email protected]>
minio_app — déclarer ses buckets depuis SON dépôt
Chacun son périmètre : les buckets d'une application appartiennent au dépôt de
cette application. Ce module standardise la déclaration, il ne la détient
pas — sans lui, chaque bucket de chaque app deviendrait une PR sur tools.
Usage
Dans l'iac/ de l'app :
# Le provisionneur MinIO : des droits d'administration MINIMAUX (créer un
# bucket, un compte de service, une politique), jamais le root — qui, lui, ne
# sort pas du pipeline `minio`.
data "vault_kv_secret_v2" "minio_provisioner" {
mount = "kvv2"
name = "minio/provisioner"
}
provider "minio" {
minio_server = "s3.arcodange.fr"
minio_user = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_ACCESS_KEY"]
minio_password = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_SECRET_KEY"]
minio_ssl = true
}
module "stockage" {
source = "git::ssh://[email protected]:2222/arcodange-org/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
app = "mon-app" # = le nom de son rôle Vault
buckets = ["mon-app-fichiers"]
providers = { minio = minio }
}
Puis, côté chart : une VaultStaticSecret sur kvv2/minio/<app> et l'injection
des variables dans le Deployment.
Ce que le module garantit
- les buckets sont privés — l'accès passe par des URL présignées ;
- les téléversements abandonnés sont ramassés au bout d'un jour (voir plus bas : c'est de l'hygiène de protocole, pas un choix de l'app) ;
- le compte de service ne peut rien toucher d'autre que ces buckets-là ;
- ses clés vont dans
kvv2/minio/<app>, que le module Vault central autorise déjà l'app à lire (règle inconditionnelle : le chemin porte le nom de l'app, donc il ne peut exposer que ses propres clés).
Plusieurs buckets
C'est le cas courant : deux contenus aux cycles de vie différents méritent deux buckets. Il suffit de les lister — le compte de service existant gagne l'accès, sans nouvelle clé.
Ce que le module ne fait PAS
Il ne pose ni quota, ni expiration de contenu, ni versioning : ces choix appartiennent à l'app et varient d'un bucket à l'autre. « Ces vidéos se purgent à 90 jours » est une décision de produit, elle se prend dans le dépôt du produit.
⚠ L'exception, et la ligne qu'elle trace
Ce module pose une seule règle de cycle de vie :
AbortIncompleteMultipartUpload à 1 jour, sur chaque bucket qu'il crée.
Elle a l'air de contredire le paragraphe ci-dessus. Elle ne le contredit pas — elle en précise la frontière, et c'est la question « à QUOI cette connaissance appartient-elle ? » qui tranche :
- une expiration de contenu porte sur des objets que l'app a voulus, qu'elle montre, et dont elle seule sait combien de temps ils valent. Elle varie d'une app à l'autre : elle est chez l'app ;
- un téléversement en plusieurs parts jamais refermé n'est le contenu de
personne. Ce sont des morceaux qu'aucune API ne montre — ni
mc ls, ni la console — laissés par un navigateur qui a fermé l'onglet. Aucune app ne veut les garder, et aucune ne peut les voir depuis son propre code. C'est un déchet de PROTOCOLE, produit par le mécanisme même du bucket : il est chez celui qui crée les buckets.
Le test pratique : si la réponse à « combien de temps ? » demande de connaître le produit, c'est chez l'app. Ici, la réponse ne demande que de connaître S3 — passé l'expiration des URL signées, un téléversement en cours ne peut plus rien recevoir, il occupe seulement.
⚠ Le délai n'est pas paramétrable, et c'est voulu (YAGNI) : un seul cas existe. Le jour où une app signe des parts pour plus de 24 h, ce sera le déclencheur pour en faire une variable — pas avant.
Plancher de version
⚠ Ce module exige aminueza/minio >= 3.10.0 : abort_incomplete_multipart_upload
y est apparu (mesuré en interrogeant le schéma du provider version par version —
3.9.0 ne l'a pas). Un appelant resté plus bas se verra dire, dès tofu init :
no available releases match the given constraints 3.3.0, >= 3.10.0
… ce qui NOMME la version à atteindre, au lieu d'un « unsupported block type » au plan, qui ne l'aurait pas dite.