Files
factory/doc/runbooks/new-web-app/04-helm-chart.md
Gabriel Radureau 8330d82225 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>
2026-05-31 17:22:30 +02:00

8.0 KiB
Raw Permalink Blame History

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 à 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 ou webapp/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.

# 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.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
# 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 erpdolibarr/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).