Helm Charts / Detect changed charts (pull_request) Successful in 17s
Helm Charts / Detect changed charts (push) Successful in 16s
MinIO / Auth with gitea for vault (pull_request) Failing after 8m32s
MinIO / Tofu - minio IAC (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 (push) Has been cancelled
Helm Charts / Library charts tool (push) Has been cancelled
MinIO / Tofu - minio IAC (push) Has been cancelled
MinIO / Auth with gitea for vault (push) Has started running
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
« Pourquoi sommes-nous restreints sur les buckets ? » (fondateur, 26/07) — parce
que JE l'avais décidé, pour rendre tout dérivable d'un seul nom. Rien ne
l'imposait, et la restriction aurait mordu au pas suivant : l'ADR-018 de Kadans
prévoit DEUX paliers de transfert (aperçu 240p régénérable, travail 360p à
garder), donc deux cycles de vie, donc potentiellement deux buckets aux
politiques de purge différentes.
Chaque bucket déclare désormais son app (`app: kadans`). C'est toujours la SEULE
déclaration, au même endroit — mais le nom du bucket redevient libre, et
`kadans-videos` retrouve un nom qui dit ce qu'il contient.
Le compte de service devient par APP et non par bucket : une seule clé, autorisée
sur tous ses buckets et eux seuls. Ajouter un bucket à une app existante ne crée
donc aucune nouvelle clé — le compte existant gagne l'accès. Le secret porte
`MINIO_BUCKETS` (tous) en plus de `MINIO_BUCKET` (le premier), pour que l'app
n'ait pas à les redéclarer de son côté.
Un bucket sans `app:` est ignoré plutôt que de faire échouer le plan : il n'aura
simplement pas de compte de service, ce qui se voit immédiatement et ne casse
rien d'existant.
Vérifié sur le VRAI fichier, pas en théorie : le groupement rend bien
{"kadans" = ["kadans-videos"]}, et avec un second bucket de la même app,
{"kadans" = ["kadans-videos", "kadans-apercus"]} — le cas ADR-018 exact.
tofu fmt propre, tofu validate réussi.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
149 lines
8.0 KiB
Markdown
149 lines
8.0 KiB
Markdown
# MinIO — stockage objet S3 du homelab
|
||
|
||
Brique **partagée** du namespace `tools`, au même titre que pgbouncer ou
|
||
clickhouse. Le serveur vit ici ; les buckets, quotas et identifiants d'une
|
||
application vivent avec cette application.
|
||
|
||
## Premier consommateur : Kadans
|
||
|
||
- [ADR-012](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/docs/adr/012-video-storage-minio-first.md)
|
||
« MinIO local d'abord » — bascule vers Cloudflare R2 prévue aux seuils :
|
||
100+ utilisateurs actifs, > 10 To/mois, ou dispersion géographique.
|
||
- [ADR-013](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/docs/adr/013-video-storage-opfs-local-first.md)
|
||
le gratuit est **local-first** (la vidéo ne quitte pas l'appareil) ; MinIO sert
|
||
les **paliers payants**.
|
||
- [ADR-018](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/docs/adr/018-qualite-video-au-transfert.md)
|
||
ce qui transite est **dérivé** (aperçu 240p ~50 Ko, travail 360p ~3,4 Mo/min) —
|
||
le master reste chez l'utilisateur. D'où le dimensionnement ci-dessous.
|
||
|
||
## Ce que ce chart pose
|
||
|
||
| | |
|
||
|---|---|
|
||
| Mode | **standalone** (1 réplique) — la donnée est dérivée et Longhorn réplique déjà le volume ; l'erasure coding distribué coûterait de la RAM que des Pi 5 n'ont pas à dépenser pour ça |
|
||
| Volume | **50 Gi** sur `longhorn` ≈ **250 h de cours** au palier « travail ». ⚠ Longhorn réplique : compter **×3** sur la capacité du cluster avant d'augmenter |
|
||
| Ressources | requests 512 Mi / 100 m · limit 2 Gi — la limite protège les voisins de `tools`, pas MinIO |
|
||
| API S3 | `s3.arcodange.lab` (interne) **et `s3.arcodange.fr`** (public, tunnel Cloudflare → entrypoint `web` + crowdsec) — voir « Pourquoi une exposition publique » |
|
||
| Console | `minio.arcodange.lab` (Traefik) |
|
||
| Buckets | **privés**, chacun déclarant son app (`app:`) — l'accès passe par des URL signées (ADR-0002 du dossier produit). Le bucket est la SEULE déclaration : compte de service et droits Vault en découlent. Une app peut en avoir plusieurs |
|
||
| Identifiants | **jamais dans le dépôt** : `iac/` les génère dans Vault (`kvv2/minio/config`), le Vault Secrets Operator les matérialise en secret `minio-config`, le chart les lit via `existingSecret` |
|
||
|
||
Le ServiceAccount du pod est nommé `minio` (et non le `minio-sa` par défaut du
|
||
chart amont) parce que le module Vault `app_roles` borne l'authentification au
|
||
SA portant le nom de l'app : un seul SA, rien à réconcilier.
|
||
|
||
## Première mise en service
|
||
|
||
L'ordre compte, et il compte **deux fois** :
|
||
|
||
1. **Workflow `Hashicorp Vault`** — MinIO doit d'abord figurer dans
|
||
`hashicorp-vault/iac/terraform.tfvars` (c'est fait) : c'est **là** que naît
|
||
le rôle CI `gitea_cicd_minio`, et non dans `minio/iac`. Sans cette étape,
|
||
le workflow MinIO échoue sur
|
||
`role "gitea_cicd_minio" could not be found` — il essaie de s'authentifier
|
||
avec un rôle que personne n'a encore créé.
|
||
2. **Workflow `MinIO`** — applique `minio/iac` : rôle Kubernetes pour le Vault
|
||
Secrets Operator, et **génération** du mot de passe root dans
|
||
`kvv2/minio/config`.
|
||
3. **ArgoCD** synchronise l'application (déclarée dans `chart/values.yaml`).
|
||
4. Vérifier : `kubectl -n tools get vaultstaticsecret minio` (secret
|
||
matérialisé) puis `kubectl -n tools get pods -l app=minio`.
|
||
|
||
> [!NOTE]
|
||
> Sans le secret `minio-config`, le pod ne démarre pas. C'est voulu — mieux
|
||
> vaut un pod en attente qu'un MinIO ouvert avec des identifiants par défaut.
|
||
|
||
|
||
## Pourquoi une exposition publique (`s3.arcodange.fr`)
|
||
|
||
La PWA Kadans est servie en `https://kadans.arcodange.fr` et téléverse ses vidéos
|
||
**directement** vers MinIO, avec des URL présignées émises par kadans-api
|
||
(kadans-api#23) : les octets ne passent jamais par l'API.
|
||
|
||
Deux raisons rendent le `.lab` inutilisable pour ça, et ce sont des faits du
|
||
navigateur, pas des préférences :
|
||
|
||
1. **Contenu mixte** — une page servie en `https` ne peut pas émettre une requête
|
||
vers `http://`. L'ingress `.lab` est en entrypoint `web` sans TLS.
|
||
2. **`.lab` n'est pas résolvable hors du LAN** — la synchronisation ne marcherait
|
||
qu'à la maison, ce qui vide de son sens « retrouver mes vidéos sur mon autre
|
||
appareil ».
|
||
|
||
**Pas de basic-auth** sur cet ingress, contrairement à `kadans-public` : une
|
||
requête S3 porte sa propre signature (SigV4). Un défi HTTP Basic casserait le PUT
|
||
présigné, auquel le navigateur ne peut pas répondre. L'autorisation vient de
|
||
l'URL signée et de sa durée de vie courte (15 min pour déposer, 1 h pour lire).
|
||
|
||
**CORS** (`MINIO_API_CORS_ALLOW_ORIGIN`) liste les origines EXACTES de la PWA —
|
||
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
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
|
||
## Donner à une app l'accès au stockage
|
||
|
||
**Une seule chose à faire** : déclarer son bucket dans `values.yaml`, en disant
|
||
à quelle app il appartient.
|
||
|
||
```yaml
|
||
buckets:
|
||
- name: mon-app-fichiers
|
||
app: mon-app # ← la seule déclaration
|
||
policy: none # privé : l'accès passe par des URL signées
|
||
```
|
||
|
||
Tout le reste en découle, sans rien écrire ailleurs :
|
||
|
||
- `iac/consumers.tf` **lit ce même fichier**, groupe les buckets par app, et crée
|
||
**un** compte de service `mon-app-app` autorisé sur **tous ses buckets et eux
|
||
seuls**. Ses clés atterrissent dans `kvv2/minio/mon-app` ;
|
||
- le module Vault central `app_policy` accorde **déjà** à toute app la lecture de
|
||
`kvv2/data/minio/<son nom>` — inconditionnellement, parce que le chemin porte
|
||
le nom de l'app et ne peut donc jamais exposer que ses propres clés. Une app
|
||
qui ne stocke rien y lit un chemin qui n'existe pas : une règle inerte.
|
||
|
||
Côté app, il reste à écrire une `VaultStaticSecret` sur `kvv2/minio/<app>` et à
|
||
injecter les variables dans son Deployment (voir `kadans-api` pour l'exemple).
|
||
Le secret porte `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`,
|
||
`MINIO_BUCKET` (le premier) et `MINIO_BUCKETS` (tous, séparés par des virgules).
|
||
|
||
### Plusieurs buckets pour une même app
|
||
|
||
C'est prévu, et c'est le cas courant : deux contenus aux **cycles de vie
|
||
différents** méritent deux buckets aux politiques de purge différentes. Kadans y
|
||
viendra avec les deux paliers de l'ADR-018 — l'aperçu 240p, régénérable, et le
|
||
rendu de travail 360p, à garder. Il suffit d'une seconde entrée avec le même
|
||
`app:` ; le compte de service existant gagne l'accès, sans nouvelle clé.
|
||
|
||
> **Il n'y a volontairement AUCUNE liste de consommateurs.** Une liste de plus
|
||
> serait une liste à tenir synchronisée avec les buckets — donc une liste à
|
||
> oublier. Le bucket fait foi.
|
||
|
||
### Pourquoi les clés vivent ICI et pas chez l'app
|
||
|
||
Seul ce pipeline possède les identifiants **root** de MinIO. Si chaque app créait
|
||
son propre compte de service, il faudrait donner ce root à chaque rôle CI —
|
||
c'est-à-dire à tout le monde. Ici il ne sort jamais, et l'app ne reçoit qu'une
|
||
clé qui **ne peut rien lire d'autre que son bucket**. Un compte de service qui
|
||
fuite ne donne accès qu'aux objets qu'il gérait déjà.
|
||
|
||
### Rotation
|
||
|
||
Détruire `random_password.app["<app>"]` et relancer le plan suffit : la clé
|
||
change, `force_destroy = false` garde le compte, et les objets déjà déposés
|
||
conservent leur propriétaire.
|