diff --git a/minio/README.md b/minio/README.md index 5d0e9c4..de98bb6 100644 --- a/minio/README.md +++ b/minio/README.md @@ -78,20 +78,43 @@ l'URL signée et de sa durée de vie courte (15 min pour déposer, 1 h pour lire 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 +### La taille maximale d'une requête — **mesurée le 2026-08-20** -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. +Le trafic public passe par un **tunnel Cloudflare**, qui plafonne la taille du +corps d'une requête proxifiée. Ce plafond était marqué ici « à vérifier avant de +s'y fier ». Il l'est. -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. +Sonde : un PUT **non signé** vers le bucket. Rien ne s'écrit, et les deux +réponses se distinguent proprement — un 403 vient de MinIO (donc le corps a +traversé le tunnel), un 413 vient du tunnel lui-même. + +| Corps | Réponse | Lecture | +|---:|---|---| +| 50 Mio | `403` | corps passé, refus de signature | +| **100 Mio** | `403` | **corps passé** | +| **101 Mio** | `413` | **refusé par le tunnel** | +| 200 Mio | `413` | refusé par le tunnel | + +⚠ **Le plafond est exactement 100 Mio.** Or `kadans-api` annonçait un gabarit de +200 Mio — le **double**. Une vidéo entre les deux était acceptée, signée, +téléversée pendant ~100 Mo… puis coupée. + +⚠ **La parade retenue n'est PAS celle que cette section recommandait.** Ramener +le gabarit sous le plafond du tunnel aurait aussi fermé les cours longs (au +palier « travail » de l'ADR-018, 360p ≈ 3,4 Mo/min, 100 Mio ≈ 29 min). C'est le +**téléversement en plusieurs parts** qui a été livré (kadans-api PR #188) : des +parts de 8 Mio passent chacune très en dessous du plafond, sans rétrécir la +promesse. + +Le corpus, lui, reste largement sous la limite : sur les 707 vidéos du fondateur +(~2 ans), **la durée moyenne est de 53 secondes** et **deux seulement dépassent +5 minutes**. C'est ce qui explique que le défaut n'ait jamais été rencontré — et +pourquoi il attendait le premier cours entier. + +⚠ **Conséquence pour le bucket, et elle est ici :** un téléversement en parts +jamais refermé laisse des parts que *rien ne montre*. Le module `minio_app` pose +donc sur chaque bucket une règle de cycle de vie +`AbortIncompleteMultipartUpload` à **1 jour** — voir son `main.tf`. ## Donner à une app l'accès au stockage diff --git a/minio/iac/modules/minio_app/README.md b/minio/iac/modules/minio_app/README.md index 3df9edf..1bb653b 100644 --- a/minio/iac/modules/minio_app/README.md +++ b/minio/iac/modules/minio_app/README.md @@ -38,6 +38,8 @@ 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/`, que le module Vault central autorise déjà l'app à lire (règle **inconditionnelle** : le chemin porte le nom de @@ -51,6 +53,48 @@ l'accès, **sans nouvelle clé**. ## Ce que le module ne fait PAS -Il ne pose ni quota, ni règle de cycle de vie, ni versioning : ces choix -appartiennent à l'app et varient d'un bucket à l'autre. À ajouter le jour où -un besoin réel apparaît, pas avant. +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. diff --git a/minio/iac/modules/minio_app/main.tf b/minio/iac/modules/minio_app/main.tf index 55a6a03..0a5cb0b 100644 --- a/minio/iac/modules/minio_app/main.tf +++ b/minio/iac/modules/minio_app/main.tf @@ -8,6 +8,48 @@ resource "minio_s3_bucket" "app" { force_destroy = false # détruire un bucket doit être un geste explicite } +# ── Les téléversements ABANDONNÉS ne s'accumulent pas ──────────────────────── +# +# Un téléversement S3 en plusieurs parts qui n'est jamais refermé laisse ses +# parts dans le bucket : elles occupent de l'espace et AUCUN objet ne les +# montre. `mc ls` n'en dit rien, la console non plus. C'est donc une fuite qui +# ne se voit qu'à la facture — ou à la saturation du volume, qui est ici de +# 50 Gi et déjà dimensionné à 250 h de cours. +# +# L'application abandonne ce qu'elle ouvre quand elle échoue en route. Ce +# qu'elle ne peut PAS rattraper : un navigateur qui ferme l'onglet, perd le +# réseau, ou expire. Ce cas-là appartient au BUCKET, pas au code applicatif — +# c'est la seule place d'où l'on voit un téléversement que plus personne ne +# suit. +# +# ⚠ POURQUOI UN JOUR, ET PAS SEPT. Les URL de parts sont signées pour quelques +# heures (2 h côté kadans-api). Passé ce délai, un téléversement en cours ne +# peut PLUS rien recevoir : il est mort, il occupe seulement. Un jour laisse +# douze fois la marge nécessaire au plus long téléversement possible, sans +# garder des déchets une semaine. +# +# ⚠ CE 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. +# +# ⚠ CE RÉGLAGE-CI FONCTIONNE VRAIMENT, contrairement au CORS par bucket. MinIO +# édition communautaire stubbe `PutBucketCors` (501, cf. `cmd/dummy-handlers.go` +# du serveur), ce qui a déjà coûté une tentative d'IaC ; les handlers de CYCLE DE +# VIE, eux, n'y figurent PAS — vérifié à la source le 2026-08-20. +resource "minio_ilm_policy" "app" { + for_each = minio_s3_bucket.app + bucket = each.value.bucket + + rule { + id = "abandon-televersements-incomplets" + status = "Enabled" + + abort_incomplete_multipart_upload { + days_after_initiation = "1d" + } + } +} + resource "minio_iam_policy" "app" { name = "${var.app}-app" policy = jsonencode({ diff --git a/minio/iac/modules/minio_app/providers.tf b/minio/iac/modules/minio_app/providers.tf index c85f94b..53069da 100644 --- a/minio/iac/modules/minio_app/providers.tf +++ b/minio/iac/modules/minio_app/providers.tf @@ -1,7 +1,14 @@ terraform { required_providers { minio = { - source = "aminueza/minio" + source = "aminueza/minio" + # ⚠ PLANCHER, ET IL N'EST PAS DÉCORATIF : `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). Sans cette + # contrainte, un appelant resté sur 3.3.0 échouerait au plan avec un + # « unsupported block type » qui ne dit pas qu'il faut monter de version. + # Le plancher fait dire à tofu la vraie phrase, au bon moment. + version = ">= 3.10.0" configuration_aliases = [minio] } vault = {