Compare commits
2
Commits
edc3a73351
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3158f2d013 | ||
|
|
6d09adab0c |
+35
-12
@@ -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
|
jamais `*` : une URL présignée qui fuiterait serait sinon rejouable depuis
|
||||||
n'importe quel site.
|
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
|
Le trafic public passe par un **tunnel Cloudflare**, qui plafonne la taille du
|
||||||
Cloudflare plafonnent la taille du corps d'une requête proxifiée (de l'ordre de
|
corps d'une requête proxifiée. Ce plafond était marqué ici « à vérifier avant de
|
||||||
**100 Mo**) — ce plafond n'a **pas** été mesuré ici, il doit l'être avec un vrai
|
s'y fier ». Il l'est.
|
||||||
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
|
Sonde : un PUT **non signé** vers le bucket. Rien ne s'écrit, et les deux
|
||||||
du fondateur (707 vidéos, ~2 ans), **la durée moyenne est de 53 secondes** et
|
réponses se distinguent proprement — un 403 vient de MinIO (donc le corps a
|
||||||
**deux vidéos seulement dépassent 5 minutes**. Au palier « travail » de l'ADR-018
|
traversé le tunnel), un 413 vient du tunnel lui-même.
|
||||||
(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é
|
| Corps | Réponse | Lecture |
|
||||||
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.
|
| 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
|
## Donner à une app l'accès au stockage
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ des variables dans le Deployment.
|
|||||||
## Ce que le module garantit
|
## Ce que le module garantit
|
||||||
|
|
||||||
- les buckets sont **privés** — l'accès passe par des URL présignées ;
|
- 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à ;
|
- 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
|
- 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
|
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
|
## Ce que le module ne fait PAS
|
||||||
|
|
||||||
Il ne pose ni quota, ni règle de cycle de vie, ni versioning : ces choix
|
Il ne pose ni quota, ni **expiration de contenu**, ni versioning : ces choix
|
||||||
appartiennent à l'app et varient d'un bucket à l'autre. À ajouter le jour où
|
appartiennent à l'app et varient d'un bucket à l'autre. « Ces vidéos se purgent
|
||||||
un besoin réel apparaît, pas avant.
|
à 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.
|
||||||
|
|||||||
@@ -8,6 +8,48 @@ resource "minio_s3_bucket" "app" {
|
|||||||
force_destroy = false # détruire un bucket doit être un geste explicite
|
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" {
|
resource "minio_iam_policy" "app" {
|
||||||
name = "${var.app}-app"
|
name = "${var.app}-app"
|
||||||
policy = jsonencode({
|
policy = jsonencode({
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
minio = {
|
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]
|
configuration_aliases = [minio]
|
||||||
}
|
}
|
||||||
vault = {
|
vault = {
|
||||||
|
|||||||
Reference in New Issue
Block a user