From 8330d82225fd64bb6684dd89362539fc72a48135 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Sun, 31 May 2026 17:22:30 +0200 Subject: [PATCH] docs(runbooks): add "new web app" setup runbook under doc/runbooks/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document, as a tree-docs tree, the end-to-end procedure to stand up a new web application on the Arcodange platform — a mechanic spread across the factory, tools and app repos with non-trivial ordering dependencies. Covers: Gitea repo creation (org-secret inheritance), Postgres DB + owner role (factory/postgres/iac), platform Vault declaration (gitea_cicd_ + policies, tools/hashicorp-vault/iac), the app Helm chart (VSO dynamic secrets via pgbouncer), the app Terraform (app_roles module), the CI workflows (tofu apply + image build, incl. the copy-pasted role pitfall), and ArgoCD registration (factory/argocd/values.yaml). Adds a naming- conventions concept page and an ordered checklist. Wires the legacy doc/adr "setup hello world web app" item and the factory README to the runbook. New docs live under doc/ (singular) per the PR #8 convention. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 5 + doc/README.md | 35 ++++ doc/adr/README.md | 6 +- doc/runbooks/README.md | 21 ++ doc/runbooks/new-web-app/01-gitea-repo.md | 88 +++++++++ doc/runbooks/new-web-app/02-database.md | 93 +++++++++ doc/runbooks/new-web-app/03-vault-platform.md | 89 +++++++++ doc/runbooks/new-web-app/04-helm-chart.md | 183 ++++++++++++++++++ doc/runbooks/new-web-app/05-app-terraform.md | 96 +++++++++ doc/runbooks/new-web-app/06-ci-workflows.md | 108 +++++++++++ .../new-web-app/07-argocd-register.md | 91 +++++++++ doc/runbooks/new-web-app/08-checklist.md | 77 ++++++++ doc/runbooks/new-web-app/README.md | 104 ++++++++++ doc/runbooks/new-web-app/conventions.md | 55 ++++++ 14 files changed, 1048 insertions(+), 3 deletions(-) create mode 100644 doc/README.md create mode 100644 doc/runbooks/README.md create mode 100644 doc/runbooks/new-web-app/01-gitea-repo.md create mode 100644 doc/runbooks/new-web-app/02-database.md create mode 100644 doc/runbooks/new-web-app/03-vault-platform.md create mode 100644 doc/runbooks/new-web-app/04-helm-chart.md create mode 100644 doc/runbooks/new-web-app/05-app-terraform.md create mode 100644 doc/runbooks/new-web-app/06-ci-workflows.md create mode 100644 doc/runbooks/new-web-app/07-argocd-register.md create mode 100644 doc/runbooks/new-web-app/08-checklist.md create mode 100644 doc/runbooks/new-web-app/README.md create mode 100644 doc/runbooks/new-web-app/conventions.md diff --git a/README.md b/README.md index df520da..9207c23 100644 --- a/README.md +++ b/README.md @@ -80,4 +80,9 @@ classDef done fill:gold,stroke:indigo,stroke-width:4px,color:blue; class prepare_hd,nodeId2 done; ``` +## Documentation + +- 📚 [`doc/`](doc/README.md) — ADR (décisions d'architecture) + runbooks. +- 🚀 [Runbook : mettre en service une nouvelle application web](doc/runbooks/new-web-app/README.md) — dépôt Gitea, base de données, Vault, chart Helm, Terraform, CI, ArgoCD. + 🏹💻🪽 diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..f9262dd --- /dev/null +++ b/doc/README.md @@ -0,0 +1,35 @@ +[Factory](../README.md) > **Doc** + +# Documentation Factory + +> **Last Updated:** 2026-05-31 +> **Status:** Production · maintenue activement +> **Related:** [README racine du dépôt](../README.md) · [Collection Ansible `arcodange.factory`](../ansible/arcodange/factory/README.md) + +## C'est quoi ? + +Le dossier `doc/` rassemble la documentation de la plateforme Arcodange (k3s + Gitea + ArgoCD + OpenTofu + Vault + Postgres, auto-hébergée sur 3 Raspberry Pi). On y trouve deux familles : + +- les **ADR** (records de décisions d'architecture), qui expliquent *pourquoi* la plateforme est faite ainsi ; +- les **runbooks**, qui expliquent *comment* exécuter une procédure opérationnelle de bout en bout. + +> [!NOTE] +> Convention du dépôt (cf. PR #8) : la documentation vit sous `doc/` (**singulier**). Un ancien dossier `docs/` (pluriel) a pu traîner non-suivi par git — ne pas y ajouter de contenu. + +## Sections + +| Section | Ce qu'on y trouve | Statut | +|---|---|---| +| [Runbooks](runbooks/README.md) | Procédures opérationnelles pas-à-pas (créer une app, etc.) | ✅ | +| [ADR](adr/README.md) | Décisions d'architecture + checklist de mise en place de la plateforme | ✅ | + +## Légende de statut + +✅ actif · 🟡 dégradé/beta · 🔴 critique/EOL · ⚠️ problème connu · ❌ désactivé + +## Comment éditer cette documentation + +1. **Ajouter une page** → la créer depuis le template adéquat **et** ajouter sa ligne dans la table d'index du `README.md` parent. +2. **Supprimer une page** → la marquer *Décommissionnée (date)* d'abord ; supprimer le fichier et sa ligne d'index ensemble une fois qu'elle est vraiment partie. +3. **Garder les liens croisés bidirectionnels** → quand on lie A→B, ajouter B→A. +4. **Mettre à jour `Last Updated:`** en tête de la racine concernée après tout changement de structure. diff --git a/doc/adr/README.md b/doc/adr/README.md index 1166776..5fdfe7f 100644 --- a/doc/adr/README.md +++ b/doc/adr/README.md @@ -18,9 +18,9 @@ - [ ] terrakube - [ ] prometheus/grafana - [ ] ansible AWX -- [ ] setup hello world web app - - [ ] manage postgres credentials - - [ ] protect public endpoint (crowdsec) +- [ ] setup hello world web app — 📖 procédure complète : [runbook « Nouvelle application web »](../runbooks/new-web-app/README.md) + - [ ] manage postgres credentials → [base de données](../runbooks/new-web-app/02-database.md) + [Vault plateforme](../runbooks/new-web-app/03-vault-platform.md) + - [ ] protect public endpoint (crowdsec) → [chart : ingress public](../runbooks/new-web-app/04-helm-chart.md) > [!NOTE] > Reference: [Arcodange _**Factory**_ Ansible Collection](/ansible/arcodange/factory/README.md) diff --git a/doc/runbooks/README.md b/doc/runbooks/README.md new file mode 100644 index 0000000..75ce0ca --- /dev/null +++ b/doc/runbooks/README.md @@ -0,0 +1,21 @@ +[Factory](../../README.md) > [Doc](../README.md) > **Runbooks** + +# Runbooks + +> **Scope :** procédures opérationnelles pas-à-pas de la plateforme Arcodange. Chaque runbook se lit du début à la fin et mène à un résultat vérifiable. Pour le *pourquoi* (décisions d'architecture), voir les [ADR](../adr/README.md). + +```mermaid +%%{init: {'theme': 'base'}}%% +flowchart LR + classDef node fill:#059669,stroke:#047857,color:#fff + OP["Opérateur"]:::node --> RB["Runbook
(procédure ordonnée)"]:::node --> RES["Résultat vérifiable
(app en ligne, etc.)"]:::node +``` + +## Index + +| Runbook | Résumé | Statut | +|---|---|---| +| [Nouvelle application web](new-web-app/README.md) | Créer une app web de zéro dans un nouveau dépôt Gitea : dépôt, base de données, Vault, chart Helm, Terraform, CI, ArgoCD | ✅ | + +> [!TIP] +> Pour ajouter un runbook : créer un dossier `kebab-case/` avec son `README.md` (front door : intro + diagramme + index), puis ajouter sa ligne ci-dessus. diff --git a/doc/runbooks/new-web-app/01-gitea-repo.md b/doc/runbooks/new-web-app/01-gitea-repo.md new file mode 100644 index 0000000..9009c81 --- /dev/null +++ b/doc/runbooks/new-web-app/01-gitea-repo.md @@ -0,0 +1,88 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **1. Dépôt Gitea** + +# 1. Créer le dépôt Gitea + +> **Status:** ✅ Active +> **Downstream:** [2. Base de données](02-database.md), [3. Vault plateforme](03-vault-platform.md) +> **Related:** [Conventions de nommage](conventions.md) · [6. Workflows CI](06-ci-workflows.md) + +--- + +## Summary + +Tout part d'un dépôt Gitea nommé exactement `` (voir [conventions](conventions.md)). Le créer **sous l'organisation `arcodange-org`** n'est pas un détail : c'est ce qui lui fait **hériter automatiquement des secrets Actions d'organisation** dont tous les workflows dépendent. Le dépôt contient un squelette fixe que les étapes suivantes viennent remplir. + +## Pourquoi sous `arcodange-org` + +Les workflows `.gitea/workflows/*` (voir [étape 6](06-ci-workflows.md)) référencent des secrets qui ne sont **pas** définis dans le dépôt mais au niveau de l'organisation et hérités par tous ses dépôts : + +| Secret d'organisation | Usage | +|---|---| +| `HOMELAB_CA_CERT` | CA interne (base64) pour parler en TLS à `vault.arcodange.lab` | +| `vault_oauth__sh_b64` | Script (base64) qui réalise l'échange OIDC Gitea → JWT Vault | +| `PACKAGES_TOKEN` | Token de push vers le registre d'images `gitea.arcodange.lab` | + +Ces secrets sont propagés par le rôle Ansible [`gitea_secret`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/roles/gitea_secret/defaults/main.yml) (`gitea_owner_type: organization`). + +> [!IMPORTANT] +> Un dépôt créé **hors** `arcodange-org` (par ex. sous l'org `arcodange`) n'hérite pas forcément de ces secrets. Si tu surcharges l'org (cf. `org:` à l'[étape 7](07-argocd-register.md)), assure-toi que les mêmes secrets y existent. + +## Options pour créer le dépôt + +| Méthode | Quand | Comment | +|---|---|---| +| UI Gitea | one-shot manuel | `https://gitea.arcodange.lab` → New Repository sous `arcodange-org` | +| MCP Gitea | depuis un agent | outil `mcp__gitea__create_repo` (cf. règle « Gitea = MCP, pas `gh` » du guide global) | +| Rôle Ansible `gitea_repo` | reproductible/inventaire | [`roles/gitea_repo`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/roles/gitea_repo/defaults/main.yml) | +| Ressource Terraform `gitea_repository` | tout-en-IaC | dans [`factory/iac`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac) (provider `go-gitea/gitea` déjà configuré) | + +## Squelette minimal du dépôt + +``` +/ +├── chart/ # chart Helm — ArgoCD déploie CE dossier (path: chart) → étape 4 +│ ├── Chart.yaml +│ ├── values.yaml +│ └── templates/ +├── iac/ # Terraform/OpenTofu de l'app → étape 5 +│ ├── providers.tf +│ ├── backend.tf +│ └── main.tf +├── .gitea/workflows/ # CI (tofu apply + build image) → étape 6 +│ ├── vault.yaml +│ └── dockerimage.yaml # uniquement si l'app build sa propre image +├── Dockerfile # uniquement si l'app build sa propre image +├── README.md +└── .gitignore +``` + +> [!IMPORTANT] +> Le dossier du chart **doit** s'appeler `chart/` et être à la racine : le template ArgoCD impose `path: chart` (cf. [étape 7](07-argocd-register.md)). Pas de `helm/`, pas de sous-dossier. + +## `.gitignore` recommandé + +Aligné sur les dépôts existants (exclut tout secret local) : + +```gitignore +.terraform +.terraform.* +.env +*.key +secrets/ +.DS_Store +``` + +## Le bot `tofu_module_reader` + +La CI de l'app clone le module Terraform partagé `tools` **en SSH** (cf. [étape 5](05-app-terraform.md)). C'est l'utilisateur restreint `tofu_module_reader` (créé dans [`factory/iac/gitea_tofu_ci_user.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac/gitea_tofu_ci_user.tf), clé privée dans Vault `kvv1/gitea/tofu_module_reader`) qui sert d'identité de lecture. Rien à faire de spécial, mais le dépôt `tools` doit rester lisible par ce bot. + +## Notes / contraintes + +- Le nom du dépôt = `` **exactement** (kebab-case minuscule). Voir [conventions](conventions.md) — ce nom se propage partout. +- Pas besoin de protéger `main` autrement que par la convention worktree/PR de l'équipe ; ArgoCD suit `HEAD`. + +## Related + +- [2. Base de données](02-database.md) — provisionner la base, en parallèle de l'étape 3. +- [3. Vault plateforme](03-vault-platform.md) — déclarer l'app côté Vault, en parallèle de l'étape 2. +- [6. Workflows CI](06-ci-workflows.md) — consomme les secrets d'org hérités ici. diff --git a/doc/runbooks/new-web-app/02-database.md b/doc/runbooks/new-web-app/02-database.md new file mode 100644 index 0000000..b6dc0a6 --- /dev/null +++ b/doc/runbooks/new-web-app/02-database.md @@ -0,0 +1,93 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **2. Base de données** + +# 2. Provisionner la base de données + +> **Status:** ✅ Active +> **Upstream:** [1. Dépôt Gitea](01-gitea-repo.md) +> **Downstream:** [5. Terraform de l'app](05-app-terraform.md) +> **Related:** [3. Vault plateforme](03-vault-platform.md) · [4. Chart Helm](04-helm-chart.md) · [Conventions de nommage](conventions.md) + +--- + +## Summary + +La base de l'app et son **rôle propriétaire** `_role` sont créés par un Terraform **côté plateforme** (`factory/postgres/iac`), pas dans le dépôt de l'app. On ajoute simplement le nom de l'app à une liste, et la CI de `factory` applique. L'app, elle, ne se connectera **jamais** avec un mot de passe statique : elle obtiendra des identifiants éphémères de Vault (cf. [étape 4](04-helm-chart.md)) qui héritent de `_role`. + +## Action + +Ajouter `""` au set `applications` de [`factory/postgres/iac/terraform.tfvars`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/terraform.tfvars) : + +```hcl +applications = [ + "webapp", + "erp", + "crowdsec", + "plausible", + "dance-lessons-coach", + "", # ← ajouter +] +``` + +Puis pousser : la CI applique automatiquement (voir plus bas). + +## Ce que ça crée + +[`postgres/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/main.tf) itère `for_each` sur le set et crée, **par app** : + +| Ressource | Nom | Rôle | +|---|---|---| +| `postgresql_role` | `_role` | Rôle **non-login**, propriétaire de la base | +| `postgresql_grant_role` | `_role` → `credentials_editor` (WITH ADMIN OPTION) | Laisse Vault rattacher les users dynamiques à ce rôle | +| `postgresql_database` | `` | La base (owner `_role`, `template0`, `alter_object_ownership`) | +| `postgresql_function` | `user_lookup()` (dans la base ``) | Authentification pgbouncer (lit `pg_shadow`) | +| `postgresql_grant` | EXECUTE sur `user_lookup` → `pgbouncer_auth` | Autorise pgbouncer à résoudre les users | + +Extrait clé : + +```hcl +resource "postgresql_role" "app_role" { + for_each = var.applications + name = "${each.value}_role" + login = false # non-login : ne sert que de "porteur de droits" +} +resource "postgresql_database" "app_db" { + for_each = var.applications + name = each.value + owner = postgresql_role.app_role[each.value].name + template = "template0" + alter_object_ownership = true +} +``` + +## Comment c'est appliqué + +Le workflow [`factory/.gitea/workflows/postgres.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/.gitea/workflows/postgres.yaml) se déclenche sur tout changement de `postgres/**/*.tf` ou `*.tfvars` : + +```mermaid +%%{init: {'theme': 'base'}}%% +flowchart LR + classDef ci fill:#059669,stroke:#047857,color:#fff + classDef db fill:#2563eb,stroke:#1e40af,color:#fff + PUSH["push tfvars"]:::ci --> JWT["OIDC Gitea → JWT Vault
(role gitea_cicd)"]:::ci + JWT --> READ["lit kvv1/postgres/credentials
→ TF_VAR_postgres_*"]:::ci + READ --> APPLY["tofu apply postgres/iac"]:::ci + APPLY --> DB["base ‹app› + ‹app›_role"]:::db +``` + +Le provider PostgreSQL pointe l'hôte `192.168.1.202` (`sslmode=disable`, `superuser=true`) et s'authentifie avec le compte `credentials_editor`, dont les identifiants sont dans Vault à `kvv1/postgres/credentials_editor/credentials`. + +## Le modèle de connexion (à retenir) + +> [!IMPORTANT] +> L'application **ne se connecte pas** directement à Postgres avec un user fixe. Elle vise **`pgbouncer.tools:5432`** et utilise des **users dynamiques courts** émis par Vault, qui héritent de `_role` (donc des droits sur la base ``). C'est l'étape 4 (chart + VSO) et l'étape 5 (rôle Vault `creds/`) qui câblent ça. Ici, on ne fait qu'établir *la base et le rôle propriétaire*. + +## Notes / contraintes + +- `credentials_editor` est un compte unique partagé par toutes les apps, à fort privilège (il peut créer des rôles). Il sert aussi de compte de connexion au moteur Postgres de Vault (cf. [étape 3](03-vault-platform.md)). +- La fonction `user_lookup()` est indispensable au mode `auth_query` de pgbouncer ; elle est `security_definer` et n'est exécutable que par `pgbouncer_auth`. + +## Related + +- [3. Vault plateforme](03-vault-platform.md) — la connexion Vault→Postgres réutilise `credentials_editor` ; à faire en parallèle. +- [5. Terraform de l'app](05-app-terraform.md) — le module `app_roles` fait `GRANT _role TO …` : il **exige** que `_role` existe déjà (créé ici). +- [4. Chart Helm](04-helm-chart.md) — où la connexion `pgbouncer.tools` + creds dynamiques est configurée. diff --git a/doc/runbooks/new-web-app/03-vault-platform.md b/doc/runbooks/new-web-app/03-vault-platform.md new file mode 100644 index 0000000..164d8ea --- /dev/null +++ b/doc/runbooks/new-web-app/03-vault-platform.md @@ -0,0 +1,89 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **3. Vault plateforme** + +# 3. Déclarer l'app côté Vault plateforme + +> **Status:** ✅ Active +> **Upstream:** [1. Dépôt Gitea](01-gitea-repo.md) +> **Downstream:** [5. Terraform de l'app](05-app-terraform.md), [6. Workflows CI](06-ci-workflows.md) +> **Related:** [2. Base de données](02-database.md) · [4. Chart Helm](04-helm-chart.md) · [Conventions de nommage](conventions.md) + +--- + +## Summary + +Avant que la CI de l'app puisse gérer ses propres secrets, Vault doit connaître l'app : il lui faut un **rôle JWT de CI** (`gitea_cicd_`) pour que la pipeline s'authentifie, une **policy CI** (`-ops`) qui l'autorise à créer ses rôles Postgres/K8s, et une **policy runtime** (``) que le pod utilisera. Tout ça est généré par un module, depuis une seule ligne ajoutée côté plateforme dans le dépôt `tools`. + +## Action + +Ajouter une entrée pour l'app au set `applications` de [`tools/hashicorp-vault/iac/terraform.tfvars`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/terraform.tfvars) : + +```hcl +applications = [ + { name = "webapp" }, + { name = "erp" }, + { name = "" }, # ← ajouter + # options possibles : + # { + # name = "" + # ops_policies = ["factory__cf_r2_arcodange_tf"] # policies ops supplémentaires (ex. token Cloudflare) + # service_account_names = ["cloudflared"] # SA additionnels autorisés à prendre la policy runtime + # service_account_namespaces = ["tools"] # namespaces additionnels + # }, +] +``` + +Puis pousser : la CI [`tools/.gitea/workflows/vault.yaml`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/.gitea/workflows/vault.yaml) applique le Terraform. + +## Ce que ça crée + +Le module [`app_policy`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_policy/main.tf) (appelé en `for_each` depuis [`main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf)) crée, **par app** : + +| Ressource Vault | Nom | À quoi ça sert | +|---|---|---| +| `vault_jwt_auth_backend_role` | `gitea_cicd_` | **Identité de la CI** : la pipeline de l'app s'y authentifie (mount `gitea_jwt`, `user_claim=email`, `bound_audiences=[gitea_app_id]`) | +| `vault_policy` (ops) | `-ops` | Droits CI : créer `postgres/roles/*`, `auth/kubernetes/role/*`, éditer `kvv2//*`, lire les secrets de bootstrap google/gitea | +| `vault_identity_group` | `-ops` | Groupe Vault rattachant les comptes Gitea à la policy ops | +| `vault_policy` (runtime) | `` | Droits **du pod** : lire `kvv2/data//*` et `postgres/creds/*` | + +Extraits clés : + +```hcl +resource "vault_jwt_auth_backend_role" "gitea_jwt_cicd" { + backend = data.vault_auth_backend.gitea_jwt.path # "gitea_jwt" + role_name = "gitea_cicd_${local.name}" + token_policies = concat(["default"], var.ops_policies) + bound_audiences = [var.gitea_app_id] + user_claim = "email" + role_type = "jwt" +} + +resource "vault_policy" "app" { # policy runtime du pod + name = local.name # = "" + policy = data.vault_policy_document.app.hcl # read kvv2/data//* + postgres/creds/* +} +``` + +## Prérequis plateforme (déjà là) + +Ces fondations vivent dans [`tools/hashicorp-vault/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf) et n'ont **pas** à être recréées : + +- mounts `kvv2` (KV v2), `postgres` (moteur de bases de données), `transit` (cache VSO), auth `kubernetes` ; +- la **connexion Vault→Postgres** (`vault_database_secret_backend_connection`) qui se connecte à `pgbouncer.tools:5432/postgres` avec le compte `credentials_editor` (issu de l'[étape 2](02-database.md)) ; +- `var.gitea_app_id` = l'id de l'application OAuth2 Gitea, réglé une fois au setup ([`gitea_oidc_auth.yml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml)). + +## Pourquoi cette étape vient avant la CI de l'app + +> [!IMPORTANT] +> C'est ici que `gitea_cicd_` **naît**. Le `iac/providers.tf` de l'app ([étape 5](05-app-terraform.md)) et le step `vault-action` du workflow ([étape 6](06-ci-workflows.md)) s'authentifient avec ce rôle. S'il n'existe pas encore, la toute première exécution de la CI de l'app échoue à l'authentification Vault. **Appliquer cette étape (et l'[étape 2](02-database.md)) avant de pousser le `iac/` de l'app.** + +## Notes / contraintes + +- Découpage des privilèges : la policy **`-ops`** (CI, large) est distincte de la policy **``** (runtime, en lecture seule sur ses propres secrets). Le pod ne peut jamais créer de rôles. +- `ops_policies` permet d'octroyer à la CI des droits transverses (ex. `cms` lit un token Cloudflare R2 via `factory__cf_r2_arcodange_tf`). + +## Related + +- [2. Base de données](02-database.md) — fournit `credentials_editor`, réutilisé par la connexion Vault→Postgres. +- [5. Terraform de l'app](05-app-terraform.md) — s'authentifie avec `gitea_cicd_` et crée `creds/` + le rôle K8s ``. +- [6. Workflows CI](06-ci-workflows.md) — le step `vault-action` et `tofu apply` utilisent `gitea_cicd_`. +- [4. Chart Helm](04-helm-chart.md) — le pod utilise la policy runtime `` via son ServiceAccount. diff --git a/doc/runbooks/new-web-app/04-helm-chart.md b/doc/runbooks/new-web-app/04-helm-chart.md new file mode 100644 index 0000000..24cbd51 --- /dev/null +++ b/doc/runbooks/new-web-app/04-helm-chart.md @@ -0,0 +1,183 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **4. Chart Helm** + +# 4. Le chart Helm de l'application + +> **Status:** ✅ Active +> **Upstream:** [5. Terraform de l'app](05-app-terraform.md) (crée les rôles Vault que le chart consomme) +> **Downstream:** [7. Enregistrement ArgoCD](07-argocd-register.md) (déploie ce chart) +> **Related:** [2. Base de données](02-database.md) · [3. Vault plateforme](03-vault-platform.md) · [6. Workflows CI](06-ci-workflows.md) · [Conventions de nommage](conventions.md) + +--- + +## Summary + +Le dossier `chart/` est l'**unité de déploiement** : c'est lui qu'ArgoCD applique (`path: chart`). Il décrit le Deployment, le Service, l'Ingress, et surtout le câblage des secrets via le **Vault Secrets Operator (VSO)** : pas un mot de passe en clair, des identifiants Postgres **dynamiques** injectés à l'exécution. On part d'un `helm create ` puis on ajoute les CRD Vault, la ConfigMap, et on ajuste l'ingress. + +## Structure du chart + +``` +chart/ +├── Chart.yaml # name: , version, appVersion (= tag image par défaut) +├── values.yaml # image, service, ingress, serviceAccount, autoscaling… +└── templates/ + ├── deployment.yaml # consomme vso-db-credentials + secretkv + ├── service.yaml # ClusterIP + ├── ingress.yaml # Traefik (voir patterns ci-dessous) + ├── serviceaccount.yaml # SA (serviceAccount.create: true) + ├── config.yaml # ConfigMap : env non-secrets (host DB = pgbouncer) + ├── vaultauth.yaml # VaultAuth : SA ↔ rôle Vault + ├── vaultdynamicsecret.yaml # creds Postgres dynamiques (postgres/creds/) + ├── vaultsecret.yaml # config statique (kvv2//config) + ├── hpa.yaml # désactivé par défaut + ├── _helpers.tpl # name/fullname/labels + └── NOTES.txt +``` + +> [!TIP] +> Bootstrap : `helm create ` génère deployment/service/ingress/serviceaccount/hpa/_helpers/NOTES. Il reste à **ajouter** `config.yaml` (ConfigMap), les 3 CRD Vault (`vaultauth`, `vaultdynamicsecret`, `vaultsecret`), et à **ajuster** `values.yaml` (image, ingress) + `deployment.yaml` (envFrom). Copier ceux d'[`erp/chart`](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/chart) ou [`webapp/chart`](https://gitea.arcodange.lab/arcodange-org/webapp/src/branch/main/chart) est le plus rapide. + +## Connexion à la base : via pgbouncer, jamais en direct + +La ConfigMap pointe **`pgbouncer.tools`** (le pooler dans le namespace `tools`), port 5432, base ``. Pas de host Postgres en dur, pas d'utilisateur statique. + +```yaml +# chart/templates/config.yaml — exemple erp +data: + DOLI_DB_TYPE: pgsql + DOLI_DB_HOST: pgbouncer.tools # ← le pooler, pas postgres directement + DOLI_DB_HOST_PORT: !!str 5432 + DOLI_DB_NAME: erp # = +``` + +```yaml +# chart/templates/config.yaml — exemple webapp (chaîne de connexion) +data: + DATABASE_URL: postgres://pgbouncer_auth:pgbouncer_auth@pgbouncer.tools/postgres?sslmode=disable +``` + +Le **vrai** user/mot de passe vient du Secret K8s `vso-db-credentials` (voir ci-dessous), pas de la ConfigMap. + +## Les secrets via VSO (le cœur) + +Trois CRD du Vault Secrets Operator (extraits du chart `erp`) : + +```yaml +# vaultauth.yaml — authentifie le pod auprès de Vault +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultAuth +spec: + method: kubernetes + mount: kubernetes + kubernetes: + role: erp # = rôle K8s Vault (créé à l'étape 5) + serviceAccount: {{ include "erp.serviceAccountName" . }} + audiences: [vault] +``` + +```yaml +# vaultdynamicsecret.yaml — identifiants Postgres dynamiques +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultDynamicSecret +spec: + mount: postgres + path: creds/erp # = postgres/creds/ + destination: + create: true + name: vso-db-credentials # Secret K8s consommé par le Deployment + rolloutRestartTargets: + - kind: Deployment + name: {{ include "erp.fullname" . }} # redémarre le pod à chaque rotation + vaultAuthRef: auth +``` + +```yaml +# vaultsecret.yaml — config statique (mots de passe admin initiaux, etc.) +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +spec: + type: kv-v2 + mount: kvv2 + path: erp/config # = kvv2//config (rempli à l'étape 5) + destination: + name: secretkv + create: true + refreshAfter: 24h + vaultAuthRef: auth +``` + +Le Deployment consomme ensuite `vso-db-credentials` (clés `username`/`password`) et `secretkv` (via `envFrom`/`secretKeyRef`). + +### Flux runtime + +```mermaid +%%{init: {'theme': 'base'}}%% +flowchart LR + classDef pod fill:#b45309,stroke:#92400e,color:#fff + classDef vault fill:#7c3aed,stroke:#6d28d9,color:#fff + classDef db fill:#2563eb,stroke:#1e40af,color:#fff + + SA["Pod · SA ‹app›"]:::pod + VA["VaultAuth
role ‹app› (k8s)"]:::vault + VDS["VaultDynamicSecret
postgres/creds/‹app›"]:::vault + VSS["VaultStaticSecret
kvv2/‹app›/config"]:::vault + SEC["Secret K8s
vso-db-credentials + secretkv"]:::pod + PGB["pgbouncer.tools:5432"]:::db + DB["base ‹app›
(user dynamique → ‹app›_role)"]:::db + + SA --> VA + VA --> VDS + VA --> VSS + VDS --> SEC + VSS --> SEC + SEC --> SA + SA --> PGB --> DB +``` + +> [!IMPORTANT] +> `serviceAccount.create` doit valoir `true` dans `values.yaml` : c'est ce SA `` que `VaultAuth` lie au rôle Vault ``. Sans lui, pas d'authentification, donc pas de creds DB. + +## Ingress : interne `.lab` vs public `.fr` + +Deux patterns selon l'exposition voulue (annotations Traefik dans `values.yaml`) : + +| | Interne (`.lab`) — ex. `erp` | Public (`.fr`) — ex. `webapp` | +|---|---|---| +| `entrypoints` | `websecure` | `web` | +| TLS | `router.tls: "true"` + `certresolver: letsencrypt` | (terminé en amont) | +| Middleware | `localIp@file` (restreint au LAN) | `kube-system-crowdsec@kubernetescrd` (WAF) | +| `nodeSelector` | — | `kubernetes.io/hostname: pi1` (garde l'IP source, point d'entrée réseau) | +| Hôte | `.arcodange.lab` | `.arcodange.fr` | + +```yaml +# values.yaml — ingress interne .lab (extrait erp) +ingress: + enabled: true + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt + traefik.ingress.kubernetes.io/router.middlewares: localIp@file + hosts: + - host: erp.arcodange.lab + paths: [{ path: /, pathType: Prefix }] +``` + +## Image + +| Cas | `image.repository` | Exemple | +|---|---|---| +| Image **maison** (build dans le dépôt) | `gitea.arcodange.lab/arcodange-org/` | `webapp` (cf. [étape 6](06-ci-workflows.md)) | +| Image **publique** | l'image upstream | `erp` → `dolibarr/dolibarr` | + +## Notes / contraintes + +- Si l'app stocke des fichiers, ajouter un `pvc.yaml` (`storageClassName: longhorn`, `accessModes: [ReadWriteMany]`, annotation `helm.sh/resource-policy: keep` pour survivre à un `helm uninstall`) — cf. `erp/chart/templates/pvc.yaml`. +- Les CRD VSO supposent que le VSO tourne dans le cluster (déployé par `tools`) et que le rôle K8s `` + le rôle `creds/` existent (étape 5). + +## Related + +- [5. Terraform de l'app](05-app-terraform.md) — crée `postgres/creds/`, le rôle K8s `` et remplit `kvv2//config` que ces CRD consomment. +- [2. Base de données](02-database.md) — la base `` et `pgbouncer.tools` ciblés ici. +- [3. Vault plateforme](03-vault-platform.md) — la policy runtime `` qu'utilise `VaultAuth`. +- [6. Workflows CI](06-ci-workflows.md) — construit l'image référencée par `image.repository`. +- [Référence VSO/secrets faisant autorité](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/README.md) — explication détaillée VaultConnection/VaultAuth/VaultDynamicSecret côté `tools` (à ne pas dupliquer ici). diff --git a/doc/runbooks/new-web-app/05-app-terraform.md b/doc/runbooks/new-web-app/05-app-terraform.md new file mode 100644 index 0000000..a5b98e5 --- /dev/null +++ b/doc/runbooks/new-web-app/05-app-terraform.md @@ -0,0 +1,96 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **5. Terraform de l'app** + +# 5. Le Terraform de l'application + +> **Status:** ✅ Active +> **Upstream:** [2. Base de données](02-database.md), [3. Vault plateforme](03-vault-platform.md) +> **Downstream:** [4. Chart Helm](04-helm-chart.md), [6. Workflows CI](06-ci-workflows.md) +> **Related:** [Conventions de nommage](conventions.md) + +--- + +## Summary + +Le `iac/` du dépôt déclare les ressources Vault **propres à l'app** : un rôle Postgres dynamique (`postgres/creds/`), un rôle d'authentification Kubernetes (``), et les secrets de config KV. Le gros du travail est fait par un **module partagé** (`app_roles`, dans `tools`) ; le dépôt se contente de l'appeler avec son nom et d'ajouter ses secrets spécifiques. + +## Les trois fichiers + +### `providers.tf` — s'authentifier à Vault avec le rôle CI de l'app + +```hcl +terraform { + required_providers { + vault = { source = "vault", version = "4.4.0" } + } +} +provider "vault" { + address = "https://vault.arcodange.lab" + auth_login_jwt { # JWT fourni par la CI via TERRAFORM_VAULT_AUTH_JWT + mount = "gitea_jwt" + role = "gitea_cicd_" # ← créé à l'étape 3 ; DOIT exister avant le 1er apply + } +} +``` + +### `backend.tf` — état distant sur GCS, préfixe par app + +```hcl +terraform { + backend "gcs" { + bucket = "arcodange-tf" + prefix = "/main" # ← un préfixe d'état dédié par app + } +} +``` + +### `main.tf` — appeler le module partagé + secrets de l'app + +```hcl +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 = "" + # database = "" # optionnel ; par défaut = name +} + +# Exemple : secrets de config statiques de l'app, écrits dans kvv2//config +resource "vault_kv_secret_v2" "config" { + mount = module.app_roles.mount_paths.kvv2 # "kvv2" + name = format("%sconfig", module.app_roles.kvv2_path_prefix) # "/config" + data_json = jsonencode({ + # … clés propres à l'app (ex. erp : DOLI_ADMIN_LOGIN/PASSWORD, DOLI_INSTANCE_UNIQUE_ID) … + }) +} +``` + +## Ce que le module `app_roles` crée + +[`modules/app_roles/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_roles/main.tf) : + +| Ressource | Effet | +|---|---| +| `vault_database_secret_backend_role` → `postgres/creds/` | À chaque demande : `CREATE ROLE "…" LOGIN PASSWORD … VALID UNTIL … ; GRANT _role TO "…"`. Le user éphémère **hérite** de `_role` (donc des droits sur la base). À la révocation : `REASSIGN OWNED … TO _role` + `REVOKE`. | +| `vault_kubernetes_auth_backend_role` → `` | Lie le SA `` du namespace `` aux policies `default` + `` (TTL 3600 s). C'est ce que `VaultAuth` cible (étape 4). | + +Sorties utiles : `mount_paths` (`{k8s, pg, kvv2}`), `kvv2_path_prefix` (`/`), `name`, `database`. + +## Dépendances (à respecter) + +> [!IMPORTANT] +> Ce Terraform **suppose** que deux choses existent déjà : +> - le rôle Postgres `_role` (créé à l'[étape 2](02-database.md)) — sinon le `GRANT _role TO …` du rôle dynamique est invalide ; +> - le rôle JWT `gitea_cicd_` et la policy `` (créés à l'[étape 3](03-vault-platform.md)) — sinon l'authentification du provider échoue / le rôle K8s ne peut référencer la policy. +> +> Ce `iac/` est appliqué par la CI de l'app, voir [étape 6](06-ci-workflows.md). Ne pas pousser ce dossier avant d'avoir appliqué les étapes 2 et 3. + +## Notes / contraintes + +- Le module est récupéré **en SSH** via le bot `tofu_module_reader` (cf. [étape 1](01-gitea-repo.md)) ; `?ref=main&depth=1` épingle la branche et limite le clone. +- L'état est isolé par `prefix = "/main"` : pas de collision entre apps dans le bucket `arcodange-tf`. +- `erp` et `webapp` montrent deux variantes : `erp` passe par `module "app_roles"` ; `webapp` inline encore les ressources (`vault_database_secret_backend_role` + `vault_kubernetes_auth_backend_role`) — préférer le module pour une nouvelle app. + +## Related + +- [2. Base de données](02-database.md) — fournit `_role`. +- [3. Vault plateforme](03-vault-platform.md) — fournit `gitea_cicd_` et la policy ``. +- [4. Chart Helm](04-helm-chart.md) — consomme `postgres/creds/`, le rôle K8s `` et `kvv2//config`. +- [6. Workflows CI](06-ci-workflows.md) — applique ce `iac/`. diff --git a/doc/runbooks/new-web-app/06-ci-workflows.md b/doc/runbooks/new-web-app/06-ci-workflows.md new file mode 100644 index 0000000..999125f --- /dev/null +++ b/doc/runbooks/new-web-app/06-ci-workflows.md @@ -0,0 +1,108 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **6. Workflows CI** + +# 6. Les workflows CI (`.gitea/workflows/`) + +> **Status:** ✅ Active +> **Upstream:** [1. Dépôt Gitea](01-gitea-repo.md) (secrets d'org), [3. Vault plateforme](03-vault-platform.md) (`gitea_cicd_`) +> **Related:** [4. Chart Helm](04-helm-chart.md) · [5. Terraform de l'app](05-app-terraform.md) · [7. Enregistrement ArgoCD](07-argocd-register.md) · [Conventions de nommage](conventions.md) + +--- + +## Summary + +Deux workflows Gitea Actions vivent dans le dépôt : **`vault.yaml`** applique le Terraform de l'app (`iac/`) en s'authentifiant à Vault via OIDC, et **`dockerimage.yaml`** (optionnel) construit l'image et la pousse au registre Gitea. Le déploiement lui-même n'est pas dans la CI : c'est ArgoCD qui s'en charge ([étape 7](07-argocd-register.md)). + +## `vault.yaml` — appliquer le `iac/` de l'app + +Déclenché sur tout changement de `iac/*.tf`. Deux jobs : obtenir un JWT depuis Gitea, puis `tofu apply`. + +```yaml +on: + workflow_dispatch: {} + push: { paths: ['iac/*.tf'] } + pull_request: { paths: ['iac/*.tf'] } + +# job 1 : échange OIDC Gitea → JWT (script base64 fourni en secret d'org) +# run: echo -n "${{ secrets.vault_oauth__sh_b64 }}" | base64 -d | bash + +# job 2 : lire les secrets de bootstrap puis appliquer +- name: read vault secret + uses: https://gitea.arcodange.lab/arcodange-org/vault-action.git@main + with: + url: https://vault.arcodange.lab + caCertificate: ${{ secrets.HOMELAB_CA_CERT }} + jwtGiteaOIDC: ${{ needs.gitea_vault_auth.outputs.gitea_vault_jwt }} + role: gitea_cicd_ # ← le rôle JWT de l'app (étape 3) + method: jwt + path: gitea_jwt + secrets: | + kvv1/google/credentials credentials | GOOGLE_BACKEND_CREDENTIALS ; + kvv1/gitea/tofu_module_reader ssh_private_key | TERRAFORM_SSH_KEY ; +- uses: actions/checkout@v4 +- name: terraform apply + uses: dflook/terraform-apply@v1 + with: { path: iac, auto_approve: true } +``` + +Les deux secrets lus servent au backend (clé GCS `GOOGLE_BACKEND_CREDENTIALS`) et au clone du module partagé en SSH (`TERRAFORM_SSH_KEY`, cf. [étape 5](05-app-terraform.md)). + +> [!WARNING] +> **Piège du `role:` copié-collé.** Le `role:` du step `vault-action` **et** le `role` de `iac/providers.tf` doivent tous deux être `gitea_cicd_`. L'exemple `erp` porte encore `role: gitea_cicd_webapp` dans son [`vault.yaml`](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/.gitea/workflows/vault.yaml) (reliquat de copier-coller) alors que son `providers.tf` utilise bien `gitea_cicd_erp`. Vérifie et aligne sur le nom de **ton** app, sinon la CI lit/écrit avec la mauvaise identité. + +## `dockerimage.yaml` — construire l'image (si image maison) + +À n'ajouter **que** si l'app build sa propre image (pas pour une image publique comme `erp`/Dolibarr). Déclenché au push sur `main`, en ignorant `README.md` et `chart/**` (changer le chart ne reconstruit pas l'image). + +```yaml +on: + push: { branches: [main], paths-ignore: ['README.md', 'chart/**'] } + +jobs: + build-and-push-image: + steps: + - uses: docker/login-action@v3 + with: + registry: gitea.arcodange.lab + username: ${{ github.actor }} + password: ${{ secrets.PACKAGES_TOKEN }} # secret d'org (étape 1) + - uses: actions/checkout@v4 + - run: | + TAGS="latest ${{ github.ref_name }}" + docker build -t app . + for TAG in $TAGS; do + docker tag app gitea.arcodange.lab/${{ github.repository }}:$TAG + docker push gitea.arcodange.lab/${{ github.repository }}:$TAG + done +``` + +L'image atterrit donc en `gitea.arcodange.lab/arcodange-org/:latest` — exactement ce que `image.repository` du chart référence ([étape 4](04-helm-chart.md)). Un `Dockerfile` multi-stage à la racine convient (cf. [`webapp/Dockerfile`](https://gitea.arcodange.lab/arcodange-org/webapp/src/branch/main/Dockerfile)). + +## Vue d'ensemble + +```mermaid +%%{init: {'theme': 'base'}}%% +flowchart TB + classDef ci fill:#059669,stroke:#047857,color:#fff + classDef out fill:#7c3aed,stroke:#6d28d9,color:#fff + PUSH["push sur main"]:::ci + PUSH -->|"iac/*.tf modifié"| TF["vault.yaml
OIDC → JWT → tofu apply iac/"]:::ci + PUSH -->|"code modifié"| IMG["dockerimage.yaml
build + push image"]:::ci + TF --> VR["creds/‹app› + rôle K8s ‹app› + KV"]:::out + IMG --> REG["registre gitea.arcodange.lab/arcodange-org/‹app›"]:::out +``` + +## Déploiement automatique sur nouvelle image + +Pour qu'ArgoCD redéploie quand une nouvelle image est poussée, on n'ajoute **rien** dans la CI : ce sont les annotations `argocd-image-updater` posées à l'[étape 7](07-argocd-register.md) (stratégie `digest`) qui surveillent le tag `latest`. + +## Notes / contraintes + +- `concurrency: cancel-in-progress` est activé sur les deux workflows : un nouveau push annule le run précédent sur la même ref. +- Le `vault-action` est lui-même un dépôt Gitea (`arcodange-org/vault-action`) épinglé `@main`. + +## Related + +- [3. Vault plateforme](03-vault-platform.md) — d'où vient `gitea_cicd_`. +- [5. Terraform de l'app](05-app-terraform.md) — ce que `vault.yaml` applique. +- [4. Chart Helm](04-helm-chart.md) — `image.repository` = l'image poussée ici. +- [7. Enregistrement ArgoCD](07-argocd-register.md) — déploie, et porte les annotations d'auto-update. diff --git a/doc/runbooks/new-web-app/07-argocd-register.md b/doc/runbooks/new-web-app/07-argocd-register.md new file mode 100644 index 0000000..9f54536 --- /dev/null +++ b/doc/runbooks/new-web-app/07-argocd-register.md @@ -0,0 +1,91 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **7. Enregistrement ArgoCD** + +# 7. Enregistrer l'app dans ArgoCD + +> **Status:** ✅ Active +> **Upstream:** [4. Chart Helm](04-helm-chart.md), [6. Workflows CI](06-ci-workflows.md) +> **Related:** [Conventions de nommage](conventions.md) · [Checklist](08-checklist.md) + +--- + +## Summary + +ArgoCD fonctionne en **app-of-apps** : le chart `factory/argocd` lit une liste d'applications dans son `values.yaml` et génère une ressource `Application` par entrée. Enregistrer l'app = ajouter son nom à cette liste. ArgoCD se charge ensuite de cloner le dépôt, déployer `chart/` dans le namespace ``, et resynchroniser à chaque push. + +## Action + +Ajouter `` sous `gitea_applications` dans [`factory/argocd/values.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/values.yaml) : + +```yaml +gitea_applications: + webapp: { … } + erp: + annotations: {} + : # ← cas simple + annotations: {} +``` + +Variante avec **auto-déploiement** sur nouvelle image (recommandé pour une image maison) : + +```yaml + : + annotations: + argocd-image-updater.argoproj.io/image-list: =gitea.arcodange.lab/arcodange-org/:latest + argocd-image-updater.argoproj.io/.update-strategy: digest +``` + +Options supplémentaires : + +| Champ | Quand l'utiliser | Effet | +|---|---|---| +| `org: arcodange` | dépôt hors `arcodange-org` | change le `repoURL` (défaut `arcodange-org`) | +| `syncPolicy: …` | contrôle manuel | surcharge la policy (défaut : `automated {prune, selfHeal}`) | + +## Ce que ça génère + +Le template [`argocd/templates/apps.yaml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/templates/apps.yaml) rend, pour chaque entrée : + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: + namespace: argocd +spec: + project: default + source: + repoURL: https://gitea.arcodange.lab/arcodange-org/ # = arcodange-org par défaut + targetRevision: HEAD + path: chart # ← d'où l'exigence du dossier chart/ + destination: + server: https://kubernetes.default.svc + namespace: # = nom de l'app + syncPolicy: + automated: { prune: true, selfHeal: true } + syncOptions: [ CreateNamespace=true ] # le namespace est créé tout seul +``` + +```mermaid +%%{init: {'theme': 'base'}}%% +flowchart LR + classDef gitops fill:#7c3aed,stroke:#6d28d9,color:#fff + classDef k8s fill:#2563eb,stroke:#1e40af,color:#fff + VAL["values.yaml
gitea_applications.‹app›"]:::gitops --> APP["Application ArgoCD
‹app›"]:::gitops + APP -->|"path: chart, HEAD"| SYNC["sync du dépôt
arcodange-org/‹app›"]:::gitops + SYNC --> NS["namespace ‹app›
(CreateNamespace=true)"]:::k8s + NS --> DEP["Deployment + Service + Ingress + CRD Vault"]:::k8s +``` + +## Notes / contraintes + +> [!IMPORTANT] +> `path: chart` et `namespace: ` sont **déduits du nom**, pas configurables par entrée. C'est pourquoi le dossier doit s'appeler `chart/` ([étape 1](01-gitea-repo.md)) et le nom doit être cohérent partout ([conventions](conventions.md)). + +- Le chart `factory/argocd` est lui-même réconcilié par ArgoCD (app-of-apps racine) : committer `values.yaml` sur `main` suffit à faire apparaître/synchroniser la nouvelle `Application`. Pas de `kubectl apply` manuel. +- `prune: true` + `selfHeal: true` : ArgoCD supprime ce qui n'est plus dans le chart et réécrase les dérives manuelles. En tenir compte avant tout `kubectl edit`. + +## Related + +- [4. Chart Helm](04-helm-chart.md) — le contenu déployé (le dossier `chart/`). +- [6. Workflows CI](06-ci-workflows.md) — les annotations `argocd-image-updater` collaborent avec l'image poussée. +- [8. Checklist](08-checklist.md) — vérifier que l'`Application` passe `Healthy`/`Synced`. diff --git a/doc/runbooks/new-web-app/08-checklist.md b/doc/runbooks/new-web-app/08-checklist.md new file mode 100644 index 0000000..13ce186 --- /dev/null +++ b/doc/runbooks/new-web-app/08-checklist.md @@ -0,0 +1,77 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **8. Checklist** + +# 8. Checklist & Definition of Done + +> **Status:** ✅ Active +> **Upstream:** [7. Enregistrement ArgoCD](07-argocd-register.md) +> **Related:** [README du runbook](README.md) · [Conventions de nommage](conventions.md) + +--- + +## Summary + +Récapitulatif imprimable de toute la procédure, dans l'ordre des dépendances. À cocher au fur et à mesure ; chaque ligne renvoie à sa page détaillée. + +## Ordre des dépendances + +``` +[01] Dépôt Gitea ──┬──> [02] base + _role ──┐ + └──> [03] gitea_cicd_ ────┤ (02 et 03 avant 05) + ▼ + [04+05+06] chart/ + iac/ + .gitea/ ── push → CI + ▼ + [07] ArgoCD ── déploie ── Runtime +``` + +## Checklist + +**Préparation** +- [ ] Nom `` choisi (kebab-case minuscule), cohérent partout — [conventions](conventions.md) + +**1 · Dépôt** — [détails](01-gitea-repo.md) +- [ ] Dépôt `arcodange-org/` créé (hérite `HOMELAB_CA_CERT`, `vault_oauth__sh_b64`, `PACKAGES_TOKEN`) +- [ ] Squelette en place : `chart/`, `iac/`, `.gitea/workflows/`, `.gitignore` (+ `Dockerfile` si image maison) + +**2 · Base de données** — [détails](02-database.md) +- [ ] `""` ajouté à `factory/postgres/iac/terraform.tfvars` +- [ ] CI `factory` « Postgres » verte → base `` + rôle `_role` créés + +**3 · Vault plateforme** — [détails](03-vault-platform.md) +- [ ] `{ name = "" }` ajouté à `tools/hashicorp-vault/iac/terraform.tfvars` +- [ ] CI `tools` « Vault » verte → `gitea_cicd_`, policies `` / `-ops` créées + +> [!IMPORTANT] +> Ne pas pousser le `iac/` de l'app (étape 5/6) tant que **2 et 3** ne sont pas appliquées : la CI Terraform de l'app en dépend. + +**4 · Chart Helm** — [détails](04-helm-chart.md) +- [ ] `Chart.yaml` (`name: `), `values.yaml` (image, ingress, `serviceAccount.create: true`) +- [ ] `config.yaml` pointe `pgbouncer.tools` / base `` +- [ ] `vaultauth.yaml` (role ``), `vaultdynamicsecret.yaml` (`creds/`), `vaultsecret.yaml` (`kvv2//config`) +- [ ] Ingress choisi : interne `.lab` (websecure + letsencrypt + `localIp@file`) ou public `.fr` (web + crowdsec + `nodeSelector pi1`) + +**5 · Terraform de l'app** — [détails](05-app-terraform.md) +- [ ] `providers.tf` : role `gitea_cicd_` +- [ ] `backend.tf` : prefix `/main` +- [ ] `main.tf` : `module "app_roles"` (`name = ""`) + secrets `kvv2//config` + +**6 · Workflows CI** — [détails](06-ci-workflows.md) +- [ ] `vault.yaml` : `role: gitea_cicd_` **aligné** (pas un reliquat copié-collé) ⚠️ +- [ ] `dockerimage.yaml` + `Dockerfile` (si image maison) → push `gitea.arcodange.lab/arcodange-org/` +- [ ] Push → CI « vault » verte (`creds/` + rôle K8s `` + KV créés), CI « image » verte + +**7 · ArgoCD** — [détails](07-argocd-register.md) +- [ ] `` ajouté sous `gitea_applications` dans `factory/argocd/values.yaml` (+ annotations image-updater si voulu) +- [ ] Commit sur `main` → `Application` `` apparaît dans ArgoCD + +## Definition of Done + +- [ ] `Application` ArgoCD `` = **Synced** + **Healthy** +- [ ] Pod `Running` dans le namespace `` (SA ``) +- [ ] Secret K8s `vso-db-credentials` présent et **roté** par VSO (TTL ~1 h) ; le pod redémarre à la rotation +- [ ] L'app répond sur son ingress (`.arcodange.lab` ou `.arcodange.fr`) +- [ ] Connexion DB OK via `pgbouncer.tools` avec un user dynamique héritant de `_role` + +## Related + +- [README du runbook](README.md) — vue d'ensemble + carte de bout en bout. +- [Conventions de nommage](conventions.md) — la cohérence du nom ``, source de la plupart des ratés. diff --git a/doc/runbooks/new-web-app/README.md b/doc/runbooks/new-web-app/README.md new file mode 100644 index 0000000..dc2eb51 --- /dev/null +++ b/doc/runbooks/new-web-app/README.md @@ -0,0 +1,104 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > **Nouvelle application web** + +# Mettre en service une nouvelle application web + +> **Last Updated:** 2026-05-31 +> **Status:** ✅ Procédure courante +> **Related:** [Conventions de nommage](conventions.md) · [Checklist](08-checklist.md) · [ADR CI/CD](../../adr/03_cicd_gitea_action_argocd.md) · [ADR Vault](../../adr/04_tool_hashicorp_vault.md) + +## C'est quoi ? + +Ce runbook décrit, de zéro, comment faire vivre une nouvelle application web sur la plateforme Arcodange. Le pattern est **GitOps** : l'app habite son propre dépôt Gitea, sa base de données et ses accès Vault sont provisionnés par Terraform/OpenTofu, et **ArgoCD** déploie son chart Helm dans un namespace dédié. Les identifiants Postgres ne sont jamais écrits en clair : ils sont générés à la volée par Vault et injectés dans le pod par le **Vault Secrets Operator (VSO)**. + +La mécanique est répartie sur **trois dépôts** — le dépôt plateforme [`factory`](https://gitea.arcodange.lab/arcodange-org/factory), le dépôt des services partagés [`tools`](https://gitea.arcodange.lab/arcodange-org/tools), et le **nouveau dépôt de l'app** — avec des **dépendances d'ordre** strictes (voir plus bas). Les exemples de référence sont [`erp`](https://gitea.arcodange.lab/arcodange-org/erp) (image publique + DB) et [`webapp`](https://gitea.arcodange.lab/arcodange-org/webapp) (image maison + DB). + +**Sert à :** + +1. Créer un dépôt Gitea et son squelette (`chart/`, `iac/`, `.gitea/workflows/`). +2. Provisionner la base de données, son rôle propriétaire, et les accès Vault (statiques + dynamiques). +3. Déployer l'app via ArgoCD et l'exposer derrière Traefik (interne `.lab` ou public `.fr` + CrowdSec). + +## Carte de bout en bout + +```mermaid +%%{init: {'theme': 'base'}}%% +flowchart TB + classDef step fill:#2563eb,stroke:#1e40af,color:#fff + classDef plat fill:#059669,stroke:#047857,color:#fff + classDef gitops fill:#7c3aed,stroke:#6d28d9,color:#fff + classDef run fill:#b45309,stroke:#92400e,color:#fff + + REPO["1 · Dépôt Gitea
arcodange-org/‹app›"]:::step + DB["2 · factory/postgres/iac
base ‹app› + rôle ‹app›_role"]:::plat + VAULT["3 · tools/hashicorp-vault/iac
gitea_cicd_‹app› + policies ‹app› / ‹app›-ops"]:::plat + CONTENT["4·5·6 · chart/ + iac/ + .gitea/
push → CI build image & tofu apply"]:::step + ARGO["7 · factory/argocd/values.yaml
→ Application ArgoCD (ns ‹app›)"]:::gitops + POD["Runtime · Pod(SA ‹app›) → VSO → Vault
creds/‹app› → pgbouncer.tools → base ‹app›"]:::run + + REPO --> DB + REPO --> VAULT + DB --> CONTENT + VAULT --> CONTENT + CONTENT --> ARGO + ARGO --> POD +``` + +## Ordre des opérations (le point le plus important) + +> [!IMPORTANT] +> Les étapes ne sont pas interchangeables. Le rôle JWT de CI `gitea_cicd_` (étape 3) et le rôle Postgres `_role` (étape 2) doivent **exister avant** que la CI Terraform de l'app (étape 6 appliquant l'étape 5) ne s'exécute — sinon l'authentification Vault de la CI échoue, ou le module `app_roles` n'a pas de `_role` à qui rattacher les credentials dynamiques. + +``` +[01] Dépôt Gitea sous arcodange-org (hérite les secrets CI d'org) + │ + ├──> [02] factory/postgres/iac → base + _role + user_lookup() + │ + └──> [03] tools/hashicorp-vault/iac → gitea_cicd_ (JWT CI) + policies / -ops + │ (02 et 03 indépendants entre eux, mais TOUS DEUX avant 05) + ▼ +[04+05+06] Contenu du dépôt : chart/ + iac/ + .gitea/workflows/ (+ Dockerfile) + │ push → CI « dockerimage » build l'image · CI « vault » applique iac/ + │ → creds/ (rôle DB dynamique) + rôle K8s + secrets KV + ▼ +[07] factory/argocd/values.yaml → ArgoCD crée l'Application → déploie le chart dans le namespace + ▼ +Runtime : Pod(SA ) → VSO → VaultAuth(role ) → creds/ + → user PG dynamique héritant de _role → pgbouncer.tools → base +``` + +## Prérequis plateforme (déjà en place) + +Ces fondations existent et ne sont **pas** à refaire pour chaque app : + +| Brique | Où | Rôle | +|---|---|---| +| Mounts Vault `kvv2`, `postgres`, `transit`, auth `kubernetes` | [`tools/hashicorp-vault/iac/main.tf`](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/main.tf) | Moteurs de secrets + auth K8s | +| Connexion Vault→Postgres (via `pgbouncer.tools`, user `credentials_editor`) | idem | Permet à Vault d'émettre des users PG dynamiques | +| Rôle JWT de bootstrap `gitea_cicd` + app OAuth2 Gitea (`gitea_app_id`) | [`gitea_oidc_auth.yml`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml) | Échange OIDC Gitea → JWT Vault dans la CI | +| Bot `tofu_module_reader` (clé SSH dans `kvv1/gitea/tofu_module_reader`) | [`factory/iac/gitea_tofu_ci_user.tf`](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/iac/gitea_tofu_ci_user.tf) | Laisse la CI cloner le module partagé `tools` en SSH | +| Secrets Actions d'organisation (`HOMELAB_CA_CERT`, `vault_oauth__sh_b64`, `PACKAGES_TOKEN`) | org Gitea `arcodange-org` | Hérités par tout dépôt de l'org | + +## Index des étapes + +| # | Page | Ce qu'on y fait | Statut | +|---|---|---|---| +| — | [Conventions de nommage](conventions.md) | Le nom `` réutilisé à l'identique partout (à lire en premier) | ✅ | +| 01 | [Dépôt Gitea](01-gitea-repo.md) | Créer le dépôt sous `arcodange-org` + squelette | ✅ | +| 02 | [Base de données](02-database.md) | `factory/postgres/iac` → base `` + rôle `_role` | ✅ | +| 03 | [Vault plateforme](03-vault-platform.md) | `tools/hashicorp-vault/iac` → `gitea_cicd_` + policies | ✅ | +| 04 | [Chart Helm](04-helm-chart.md) | Le chart de l'app (DB via pgbouncer, secrets VSO, ingress) | ✅ | +| 05 | [Terraform de l'app](05-app-terraform.md) | `iac/` → module `app_roles` (creds dynamiques + rôle K8s) | ✅ | +| 06 | [Workflows CI](06-ci-workflows.md) | `.gitea/workflows/` : `tofu apply` + build image | ✅ | +| 07 | [Enregistrement ArgoCD](07-argocd-register.md) | `factory/argocd/values.yaml` → Application + déploiement | ✅ | +| 08 | [Checklist](08-checklist.md) | Récapitulatif ordonné + definition of done | ✅ | + +## Légende de statut + +✅ actif · 🟡 dégradé/beta · 🔴 critique/EOL · ⚠️ problème connu · ❌ désactivé + +## Comment éditer ce runbook + +1. **Ajouter une page** → la créer depuis le template tree-docs adéquat **et** ajouter sa ligne dans l'index ci-dessus. +2. **Garder les liens croisés bidirectionnels** → toute dépendance citée dans une page (`Upstream`/`Downstream`) doit avoir sa réciproque sur l'autre page. +3. **Mettre à jour `Last Updated:`** ci-dessus après tout changement de structure. +4. Les exemples cités (`erp`, `webapp`) sont vivants : revérifier les snippets contre le code réel avant de s'y fier aveuglément. diff --git a/doc/runbooks/new-web-app/conventions.md b/doc/runbooks/new-web-app/conventions.md new file mode 100644 index 0000000..4569e47 --- /dev/null +++ b/doc/runbooks/new-web-app/conventions.md @@ -0,0 +1,55 @@ +[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **Conventions de nommage** + +# Conventions de nommage + +> **Pourquoi cette page.** Un seul nom — `` — est réutilisé à l'identique dans une dizaine de systèmes (Gitea, Postgres, Vault, Kubernetes, ArgoCD, GCS, DNS). Toutes les étapes du runbook en dépendent ; on le centralise ici pour ne pas le ré-expliquer partout. +> **Audience.** Quiconque crée ou audite une app sur la plateforme. +> **Status.** Actif · revu le 2026-05-31. + +--- + +## TL;DR + +Choisis **un** identifiant `` en **kebab-case minuscule** (`webapp`, `erp`, `dance-lessons-coach`, `url-shortener`). Ce nom devient, **sans variation**, la clé de toutes les ressources. Une seule faute de frappe quelque part casse la chaîne (auth Vault, rattachement DB, sync ArgoCD). + +## Le nom `` dans chaque système + +| Système | Identifiant dérivé de `` | Exemple (`erp`) | Source de vérité | +|---|---|---|---| +| Dépôt Gitea | `arcodange-org/` (ou `arcodange/` si `org` surchargé) | `arcodange-org/erp` | [argocd/templates/apps.yaml](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/argocd/templates/apps.yaml) | +| Base PostgreSQL | `` | `erp` | [postgres/iac/main.tf](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/postgres/iac/main.tf) | +| Rôle propriétaire PG (non-login) | `_role` | `erp_role` | postgres/iac/main.tf | +| Rôle DB dynamique Vault | `postgres/creds/` | `postgres/creds/erp` | [modules/app_roles/main.tf](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_roles/main.tf) | +| Rôle d'auth Kubernetes (Vault) | `` | `erp` | modules/app_roles/main.tf | +| Policy Vault **runtime** (pod) | `` | `erp` | [modules/app_policy/main.tf](https://gitea.arcodange.lab/arcodange-org/tools/src/branch/main/hashicorp-vault/iac/modules/app_policy/main.tf) | +| Policy Vault **CI** (ops) | `-ops` | `erp-ops` | modules/app_policy/main.tf | +| Rôle JWT de CI (Vault) | `gitea_cicd_` | `gitea_cicd_erp` | modules/app_policy/main.tf | +| Groupe d'identité Vault | `-ops` | `erp-ops` | modules/app_policy/main.tf | +| Secret KV de config | `kvv2//config` | `kvv2/erp/config` | modules/app_roles (sortie `kvv2_path_prefix`) | +| Namespace Kubernetes | `` | `erp` | apps.yaml (`CreateNamespace=true`) | +| ServiceAccount Kubernetes | `` | `erp` | [chart/templates/serviceaccount.yaml](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/chart/templates/serviceaccount.yaml) | +| Application ArgoCD | `` | `erp` | apps.yaml | +| Préfixe d'état OpenTofu (GCS) | `/main` | `erp/main` | [iac/backend.tf](https://gitea.arcodange.lab/arcodange-org/erp/src/branch/main/iac/backend.tf) | +| Domaine interne | `.arcodange.lab` | `erp.arcodange.lab` | chart/values.yaml (ingress) | +| Domaine public | `.arcodange.fr` | `webapp.arcodange.fr` | chart/values.yaml (ingress) | + +## Pourquoi cette uniformité est structurante + +Les briques se « branchent » entre elles **par convention de nom**, pas par configuration explicite : + +- Le module `app_roles` génère un user PG dynamique avec `GRANT _role TO …` → il **suppose** que `_role` (créé à l'[étape 2](02-database.md)) porte exactement ce nom. +- Le `VaultDynamicSecret` du chart lit `postgres/creds/` → il **suppose** que le rôle Vault (créé à l'[étape 5](05-app-terraform.md)) porte exactement ``. +- L'`Application` ArgoCD déduit `repoURL=.../`, `path=chart`, `namespace=` du seul nom → le dépôt ([étape 1](01-gitea-repo.md)) et le namespace doivent matcher. + +✅ **Utilise un nom court, stable, kebab-case** dès le départ. +❌ **N'introduis pas** de variantes (`my_app` vs `my-app`, `MyApp`, pluriels) : rien ne te préviendra, l'app échouera silencieusement à se connecter ou à se déployer. + +## Références croisées + +- [01 · Dépôt Gitea](01-gitea-repo.md) — fixe `` comme nom de dépôt sous `arcodange-org`. +- [02 · Base de données](02-database.md) — crée `` et `_role`. +- [03 · Vault plateforme](03-vault-platform.md) — crée `gitea_cicd_`, policies `` / `-ops`. +- [04 · Chart Helm](04-helm-chart.md) — référence ``, `creds/`, `kvv2//config`. +- [05 · Terraform de l'app](05-app-terraform.md) — appelle `app_roles` avec `name=`. +- [06 · Workflows CI](06-ci-workflows.md) — s'authentifie avec `gitea_cicd_`. +- [07 · Enregistrement ArgoCD](07-argocd-register.md) — déclare `` dans `gitea_applications`.