refactor(minio) — chacun son périmètre : les buckets se déclarent depuis le dépôt de l'app
Helm Charts / Detect changed charts (push) Successful in 16s
Helm Charts / Detect changed charts (pull_request) Successful in 16s
MinIO / Auth with gitea for vault (pull_request) Failing after 8m32s
MinIO / Tofu - minio IAC (pull_request) Has been skipped
MinIO / Auth with gitea for vault (push) Failing after 8m31s
MinIO / Tofu - minio IAC (push) Has been skipped
Helm Charts / Library charts tool (push) Has been cancelled
Helm Charts / Application charts pgcat (push) Has been cancelled
Hashicorp Vault / Auth with gitea for vault (push) Has been cancelled
Hashicorp Vault / Tofu - Vault IAC (push) Has been cancelled
Helm Charts / Library charts tool (pull_request) Has been skipped
Hashicorp Vault / Auth with gitea for vault (pull_request) Failing after 8m32s
Hashicorp Vault / Tofu - Vault IAC (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped

« Les buckets sont à déclarer dans le repo de kadans-api. On ne va pas modifier
le repo tools à chaque changement d'application. Chacun son périmètre. Tools
peut proposer un module pour standardiser la déclaration de buckets à la
limite, mais c'est tout. » (fondateur, 26/07)

C'est une erreur de fond de ma part : j'avais fait de `tools` le PROPRIÉTAIRE de
déclarations qui appartiennent aux applications. À ce rythme, chaque nouveau
bucket de n'importe quelle app devenait une PR sur l'infra partagée.

CE QUI CHANGE. `consumers.tf` disparaît, et la liste de buckets du chart se vide.
À la place, un module réutilisable `iac/modules/minio_app` : une app lui donne
son nom et ses buckets, et reçoit des buckets privés, un compte de service qui
ne peut rien toucher d'autre, et ses clés dans `kvv2/minio/<app>`.

L'OBSTACLE, ET SA RÉPONSE. Déclarer ses buckets depuis son propre dépôt suppose
des droits d'ADMINISTRATION sur MinIO. Confier le root serait absurde : il lit et
écrit tous les objets de toutes les apps. `tools` fournit donc un compte
PROVISIONNEUR aux droits minimaux — créer un bucket, une politique, un compte de
service — et AUCUN droit sur les objets. Une app compromise pourrait créer des
buckets (une nuisance), pas lire les vidéos d'une autre. Le root, lui, ne sort
toujours pas de ce pipeline.

Le rôle CI de chaque app gagne la lecture de `kvv2/data/minio/provisioner` dans
`app_policy` — générique, et c'est exactement le genre de standardisation qui
appartient au dépôt commun.

⚠ CE QUE JE N'AI PAS PU PROUVER : les noms d'actions d'administration MinIO de la
politique du provisionneur viennent de la documentation, pas d'un essai — je n'ai
pas d'identifiants admin en main. Le premier `apply` les confirmera ou les
corrigera. C'est le seul point non vérifié de cette PR, et il est signalé dans le
code à l'endroit exact.

tofu fmt propre · tofu validate réussi sur minio/iac ET hashicorp-vault/iac.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
This commit is contained in:
2026-07-26 10:07:05 +02:00
co-authored by Claude Opus 5
parent 0e1b6e1062
commit ec71571b98
10 changed files with 314 additions and 155 deletions
+89
View File
@@ -0,0 +1,89 @@
# ── Module `minio_app` — une app déclare SES buckets, depuis SON dépôt ───────
#
# Chacun son périmètre (fondateur 2026-07-26) : les buckets d'une application
# appartiennent au dépôt de cette application, pas à l'infra partagée. `tools`
# ne fait que STANDARDISER la déclaration — il ne la détient pas. Sans ça,
# chaque bucket de chaque app deviendrait une PR sur le dépôt commun.
#
# Usage, depuis l'`iac/` de l'app :
#
# module "stockage" {
# source = "git::…/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
# app = "kadans"
# buckets = ["kadans-videos"]
# providers = { minio = minio }
# }
#
# Ce que le module garantit :
# · les buckets sont PRIVÉS — l'accès passe par des URL présignées ;
# · le compte de service ne peut RIEN toucher d'autre que ces buckets-là ;
# · ses clés atterrissent 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).
resource "minio_s3_bucket" "app" {
for_each = toset(var.buckets)
bucket = each.key
acl = "private"
# Les objets sont DÉRIVÉS (rendus de transfert) et régénérables depuis les
# originaux restés sur l'appareil : un `force_destroy` accidentel ne détruit
# donc rien d'irremplaçable, mais on l'interdit quand même — la destruction
# d'un bucket doit être un geste explicite, pas un effet de bord d'un plan.
force_destroy = false
}
# La politique : SES buckets, rien d'autre. Ni les autres, ni l'administration.
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}/*"]
},
{
# Nécessaire pour qu'un client S3 vérifie l'existence d'un bucket et
# liste SES objets — jamais ceux d'une autre app.
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"
# Le mot de passe EST la clé secrète S3 : généré ici, jamais choisi.
secret = random_password.app.result
# `false` : une rotation du mot de passe ne recrée pas l'utilisateur, donc les
# objets déjà déposés gardent leur propriétaire.
force_destroy = false
}
resource "minio_iam_user_policy_attachment" "app" {
user_name = minio_iam_user.app.id
policy_name = minio_iam_policy.app.id
}
# Les clés, dans l'espace Vault de MinIO — lu par le pod de l'app.
# `MINIO_BUCKETS` les liste toutes pour que l'app n'ait pas à les redéclarer ;
# `MINIO_BUCKET` reste le premier, pour celles qui n'en ont qu'un.
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
})
}