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>
8.0 KiB
Factory > Doc > Runbooks > Nouvelle application web > 4. Chart Helm
4. Le chart Helm de l'application
Status: ✅ Active Upstream: 5. Terraform de l'app (crée les rôles Vault que le chart consomme) Downstream: 7. Enregistrement ArgoCD (déploie ce chart) Related: 2. Base de données · 3. Vault plateforme · 6. Workflows CI · Conventions de nommage
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 à ajouterconfig.yaml(ConfigMap), les 3 CRD Vault (vaultauth,vaultdynamicsecret,vaultsecret), et à ajustervalues.yaml(image, ingress) +deployment.yaml(envFrom). Copier ceux d'erp/chartouwebapp/chartest 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.
# 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>
# 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) :
# 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]
# 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
# 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
%%{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.createdoit valoirtruedansvalues.yaml: c'est ce SA<app>queVaultAuthlie 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 |
# 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) |
| Image publique | l'image upstream | erp → dolibarr/dolibarr |
Notes / contraintes
- Si l'app stocke des fichiers, ajouter un
pvc.yaml(storageClassName: longhorn,accessModes: [ReadWriteMany], annotationhelm.sh/resource-policy: keeppour survivre à unhelm 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ôlecreds/<app>existent (étape 5).
Related
- 5. Terraform de l'app — crée
postgres/creds/<app>, le rôle K8s<app>et remplitkvv2/<app>/configque ces CRD consomment. - 2. Base de données — la base
<app>etpgbouncer.toolsciblés ici. - 3. Vault plateforme — la policy runtime
<app>qu'utiliseVaultAuth. - 6. Workflows CI — construit l'image référencée par
image.repository. - Référence VSO/secrets faisant autorité — explication détaillée VaultConnection/VaultAuth/VaultDynamicSecret côté
tools(à ne pas dupliquer ici).