# 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" Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] 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 }) }