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`.