docs(runbooks): add "new web app" setup runbook under doc/runbooks/
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_<app> + 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) <noreply@anthropic.com>
This commit is contained in:
183
doc/runbooks/new-web-app/04-helm-chart.md
Normal file
183
doc/runbooks/new-web-app/04-helm-chart.md
Normal file
@@ -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 <app>` puis on ajoute les CRD Vault, la ConfigMap, et on ajuste l'ingress.
|
||||
|
||||
## Structure du chart
|
||||
|
||||
```
|
||||
chart/
|
||||
├── Chart.yaml # name: <app>, 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 <app> (serviceAccount.create: true)
|
||||
├── config.yaml # ConfigMap : env non-secrets (host DB = pgbouncer)
|
||||
├── vaultauth.yaml # VaultAuth : SA <app> ↔ rôle Vault <app>
|
||||
├── vaultdynamicsecret.yaml # creds Postgres dynamiques (postgres/creds/<app>)
|
||||
├── vaultsecret.yaml # config statique (kvv2/<app>/config)
|
||||
├── hpa.yaml # désactivé par défaut
|
||||
├── _helpers.tpl # name/fullname/labels
|
||||
└── NOTES.txt
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Bootstrap : `helm create <app>` 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 `<app>`. 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 # = <app>
|
||||
```
|
||||
|
||||
```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 <app> (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/<app>
|
||||
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/<app>/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<br>role ‹app› (k8s)"]:::vault
|
||||
VDS["VaultDynamicSecret<br>postgres/creds/‹app›"]:::vault
|
||||
VSS["VaultStaticSecret<br>kvv2/‹app›/config"]:::vault
|
||||
SEC["Secret K8s<br>vso-db-credentials + secretkv"]:::pod
|
||||
PGB["pgbouncer.tools:5432"]:::db
|
||||
DB["base ‹app›<br>(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 `<app>` que `VaultAuth` lie au rôle Vault `<app>`. 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 | `<app>.arcodange.lab` | `<app>.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/<app>` | `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 `<app>` + le rôle `creds/<app>` existent (étape 5).
|
||||
|
||||
## Related
|
||||
|
||||
- [5. Terraform de l'app](05-app-terraform.md) — crée `postgres/creds/<app>`, le rôle K8s `<app>` et remplit `kvv2/<app>/config` que ces CRD consomment.
|
||||
- [2. Base de données](02-database.md) — la base `<app>` et `pgbouncer.tools` ciblés ici.
|
||||
- [3. Vault plateforme](03-vault-platform.md) — la policy runtime `<app>` 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).
|
||||
Reference in New Issue
Block a user