Files
tools/minio/iac/modules/minio_app/main.tf
T
arcodangeandClaude Fable 5.1 56d0421943 minio_app : le compte de service peut refermer et abandonner un téléversement en parts (kadans#1033)
`ListObjectParts` et `AbortMultipartUpload` rendaient 403 AccessDenied avec les
identifiants de kadans-api — mesuré le 2026-09-07 sur le homelab, en rejouant les
appels de contrôle exacts de l'API depuis un poste. Ouvrir et déposer les parts
sont des PutObject et passaient ; l'assemblage n'a jamais pu avoir lieu, et toute
vidéo de plus d'une part échouait à la confirmation derrière « stockage injoignable ».

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-07 08:22:44 +02:00

113 lines
4.5 KiB
Terraform

# Module `minio_app` — une app déclare SES buckets depuis SON dépôt.
# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md
resource "minio_s3_bucket" "app" {
for_each = toset(var.buckets)
bucket = each.key
acl = "private" # l'accès passe par des URL présignées
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({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
# ⚠ LES DEUX ACTIONS DU MULTIPART QUE `s3:PutObject` NE COUVRE PAS — mesuré
# le 2026-09-07 sur le homelab, avec les identifiants de kadans-api rejoués
# depuis un poste : ouvrir un téléversement et déposer ses parts passent
# (ce sont des PutObject), mais `ListObjectParts` rend 403 AccessDenied.
# L'app ne pouvait donc JAMAIS refermer un téléversement en parts : toute
# vidéo qui dépasse la taille d'une part échouait à l'assemblage, à tous
# les coups, derrière un « stockage injoignable » qui ne nommait rien.
# `AbortMultipartUpload` était refusé pareil : l'app ne pouvait pas non
# plus abandonner ce qu'elle avait ouvert — seule la règle de cycle de vie
# ci-dessus ramassait, un jour plus tard.
Action = [
"s3:GetObject", "s3:PutObject", "s3:DeleteObject",
"s3:ListMultipartUploadParts", "s3:AbortMultipartUpload",
]
Resource = [for b in var.buckets : "arn:aws:s3:::${b}/*"]
},
{
Effect = "Allow"
Action = ["s3:ListBucket", "s3:GetBucketLocation"]
Resource = [for b in var.buckets : "arn:aws:s3:::${b}"]
},
]
})
}
resource "random_password" "app" {
length = 40
special = false # les outils S3 transportent mal certains caractères en URL
}
resource "minio_iam_user" "app" {
name = "${var.app}-app"
secret = random_password.app.result
force_destroy = false # une rotation ne recrée pas l'utilisateur : les objets gardent leur propriétaire
}
resource "minio_iam_user_policy_attachment" "app" {
user_name = minio_iam_user.app.id
policy_name = minio_iam_policy.app.id
}
# Lu par le pod de l'app — `app_policy` lui accorde déjà ce chemin.
resource "vault_kv_secret_v2" "app" {
mount = "kvv2"
name = "minio/${var.app}"
data_json = jsonencode({
MINIO_ENDPOINT = var.endpoint
MINIO_BUCKET = var.buckets[0]
MINIO_BUCKETS = join(",", var.buckets)
MINIO_ACCESS_KEY = minio_iam_user.app.id
MINIO_SECRET_KEY = random_password.app.result
})
}