diff --git a/.gitea/workflows/minio.yaml b/.gitea/workflows/minio.yaml new file mode 100644 index 0000000..c451438 --- /dev/null +++ b/.gitea/workflows/minio.yaml @@ -0,0 +1,70 @@ +--- +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). +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: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +.vault_step: &vault_step + name: read vault secret + uses: https://gitea.arcodange.lab/arcodange-org/vault-action.git@main + id: vault-secrets + with: + url: https://vault.arcodange.lab + caCertificate: ${{ secrets.HOMELAB_CA_CERT }} + jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }} + role: gitea_cicd_minio + method: jwt + path: gitea_jwt + secrets: | + kvv1/google/credentials credentials | GOOGLE_BACKEND_CREDENTIALS ; + kvv1/gitea/tofu_module_reader ssh_private_key | TERRAFORM_SSH_KEY ; + +jobs: + gitea_vault_auth: + name: Auth with gitea for vault + runs-on: ubuntu-latest + outputs: + gitea_vault_jwt: ${{steps.gitea_vault_jwt.outputs.id_token}} + steps: + + - name: Auth with gitea for vault + id: gitea_vault_jwt + run: | + echo -n "${{ secrets.vault_oauth__sh_b64 }}" | base64 -d | bash + + tofu: + name: Tofu - minio IAC + needs: + - gitea_vault_auth + runs-on: ubuntu-latest + env: + OPENTOFU_VERSION: 1.8.2 + TERRAFORM_VAULT_AUTH_JWT: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }} + VAULT_CACERT: "${{ github.workspace }}/homelab.pem" + steps: + - *vault_step + - uses: actions/checkout@v4 + - name: prepare vault self signed cert + run: echo -n "${{ secrets.HOMELAB_CA_CERT }}" | base64 -d > $VAULT_CACERT + - name: terraform apply + uses: dflook/terraform-apply@v1 + with: + path: minio/iac + auto_approve: true diff --git a/chart/values.yaml b/chart/values.yaml index 876472d..e6be51b 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -8,4 +8,5 @@ tools: clickhouse: {} grafana: {} plausible: {} - prometheus: {} \ No newline at end of file + prometheus: {} + minio: {} diff --git a/minio/Chart.yaml b/minio/Chart.yaml new file mode 100644 index 0000000..e24e4ad --- /dev/null +++ b/minio/Chart.yaml @@ -0,0 +1,40 @@ +# ----------------------------------------------------------------------------- +# MinIO — stockage objet S3 du homelab (3 × Raspberry Pi 5, arm64). +# +# Pourquoi ici : c'est une BRIQUE PARTAGÉE, au même titre que pgbouncer ou +# clickhouse — le namespace `tools` en héberge le serveur ; les buckets, quotas +# et identifiants d'une application vivent, eux, avec cette application. +# +# Premier consommateur : Kadans (ADR-012 « MinIO local d'abord », ADR-013 +# « OPFS local-first, MinIO/R2 = paliers payants »). La bascule vers Cloudflare +# R2 est prévue par l'ADR-012 aux seuils : 100+ utilisateurs actifs, > 10 To/mois +# de bande passante, ou dispersion géographique. +# +# Mode STANDALONE assumé : 3 nœuds, mais la donnée servie ici est DÉRIVÉE (le +# master d'une vidéo reste sur l'appareil de son propriétaire — ADR-018 du front) ; +# la redondance de Longhorn suffit, l'erasure coding distribué de MinIO coûterait +# de la RAM et des IOPS que des Pi n'ont pas à dépenser pour ça. +# ----------------------------------------------------------------------------- +apiVersion: v2 +name: minio +description: A Helm chart for Kubernetes + +dependencies: +- name: tool + version: 0.1.0 + repository: https://gitea.arcodange.lab/api/packages/arcodange-org/helm +- name: minio + version: 5.4.0 + repository: https://charts.min.io/ + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application +version: 0.1.0 +appVersion: "latest" diff --git a/minio/README.md b/minio/README.md new file mode 100644 index 0000000..7b80ae0 --- /dev/null +++ b/minio/README.md @@ -0,0 +1,46 @@ +# 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` (Traefik) | +| Console | `minio.arcodange.lab` (Traefik) | +| Bucket | `kadans-videos`, **privé** — 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 +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 + +1. **Appliquer l'IaC** — crée le rôle Vault et génère le mot de passe root : + workflow `MinIO` (déclenchable à la main), ou `tofu apply` dans `minio/iac`. +2. **ArgoCD** synchronise l'application (déclarée dans `chart/values.yaml`). +3. Vérifier : `kubectl -n tools get vaultstaticsecret minio` (secret matérialisé) + puis `kubectl -n tools get pods -l app=minio`. + +> [!NOTE] +> L'ordre compte : 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. diff --git a/minio/iac/backend.tf b/minio/iac/backend.tf new file mode 100644 index 0000000..5ce6cb6 --- /dev/null +++ b/minio/iac/backend.tf @@ -0,0 +1,6 @@ +terraform { + backend "gcs" { + bucket = "arcodange-tf" + prefix = "tools/minio/main" + } +} diff --git a/minio/iac/main.tf b/minio/iac/main.tf new file mode 100644 index 0000000..9a7f30f --- /dev/null +++ b/minio/iac/main.tf @@ -0,0 +1,30 @@ +# Rôle Vault + politique pour que le VaultAuth du chart puisse lire kvv2/minio/* +# (module partagé du repo — même recette que plausible). +module "app_roles" { + source = "git::ssh://git@192.168.1.202:2222/arcodange-org/tools.git//hashicorp-vault/iac/modules/app_roles?depth=1&ref=main" + name = "minio" + service_account_namespaces = ["tools"] +} + +# Identifiants ROOT de MinIO. Générés ici, jamais choisis à la main et jamais +# écrits dans le dépôt : le chart officiel les lit dans le secret k8s +# `minio-config` (clés rootUser / rootPassword), matérialisé par le Vault +# Secrets Operator depuis ce chemin. +resource "random_password" "root" { + length = 40 + special = false # les outils S3 transportent mal certains caractères en URL +} + +locals { + config = { + rootUser = "kadans-root" + rootPassword = random_password.root.result + } +} + +resource "vault_kv_secret_v2" "config" { + mount = "kvv2" + name = "minio/config" + cas = 1 + data_json = jsonencode(local.config) +} diff --git a/minio/iac/providers.tf b/minio/iac/providers.tf new file mode 100644 index 0000000..501193f --- /dev/null +++ b/minio/iac/providers.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + vault = { + source = "vault" + version = "4.4.0" + } + } +} + +provider "vault" { + address = "https://vault.arcodange.lab" + auth_login_jwt { # TERRAFORM_VAULT_AUTH_JWT environment variable + mount = "gitea_jwt" + role = "gitea_cicd_minio" + } +} diff --git a/minio/templates/helm-chart-config.yaml b/minio/templates/helm-chart-config.yaml new file mode 100644 index 0000000..31cb271 --- /dev/null +++ b/minio/templates/helm-chart-config.yaml @@ -0,0 +1,3 @@ +{{- if eq .Values.tool.kind "HelmChart" -}} +{{- include "tool.helm-chart-config.tpl" . -}} +{{- end -}} \ No newline at end of file diff --git a/minio/templates/helm-chart.yaml b/minio/templates/helm-chart.yaml new file mode 100644 index 0000000..c6b793d --- /dev/null +++ b/minio/templates/helm-chart.yaml @@ -0,0 +1,3 @@ +{{- if eq .Values.tool.kind "HelmChart" -}} +{{- include "tool.helm-chart.tpl" . -}} +{{- end -}} \ No newline at end of file diff --git a/minio/templates/vault-auth.yaml b/minio/templates/vault-auth.yaml new file mode 100644 index 0000000..7d06382 --- /dev/null +++ b/minio/templates/vault-auth.yaml @@ -0,0 +1,18 @@ +{{- /* Identifiants MinIO : jamais dans le dépôt. Le Vault Secrets Operator + authentifie ce chart auprès de Vault (rôle créé par iac/) puis + matérialise le secret k8s consommé via `existingSecret`. Même recette + que plausible. */ -}} +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultAuth +metadata: + name: minio + namespace: tools +spec: + vaultConnectionRef: default + method: kubernetes + mount: kubernetes + kubernetes: + role: minio + serviceAccount: minio + audiences: + - vault diff --git a/minio/templates/vault-secret.yaml b/minio/templates/vault-secret.yaml new file mode 100644 index 0000000..f904b59 --- /dev/null +++ b/minio/templates/vault-secret.yaml @@ -0,0 +1,17 @@ +{{- /* kvv2/minio/config → secret `minio-config` (clés rootUser / rootPassword, + les deux noms exigés par le chart officiel). Le mot de passe est généré + par Terraform (iac/), jamais choisi à la main. */ -}} +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: minio + namespace: tools +spec: + type: kv-v2 + mount: kvv2 + path: minio/config + destination: + name: minio-config + create: true + refreshAfter: 30s + vaultAuthRef: minio diff --git a/minio/values.yaml b/minio/values.yaml new file mode 100644 index 0000000..cc6dde2 --- /dev/null +++ b/minio/values.yaml @@ -0,0 +1,84 @@ +minio: &minio_config + # Image officielle MinIO — multi-arch, arm64 inclus (les nœuds sont des Pi 5). + image: + repository: quay.io/minio/minio + pullPolicy: IfNotPresent + + # STANDALONE : un seul serveur, un seul volume. La donnée servie ici est + # DÉRIVÉE (le master reste chez l'utilisateur) et Longhorn réplique déjà le + # volume ; l'erasure coding distribué coûterait de la RAM que les Pi n'ont pas + # à dépenser pour ça. `replicas` est ignoré en standalone. + mode: standalone + replicas: 1 + + # Le rôle Vault du module `app_roles` borne l'authentification au SERVICE + # ACCOUNT nommé comme l'app (`bound_service_account_names = [minio]`) : on + # aligne donc le SA du pod sur ce nom, plutôt que le « minio-sa » par défaut + # du chart amont — un SA pour le pod, le même pour Vault, rien à réconcilier. + serviceAccount: + create: true + name: minio + + # Identifiants JAMAIS dans le dépôt : le secret est matérialisé par le + # Vault Secrets Operator depuis kvv2/minio/config (voir resources/ et iac/). + # Le chart lit `.data.rootUser` et `.data.rootPassword` de ce secret. + existingSecret: minio-config + + persistence: + enabled: true + storageClass: longhorn + accessMode: ReadWriteOnce + # 50 Gi ≈ 250 heures de cours au palier « travail » de Kadans (360p, + # 3,4 Mo/min — ADR-018 du front). Longhorn réplique ce volume sur les + # nœuds : compter ×3 sur la capacité du cluster avant d'augmenter. + size: 50Gi + + # Des Pi 5 à 8 Go partagés avec le reste de `tools` : on borne franchement. + # MinIO standalone est frugal ; la limite protège les voisins, pas MinIO. + resources: + requests: + memory: 512Mi + cpu: 100m + limits: + memory: 2Gi + + # API S3 (les applications parlent ici). + ingress: + enabled: true + ingressClassName: traefik + path: / + hosts: + - s3.arcodange.lab + + # Console d'administration (humains). + consoleIngress: + enabled: true + ingressClassName: traefik + path: / + hosts: + - minio.arcodange.lab + + # 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 + + # Métriques : Prometheus (namespace `tools`) scrape déjà la façade et le + # laptop (ADR-0014 du dossier) — MinIO rejoint la même vue. + metrics: + serviceMonitor: + enabled: false # pas d'opérateur Prometheus ici : scrape par annotation + environment: + MINIO_PROMETHEUS_AUTH_TYPE: "public" + +tool: + # kind: 'SubChart' or 'HelmChart', if subchart then uncomment Chart.yaml dependency, else comment and use tool library with helm chart template + kind: 'SubChart' + repo: https://charts.min.io/ + chart: minio + version: 5.4.0 + values: *minio_config