Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53c3adb7d | ||
|
|
9de9a9663b | ||
|
|
2bdc486ae6 | ||
|
|
901aa9a9dc | ||
|
|
e8ab19962b | ||
|
|
5c7dce96e1 | ||
|
|
7074a94d7e | ||
|
|
ec71571b98 | ||
|
|
0e1b6e1062 | ||
|
|
287e3dcf1e | ||
|
|
91a0f09b49 | ||
|
|
4ca4a05370 | ||
|
|
f49a79e393 | ||
|
|
1490f4c514 | ||
|
|
dce19141a8 |
@@ -2,12 +2,15 @@
|
||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||
name: Crowdsec
|
||||
|
||||
on: #[push,pull_request]
|
||||
# À LA DEMANDE, et seulement à la demande — comme minio.yaml : auth Vault par
|
||||
# flux OIDC (un humain doit ouvrir un lien) et apply `auto_approve` contre la prod.
|
||||
#
|
||||
# Note : les triggers `push`/`pull_request` retirés ici étaient de toute façon
|
||||
# INERTES — ils passaient par une ancre YAML (`&`/`*`), que le parseur
|
||||
# d'événements de Gitea ne résout pas (vécu sur arcodange/kadans, issues 113
|
||||
# → 117). Ce workflow ne partait déjà qu'à la main ; c'est maintenant écrit.
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push: &crowdsecPaths
|
||||
paths:
|
||||
- 'crowdsec/**/*.tf'
|
||||
pull_request: *crowdsecPaths
|
||||
|
||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||
concurrency:
|
||||
|
||||
@@ -2,14 +2,32 @@
|
||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||
name: Helm Charts
|
||||
|
||||
on: [push,pull_request,workflow_dispatch]
|
||||
# push: &helmPaths # turns out gitea don't handle well the paths filter
|
||||
# paths:
|
||||
# - '*/\.yaml'
|
||||
# - '*/\.tpl'
|
||||
# - '*/NOTES.txt'
|
||||
# - '*/\.helmignore'
|
||||
# pull_request: *helmPaths
|
||||
# Celui-ci travaille SEUL (pas d'auth Vault, pas d'apply) : on le garde
|
||||
# automatique. Mais `push` sur TOUTES les branches + `pull_request` faisait
|
||||
# partir DEUX runs pour le même commit dès qu'une branche avait une PR.
|
||||
#
|
||||
# Même forme que la CI de kadans : la branche est couverte par `pull_request`,
|
||||
# `main` par le `push` d'après-merge. Un run par événement, aucun angle mort.
|
||||
#
|
||||
# (Le filtre de chemins d'origine, resté en commentaire des années sous un
|
||||
# « gitea don't handle well the paths filter », n'était probablement pas en
|
||||
# cause : il passait par une ancre YAML, et le parseur d'événements de Gitea ne
|
||||
# les résout pas — issues 113 → 117 de kadans. Le job `filter-chart` fait déjà
|
||||
# ce tri au niveau job, donc on n'y retouche pas.)
|
||||
#
|
||||
# ⚠ Chaque clé porte un CORPS explicite : un `pull_request:` nu (valeur nulle)
|
||||
# n'est pas une forme éprouvée sur ce Gitea, et son mode d'échec est le
|
||||
# silencieux — aucun run, aucune erreur. On copie la forme qui tourne (kadans
|
||||
# ci.yml), listes dupliquées à la main, sans ancre.
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
|
||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||
concurrency:
|
||||
|
||||
+11
-12
@@ -1,20 +1,19 @@
|
||||
---
|
||||
name: MinIO
|
||||
|
||||
# ⚠ Triggers écrits EN TOUTES LETTRES, sans ancre YAML (`&`/`*`).
|
||||
# Une ancre dans un trigger Gitea Actions fait taire push ET pull_request —
|
||||
# en silence, aucun run, aucune erreur (vécu sur arcodange/kadans, issues 113
|
||||
# → 117). Les autres workflows de ce repo utilisent encore des ancres : à
|
||||
# vérifier séparément, c'est probablement pour ça qu'ils ne partent qu'à la
|
||||
# main (workflow_dispatch).
|
||||
# À LA DEMANDE, et seulement à la demande. Deux raisons, chacune suffisante :
|
||||
#
|
||||
# 1. Ce workflow ne PEUT PAS aboutir sans un humain : l'auth Vault passe par
|
||||
# un flux OIDC dont le lien doit être ouvert dans un navigateur connecté.
|
||||
# Déclenché tout seul, il occupe un runner jusqu'à son timeout — et retarde
|
||||
# les runs que quelqu'un attend vraiment.
|
||||
# 2. Il fait `terraform apply` en `auto_approve` CONTRE LA PROD. Se déclencher
|
||||
# sur le push d'une branche, c'est appliquer du code que personne n'a relu.
|
||||
#
|
||||
# Au passage : `push` (toutes branches) + `pull_request` faisait partir DEUX runs
|
||||
# par commit d'une branche en PR — le même SHA, deux fois.
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
paths:
|
||||
- 'minio/**/*.tf'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'minio/**/*.tf'
|
||||
|
||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||
concurrency:
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||
name: Plausible
|
||||
|
||||
on: #[push,pull_request]
|
||||
# À LA DEMANDE, et seulement à la demande — comme minio.yaml : auth Vault par
|
||||
# flux OIDC (un humain doit ouvrir un lien) et apply `auto_approve` contre la prod.
|
||||
#
|
||||
# Note : les triggers `push`/`pull_request` retirés ici étaient de toute façon
|
||||
# INERTES (ancre YAML non résolue par Gitea, issues 113 → 117 de kadans). Ce
|
||||
# workflow ne partait déjà qu'à la main ; c'est maintenant écrit.
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push: &plausiblePaths
|
||||
paths:
|
||||
- 'plausible/**/*.tf'
|
||||
pull_request: *plausiblePaths
|
||||
|
||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||
concurrency:
|
||||
|
||||
+10
-15
@@ -2,23 +2,18 @@
|
||||
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
|
||||
name: Hashicorp Vault
|
||||
|
||||
# ⚠ Triggers écrits EN TOUTES LETTRES, sans ancre YAML (`&`/`*`) : une ancre
|
||||
# dans un trigger Gitea Actions fait taire push ET pull_request, en silence
|
||||
# (vécu sur arcodange/kadans, issues 113 → 117) — c'est probablement pourquoi
|
||||
# ce workflow ne partait qu'à la main.
|
||||
# Et `*.tfvars` compte AUTANT que `*.tf` : la liste des applications (donc les
|
||||
# rôles gitea_cicd_<app>) vit dans terraform.tfvars — l'oublier, c'est ajouter
|
||||
# une app sans jamais créer son rôle.
|
||||
# À LA DEMANDE, et seulement à la demande — comme minio.yaml, et pour les mêmes
|
||||
# deux raisons : l'auth Vault exige qu'un humain ouvre un lien OIDC (sans lui, le
|
||||
# run squatte un runner jusqu'au timeout), et l'apply se fait en `auto_approve`
|
||||
# contre la prod.
|
||||
#
|
||||
# ⚠ Ce qui change AUSSI de nature : `hashicorp-vault/**/*.tfvars` compte autant
|
||||
# que `*.tf` — la liste des applications (donc les rôles gitea_cicd_<app>) vit
|
||||
# dans terraform.tfvars. Ce n'est plus un filtre de chemins mais ça reste vrai
|
||||
# du POURQUOI on relance : ajouter une app au tfvars sans relancer ce workflow,
|
||||
# c'est une app sans rôle CI.
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
paths:
|
||||
- 'hashicorp-vault/**/*.tf'
|
||||
- 'hashicorp-vault/**/*.tfvars'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'hashicorp-vault/**/*.tf'
|
||||
- 'hashicorp-vault/**/*.tfvars'
|
||||
|
||||
# cancel any previously-started, yet still active runs of this workflow on the same branch
|
||||
concurrency:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
locals {
|
||||
factory_crowdsec_conf_sa_name = "factory-ansible-tool-crowdsec-traefik-plugin"
|
||||
factory_crowdsec_conf_sa_name = "factory-ansible-tool-crowdsec-traefik-plugin"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,13 @@ data "vault_policy_document" "ops" {
|
||||
path = "kvv1/google/credentials"
|
||||
capabilities = ["read"]
|
||||
}
|
||||
# Provisionneur MinIO — l'app crée ses buckets depuis son dépôt sans le root.
|
||||
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
|
||||
rule {
|
||||
path = "kvv2/data/minio/provisioner"
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
# read cloudflare related secrets
|
||||
rule {
|
||||
path = "kvv1/cloudflare/${local.name}*"
|
||||
@@ -178,6 +185,19 @@ data "vault_policy_document" "app" {
|
||||
path = "postgres/creds/${local.name}*"
|
||||
capabilities = ["read"]
|
||||
}
|
||||
# Ses identifiants MinIO. INCONDITIONNEL : le chemin porte le nom de l'app,
|
||||
# donc la règle ne peut exposer que ses propres clés ; une app sans stockage
|
||||
# lit un chemin qui n'existe pas.
|
||||
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
|
||||
rule {
|
||||
path = "kvv2/data/minio/${local.name}/*"
|
||||
capabilities = ["read", "list"]
|
||||
}
|
||||
rule {
|
||||
# Le document lui-même (la règle ci-dessus ne couvre que ses descendants).
|
||||
path = "kvv2/data/minio/${local.name}"
|
||||
capabilities = ["read", "list"]
|
||||
}
|
||||
# Extra shared paths this app's prod runtime may read (e.g. backup creds).
|
||||
dynamic "rule" {
|
||||
for_each = var.kv_read_paths
|
||||
@@ -204,6 +224,15 @@ data "vault_policy_document" "app_non_prod" {
|
||||
path = "postgres/creds/${each.key}*"
|
||||
capabilities = ["read"]
|
||||
}
|
||||
# Idem prod : chaque instance lit les identifiants MinIO portant SON nom.
|
||||
rule {
|
||||
path = "kvv2/data/minio/${each.key}/*"
|
||||
capabilities = ["read", "list"]
|
||||
}
|
||||
rule {
|
||||
path = "kvv2/data/minio/${each.key}"
|
||||
capabilities = ["read", "list"]
|
||||
}
|
||||
}
|
||||
resource "vault_policy" "app_non_prod" {
|
||||
for_each = toset(local.non_prod_instances)
|
||||
|
||||
+93
-2
@@ -23,9 +23,9 @@ application vivent avec cette application.
|
||||
| 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` (Traefik) |
|
||||
| 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) |
|
||||
| Bucket | `kadans-videos`, **privé** — l'accès passe par des URL signées (ADR-0002 du dossier produit) |
|
||||
| Buckets | **aucun ici** — chaque app déclare les siens depuis son dépôt (module `minio_app`). Tous privés : l'accès passe par des URL signées (ADR-0002 du dossier produit) |
|
||||
| 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
|
||||
@@ -52,3 +52,94 @@ L'ordre compte, et il compte **deux fois** :
|
||||
> [!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
|
||||
|
||||
**Rien à faire ici.** Chaque application déclare **ses** buckets **depuis son
|
||||
propre dépôt**, avec le module que ce dépôt-ci fournit :
|
||||
|
||||
```hcl
|
||||
# iac/main.tf de l'application
|
||||
data "vault_kv_secret_v2" "minio_provisioner" {
|
||||
mount = "kvv2"
|
||||
name = "minio/provisioner"
|
||||
}
|
||||
|
||||
provider "minio" {
|
||||
minio_server = "s3.arcodange.fr"
|
||||
minio_user = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_ACCESS_KEY"]
|
||||
minio_password = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_SECRET_KEY"]
|
||||
minio_ssl = true
|
||||
}
|
||||
|
||||
module "stockage" {
|
||||
source = "git::…/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
|
||||
app = "mon-app"
|
||||
buckets = ["mon-app-fichiers"]
|
||||
providers = { minio = minio }
|
||||
}
|
||||
```
|
||||
|
||||
Voir `iac/modules/minio_app/README.md`. **Chacun son périmètre** : `tools`
|
||||
fournit le serveur, le provisionneur et le module — pas la liste des buckets.
|
||||
Sans ça, chaque bucket de chaque app deviendrait une PR sur l'infra partagée.
|
||||
|
||||
### Ce que `tools` fournit, et pourquoi
|
||||
|
||||
| Pièce | Rôle |
|
||||
|---|---|
|
||||
| Le serveur | le chart, son volume, ses ingress (interne + public) |
|
||||
| Le **root** | généré ici, écrit dans `kvv2/minio/config`, **ne sort jamais** de ce pipeline |
|
||||
| Le **provisionneur** | un compte aux droits d'administration MINIMAUX (créer bucket, politique, compte de service) et **aucun droit sur les objets** — lisible par le rôle CI de chaque app |
|
||||
| Le **module** | `minio_app` : standardise la déclaration, sans la détenir |
|
||||
|
||||
Donner le root aux apps aurait été absurde : il lit et écrit **tous** les objets
|
||||
de **toutes** les apps. Le provisionneur, lui, peut créer des buckets — une
|
||||
nuisance si une app est compromise — mais **pas lire les vidéos d'une autre**.
|
||||
|
||||
### Rotation
|
||||
|
||||
Depuis l'`iac/` de l'app : détruire `module.stockage.random_password.app` et
|
||||
relancer son plan. La clé change, `force_destroy = false` garde le compte, et
|
||||
les objets déjà déposés conservent leur propriétaire.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# `minio_app` — déclarer ses buckets depuis SON dépôt
|
||||
|
||||
Chacun son périmètre : les buckets d'une application appartiennent au dépôt de
|
||||
cette application. Ce module **standardise** la déclaration, il ne la détient
|
||||
pas — sans lui, chaque bucket de chaque app deviendrait une PR sur `tools`.
|
||||
|
||||
## Usage
|
||||
|
||||
Dans l'`iac/` de l'app :
|
||||
|
||||
```hcl
|
||||
# Le provisionneur MinIO : des droits d'administration MINIMAUX (créer un
|
||||
# bucket, un compte de service, une politique), jamais le root — qui, lui, ne
|
||||
# sort pas du pipeline `minio`.
|
||||
data "vault_kv_secret_v2" "minio_provisioner" {
|
||||
mount = "kvv2"
|
||||
name = "minio/provisioner"
|
||||
}
|
||||
|
||||
provider "minio" {
|
||||
minio_server = "s3.arcodange.fr"
|
||||
minio_user = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_ACCESS_KEY"]
|
||||
minio_password = data.vault_kv_secret_v2.minio_provisioner.data["MINIO_SECRET_KEY"]
|
||||
minio_ssl = true
|
||||
}
|
||||
|
||||
module "stockage" {
|
||||
source = "git::ssh://[email protected]:2222/arcodange-org/tools.git//minio/iac/modules/minio_app?depth=1&ref=main"
|
||||
app = "mon-app" # = le nom de son rôle Vault
|
||||
buckets = ["mon-app-fichiers"]
|
||||
providers = { minio = minio }
|
||||
}
|
||||
```
|
||||
|
||||
Puis, côté chart : une `VaultStaticSecret` sur `kvv2/minio/<app>` et l'injection
|
||||
des variables dans le Deployment.
|
||||
|
||||
## 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 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
|
||||
l'app, donc il ne peut exposer que ses propres clés).
|
||||
|
||||
## Plusieurs buckets
|
||||
|
||||
C'est le cas courant : deux contenus aux **cycles de vie différents** méritent
|
||||
deux buckets. Il suffit de les lister — le compte de service existant gagne
|
||||
l'accès, **sans nouvelle clé**.
|
||||
|
||||
## Ce que le module ne fait PAS
|
||||
|
||||
Il ne pose ni quota, ni règle de cycle de vie, ni versioning : ces choix
|
||||
appartiennent à l'app et varient d'un bucket à l'autre. À ajouter le jour où
|
||||
un besoin réel apparaît, pas avant.
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "vault_path" {
|
||||
value = "kvv2/minio/${var.app}"
|
||||
description = "Où le pod lira ses identifiants (VaultStaticSecret)."
|
||||
}
|
||||
|
||||
output "buckets" {
|
||||
value = var.buckets
|
||||
description = "Écho des buckets créés."
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
minio = {
|
||||
source = "aminueza/minio"
|
||||
configuration_aliases = [minio]
|
||||
}
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
variable "app" {
|
||||
type = string
|
||||
description = "Nom de l'app (= son rôle Vault). Décide du chemin du secret et du nom du compte de service."
|
||||
}
|
||||
|
||||
variable "buckets" {
|
||||
type = list(string)
|
||||
description = "Ses buckets, créés ici (privés). Le compte de service n'a de droits que sur eux."
|
||||
}
|
||||
|
||||
variable "endpoint" {
|
||||
type = string
|
||||
default = "s3.arcodange.fr"
|
||||
description = "Hôte de l'API S3, sans schéma. Public : le runner CI n'est pas dans le LAN."
|
||||
}
|
||||
@@ -4,6 +4,14 @@ terraform {
|
||||
source = "vault"
|
||||
version = "4.4.0"
|
||||
}
|
||||
minio = {
|
||||
source = "aminueza/minio"
|
||||
version = "3.3.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = "3.6.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,3 +22,11 @@ provider "vault" {
|
||||
role = "gitea_cicd_minio"
|
||||
}
|
||||
}
|
||||
|
||||
# Provider MinIO — crée le compte de provisionnement (provisioner.tf).
|
||||
provider "minio" {
|
||||
minio_server = var.minio_endpoint
|
||||
minio_user = local.config.rootUser
|
||||
minio_password = local.config.rootPassword
|
||||
minio_ssl = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Compte de PROVISIONNEMENT : crée buckets, politiques et comptes de service —
|
||||
# aucun droit sur les objets. C'est lui que lisent les rôles CI des apps, pour
|
||||
# qu'elles déclarent leurs buckets sans qu'on leur confie le root.
|
||||
# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md
|
||||
#
|
||||
# Noms d'actions confirmés par le premier apply réel (kadans, 2026-07-26) : le
|
||||
# bloc admin passe tel quel ; côté s3 il manquait `s3:ListBucket`, que le
|
||||
# provider appelle AVANT de créer un bucket pour savoir s'il existe déjà.
|
||||
resource "minio_iam_policy" "provisioner" {
|
||||
name = "provisioner"
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"admin:CreateUser",
|
||||
"admin:DeleteUser",
|
||||
"admin:ListUsers",
|
||||
"admin:GetUser",
|
||||
"admin:CreatePolicy",
|
||||
"admin:DeletePolicy",
|
||||
"admin:GetPolicy",
|
||||
"admin:ListUserPolicies",
|
||||
"admin:AttachUserOrGroupPolicy",
|
||||
]
|
||||
Resource = ["arn:aws:s3:::*"]
|
||||
},
|
||||
{
|
||||
# s3:GetObject / s3:PutObject volontairement ABSENTS : le provisionneur
|
||||
# ne LIT ni n'ÉCRIT aucun objet, c'est ce qui rend son partage entre
|
||||
# rôles CI acceptable.
|
||||
#
|
||||
# `s3:ListBucket` est la seule concession : MinIO le demande pour un
|
||||
# HeadBucket, et le provider teste l'existence du bucket avant de le
|
||||
# créer. Il donne la vue des CLÉS d'un bucket, jamais leur contenu.
|
||||
Effect = "Allow"
|
||||
Action = ["s3:CreateBucket", "s3:DeleteBucket", "s3:ListBucket", "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:GetBucketPolicy", "s3:PutBucketPolicy"]
|
||||
Resource = ["arn:aws:s3:::*"]
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "random_password" "provisioner" {
|
||||
length = 40
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "minio_iam_user" "provisioner" {
|
||||
name = "provisioner"
|
||||
secret = random_password.provisioner.result
|
||||
force_destroy = false
|
||||
}
|
||||
|
||||
resource "minio_iam_user_policy_attachment" "provisioner" {
|
||||
user_name = minio_iam_user.provisioner.id
|
||||
policy_name = minio_iam_policy.provisioner.id
|
||||
}
|
||||
|
||||
resource "vault_kv_secret_v2" "provisioner" {
|
||||
mount = "kvv2"
|
||||
name = "minio/provisioner"
|
||||
data_json = jsonencode({
|
||||
MINIO_ENDPOINT = var.minio_endpoint
|
||||
MINIO_ACCESS_KEY = minio_iam_user.provisioner.id
|
||||
MINIO_SECRET_KEY = random_password.provisioner.result
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
variable "minio_endpoint" {
|
||||
type = string
|
||||
default = "s3.arcodange.fr"
|
||||
description = "Hôte de l'API S3, sans schéma. Public : le runner CI n'est pas dans le LAN."
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Exposition PUBLIQUE s3.arcodange.fr — TLS terminé par le tunnel Cloudflare,
|
||||
# middleware crowdsec. Le `.lab` interne reste inchangé.
|
||||
#
|
||||
# ⚠ PAS de basic-auth ici, contrairement à kadans-public : une requête S3 porte
|
||||
# sa propre signature (SigV4), et un défi HTTP Basic casserait le PUT présigné
|
||||
# auquel le navigateur ne peut pas répondre.
|
||||
#
|
||||
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: minio-public
|
||||
namespace: tools
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.middlewares: kube-system-crowdsec@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: s3.arcodange.fr
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: minio
|
||||
port:
|
||||
number: 9000
|
||||
+7
-5
@@ -61,11 +61,10 @@ minio: &minio_config
|
||||
# Buckets créés au déploiement. `versioning: false` assumé : ces objets sont
|
||||
# DÉRIVÉS et re-générables depuis le master local — versionner doublerait le
|
||||
# stockage pour un filet dont on n'a pas besoin.
|
||||
buckets:
|
||||
- name: kadans-videos
|
||||
policy: none # privé : l'accès passe par des URL signées (ADR-0002 du dossier)
|
||||
purge: false
|
||||
versioning: false
|
||||
# AUCUN bucket ici : chaque app déclare les siens depuis son dépôt, via le
|
||||
# module `iac/modules/minio_app`.
|
||||
# ADR : factory/doc/adr/20260726-stockage-objet-minio.md
|
||||
buckets: []
|
||||
|
||||
# Métriques : Prometheus (namespace `tools`) scrape déjà la façade et le
|
||||
# laptop (ADR-0014 du dossier) — MinIO rejoint la même vue.
|
||||
@@ -74,6 +73,9 @@ minio: &minio_config
|
||||
enabled: false # pas d'opérateur Prometheus ici : scrape par annotation
|
||||
environment:
|
||||
MINIO_PROMETHEUS_AUTH_TYPE: "public"
|
||||
# CORS : le navigateur téléverse directement (URL présignées) — origines
|
||||
# EXACTES, jamais « * » : une URL qui fuite serait sinon rejouable partout.
|
||||
MINIO_API_CORS_ALLOW_ORIGIN: "https://kadans.arcodange.fr,https://kadans.arcodange.lab"
|
||||
|
||||
tool:
|
||||
# kind: 'SubChart' or 'HelmChart', if subchart then uncomment Chart.yaml dependency, else comment and use tool library with helm chart template
|
||||
|
||||
+30
-7
@@ -140,13 +140,36 @@ redis: &redis_config
|
||||
runAsUser: 999
|
||||
|
||||
# -- Compute resources used by the container. More info [here](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/).
|
||||
resources: {}
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
#
|
||||
# ⚠ CE N'EST PAS DU CONFORT — c'est ce qui empêche Redis de mourir en boucle.
|
||||
#
|
||||
# Le sous-chart FIGE ses sondes à `timeoutSeconds: 1` sur `redis-cli ping`, et
|
||||
# n'expose aucune valeur pour les surcharger. Sur ce matériel (Raspberry Pi),
|
||||
# un conteneur SANS réservation tombe dans la classe QoS `BestEffort` : il est
|
||||
# le premier affamé quand le nœud est chargé, et le simple lancement de
|
||||
# `redis-cli` y dépasse la seconde.
|
||||
#
|
||||
# Mesuré le 2026-07-29 sur `redis-0` : « Liveness probe failed: command timed
|
||||
# out: "redis-cli ping" timed out after 1s » **836 fois en 23 jours**, 135
|
||||
# redémarrages, le conteneur tué par SIGTERM après ~90 s de vie à chaque tour.
|
||||
#
|
||||
# ⚠ CE QUE ÇA CASSAIT, ET QUI N'AVAIT RIEN À VOIR AVEC REDIS EN APPARENCE : le
|
||||
# plugin crowdsec de Traefik utilise ce Redis comme cache de décisions. Cache
|
||||
# injoignable ⇒ `isCrowdsecStreamHealthy:false` ⇒ le plugin REFUSE PAR DÉFAUT
|
||||
# tout client non listé dans `clientTrustedIPs` ⇒ **403, corps vide, sur
|
||||
# `kadans.arcodange.fr` et `gitea.arcodange.fr`**, pendant que les hôtes qui ne
|
||||
# portent pas ce middleware répondaient normalement. Le symptôme ne nomme ni
|
||||
# Redis, ni crowdsec, ni la sonde — il ressemble à un bannissement d'IP, et
|
||||
# c'est par là qu'on cherche d'abord.
|
||||
#
|
||||
# Une réservation fait passer le conteneur en `Burstable` et lui garantit sa
|
||||
# part. Les limites restent larges : Redis n'est pas ce qui sature ce nœud.
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
|
||||
# -- Pod-level affinity. More info [here](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling).
|
||||
affinity: {}
|
||||
|
||||
Reference in New Issue
Block a user