Déployer sur Kubernetes

Vaks PM · Déploiement sur un cluster existant · Kubernetes · juin 2026

Objet de ce guide. Ce document explique comment installer Vaks PM, une application de gestion de projet multi-utilisateurs auto-hébergée, sur un cluster Kubernetes existant à l'aide du chart Helm fourni. Il s'adresse à un administrateur système qui ne connaît rien à ce produit mais qui est à l'aise avec Kubernetes et Helm. Il liste chaque prérequis et l'état initial attendu, donne deux configurations d'exemple complètes (mono-nœud pour l'évaluation et haute disponibilité pour la production) et couvre les couches de chiffrement qui doivent être activées avant la production. Le chart déploie les mêmes images de conteneur que le parcours d'installation Docker Swarm décrit dans Déployer sur Docker Swarm.
Le chiffrement n'est pas entièrement activé par défaut. Les valeurs de démarrage rapide activent HTTPS à la bordure du cluster et l'application chiffre ses propres secrets au niveau champ, mais le TLS (Transport Layer Security) entre services internes et le chiffrement au repos des volumes et des Secrets sont des contrôles du cluster laissés désactivés par le démarrage rapide. Avant le passage en production, compléter chaque couche dans les sections Chiffrement sur Kubernetes ci-dessous.

Vue d'ensemble

Vaks PM est livré sous la forme d'un petit ensemble d'images de conteneur et est conçu pour fonctionner entièrement sur une infrastructure contrôlée par l'organisation, sans dépendance obligatoire à un service en ligne externe. Les seules connexions sortantes optionnelles sont un relais de messagerie (pour les notifications) et un fournisseur d'identité d'entreprise (pour l'authentification unique).

Ce guide couvre le parcours d'installation Kubernetes, destiné aux organisations qui exploitent déjà un cluster Kubernetes. L'autre parcours pris en charge, un installateur guidé qui provisionne un cluster Docker Swarm de zéro via SSH, est documenté séparément dans Déployer sur Docker Swarm. Les deux parcours exécutent la même application à partir des mêmes images ; le choix est dicté par l'outillage opérationnel existant, pas par des différences de fonctionnalités.

L'installation utilise Helm, le gestionnaire de paquets de Kubernetes. Un chart (paquet Helm) prêt à l'emploi est fourni dans le dépôt à deploy/helm/vaks-pm. Le chart déploie tous les services applicatifs sans état et, en option, les trois dépendances de support (base de données, cache, stockage objet) soit à l'intérieur du cluster (pour l'évaluation), soit en pointant vers des services externes existants (la recommandation de production).

Glossaire

Les acronymes et termes utilisés tout au long de ce guide, définis ici une fois pour référence.

TermeSignification
K8sKubernetes : une plateforme d'orchestration de conteneurs qui planifie, met à l'échelle et supervise des charges de travail conteneurisées.
HelmGestionnaire de paquets de Kubernetes. Une installation empaquetée est un chart ; ses réglages ajustables vivent dans un fichier de valeurs.
ChartUn ensemble de modèles de ressources Kubernetes paramétrés par un fichier de valeurs. Le chart Vaks PM se trouve à deploy/helm/vaks-pm.
Fichier de valeursUn fichier YAML (YAML Ain't Markup Language) fournissant les paramètres du chart, passé avec -f au moment de l'installation.
IngressL'objet Kubernetes qui expose les services HTTP/HTTPS vers l'extérieur et route par nom d'hôte et par chemin. Un contrôleur Ingress doit être installé pour qu'il prenne effet.
PVCPersistentVolumeClaim (demande de volume persistant) : une demande de disque durable dans Kubernetes, afin que les données d'un conteneur survivent aux redémarrages.
StorageClassUn profil de stockage nommé que le cluster utilise pour satisfaire automatiquement un PVC. Une StorageClass par défaut est requise si les dépendances embarquées sont utilisées.
SecretUn objet Kubernetes contenant des valeurs sensibles (clés, mots de passe, chaînes de connexion). Encodé en base64 dans le magasin de données du cluster, non chiffré sauf si le chiffrement du magasin de données est activé (voir Au repos).
CNIContainer Network Interface (interface réseau de conteneurs) : le plugin réseau du cluster (par exemple Cilium, Calico ou Flannel). Certains CNI peuvent chiffrer le trafic de pod à pod.
mTLSMutual Transport Layer Security (TLS mutuel) : un TLS où les deux extrémités présentent et vérifient un certificat ; utilisé par les maillages de services pour chiffrer le trafic de pod à pod.
TLSTransport Layer Security : la couche de chiffrement derrière HTTPS.
KMSKey Management Service (service de gestion de clés) : un magasin de clés géré (par exemple AWS KMS) qui réalise les opérations de clé sans exposer le matériel cryptographique.
HSMHardware Security Module (module matériel de sécurité) : un appareil inviolable qui détient les clés et réalise les opérations cryptographiques sur site.
KEKKey Encryption Key (clé de chiffrement de clés) : une clé maîtresse qui enveloppe (chiffre) les clés de données. Elle peut être tournée sans rechiffrer aucune donnée.
DEKData Encryption Key (clé de chiffrement de données) : une clé par tenant qui chiffre les données ; elle est elle-même enveloppée par la KEK.
RPORecovery Point Objective (objectif de point de récupération) : la quantité maximale acceptable de perte de données, mesurée en temps, après une défaillance. Détermine la fréquence des sauvegardes.
JWTJSON Web Token (jeton web JSON) : un jeton de session signé émis après la connexion.
SMTPSimple Mail Transfer Protocol (protocole simple de transfert de courrier) : le protocole d'envoi des courriels ; utilisé pour les notifications.
TenantUne organisation isolée servie par l'instance. Chaque tenant dispose de sa propre base de données.

Architecture déployée

Quelles que soient les options choisies, le système en exécution est le même ensemble de services. L'application est sans état là où elle peut l'être (les services api, web et worker ne détiennent aucune donnée locale et passent à l'échelle horizontalement) ; tout l'état durable vit dans trois magasins de support : PostgreSQL, Redis et le magasin d'objets S3.

ComposantRôleDéployé
apiBack end NestJS : l'API REST (préfixe /api/v1) plus le canal WebSocket temps réel.Toujours
webSert le front end React mono-page (ressources statiques, via nginx).Toujours
workerExécuteur de tâches d'arrière-plan (envoi de courriels, livraison de webhooks, transfert d'audit, crons planifiés), adossé à la file Redis.Toujours
controlplanePorte d'authentification partagée qui émet un jeton estampillé du tenant. Même image que api, avec un drapeau de rôle différent.tenancy pooled uniquement
PostgreSQL 17Base de données relationnelle principale. Une base par tenant.Optionnel (embarqué ou externe)
Redis 7Cache, pub/sub et la file de tâches d'arrière-plan.Optionnel (embarqué ou externe)
MinIO (S3)Magasin d'objets compatible S3 pour les pièces jointes, le branding, les modèles et les archives d'audit.Optionnel (embarqué ou externe)

Une tâche de migration de schéma (un hook Helm nommé vaks-pm-dbpush) s'exécute automatiquement à chaque installation et mise à niveau, avant le démarrage des services, pour synchroniser la structure de la base de données. Aucune étape de migration manuelle n'est requise.

Routage réseau (Ingress)

Le chart génère un unique Ingress qui répartit le trafic d'un seul nom d'hôte par chemin de requête :

CheminService cible
/api/v1/authcontrolplane pooled ; sinon api
/socket.ioapi (canal WebSocket temps réel)
/apiapi
/web

Modes de tenancy

Le chart prend en charge deux modèles de tenancy, sélectionnés par tenancy.mode. Ceci est indépendant de la décision mono-vs-HA de la section suivante.

ModeSignification
dedicated (par défaut)Une organisation par instance, un nom de domaine. L'authentification est gérée par le service api ; il n'y a pas de plan de contrôle séparé. C'est le modèle d'une organisation qui déploie l'outil pour son propre usage. À choisir en cas de doute.
pooledPlusieurs organisations (« tenants ») partagent une instance. L'organisation est résolue à partir du sous-domaine de la requête (<slug>.<baseDomain>) et un service controlplane séparé émet les jetons d'authentification. Nécessite un Ingress à domaine générique et le paramètre tenancy.baseDomain. Pertinent uniquement pour un fournisseur revendant l'application comme un service.

Prérequis et état initial

Les listes ci-dessous décrivent le point de départ attendu avant l'exécution de toute commande d'installation. L'état initial attendu est un cluster Kubernetes opérationnel sur lequel l'administrateur dispose des pleins droits sur au moins un namespace.

Cluster et poste de travail

ÉlémentExigence
Cluster KubernetesVersion 1.24 ou plus récente, en état de fonctionnement.
Droits sur le namespaceAccès administratif à au moins un namespace (les exemples utilisent vaks-pm).
Contrôleur IngressUn contrôleur opérationnel déjà installé (par exemple ingress-nginx ou Traefik). Le nom de sa classe est nécessaire au moment de l'installation.
StorageClass par défautRequise si les PostgreSQL / Redis / MinIO embarqués sont utilisés, car ils demandent des PVC. Inutile lorsque les trois dépendances sont externes.
kubectlInstallé sur le poste de travail et configuré pour cibler le cluster (kubeconfig valide).
helmHelm 3.x installé sur le poste de travail.
opensslDisponible sur le poste de travail pour générer les secrets aléatoires.
Images applicativesLes trois images vaks-pm/api, vaks-pm/web, vaks-pm/worker accessibles depuis un registre que le cluster peut tirer (avec des identifiants de pull si le registre est privé).

Réseau et certificats

Optionnel, configurable après l'installation

Ne jamais placer d'identifiants, de mots de passe ou d'adresses dans la gestion de versions. Toutes les valeurs sensibles appartiennent à des Secrets de cluster ou à un fichier de valeurs tenu hors du contrôle de versions. Pour la production, les fournir via secrets.existingSecret afin qu'aucun texte en clair n'atterrisse dans un fichier de valeurs ni dans l'historique des releases Helm.

Mono vs HA · choisir la topologie

Le chart peut s'installer sous deux formes très différentes. Ce choix est indépendant du mode de tenancy et constitue la décision la plus lourde de conséquences pour un déploiement de production. Lire le tableau attentivement : une installation mono ne convient qu'à l'évaluation.

AspectMONO / évaluationHA / production
ObjetÉvaluation, démonstrations, essais hors ligne.Production pour environ 200 utilisateurs et plus.
Réplicas applicatifs (api/web/worker/controlplane)replicaCount: 1replicaCount: 2 à 3 chacun.
Répartition des podsAucune.Anti-affinité de pods via la valeur affinity de chaque service, afin que les réplicas atterrissent sur des nœuds différents.
PostgreSQLPostgreSQL embarqué mono-nœud (postgresql.enabled=true) acceptable.PostgreSQL externe / géré OBLIGATOIRE (postgresql.enabled=false + externalDatabase.*).
RedisRedis embarqué mono-nœud (redis.enabled=true) acceptable.Redis externe / géré OBLIGATOIRE (redis.enabled=false + externalRedis.url).
Stockage objet S3MinIO embarqué mono-nœud (minio.enabled=true) acceptable.S3 externe / géré OBLIGATOIRE (minio.enabled=false + s3.external.*).
Sondes de santéIntégrées au chart (liveness + readiness sur api/web/worker).Identiques ; déjà présentes, et sur lesquelles repose la mise à niveau progressive sans interruption.
Les PostgreSQL, Redis et MinIO embarqués sont mono-nœud uniquement. Ils n'ont aucune haute disponibilité et demandent chacun un unique PVC. Ils sont destinés à l'évaluation ou à un usage non critique. Pour tout déploiement de production, la base de données, le cache et le magasin d'objets doivent être des services externes hautement disponibles apportés par l'organisation. Définir postgresql.enabled=false, redis.enabled=false et minio.enabled=false, et pointer le chart vers les services gérés.
Anti-affinité. Chaque service applicatif expose une valeur affinity (api.affinity, web.affinity, worker.affinity) passée directement à la spécification du pod. Y définir une podAntiAffinity afin que les deux ou trois réplicas d'un service soient planifiés sur des nœuds distincts ; sinon Kubernetes peut les co-localiser et la défaillance d'un seul nœud supprime tout le service.

Dimensionnement

Les valeurs ci-dessous sont les requests (réservations) par défaut du chart, indicatives pour un déploiement de production servant environ 200 utilisateurs. À ajuster à la charge observée. L'unité m est le milli-cœur de processeur (1000 m = 1 vCPU) ; Mi et Gi sont des mébioctets et des gibioctets de mémoire.

ServiceRéplicas (défaut)Requête CPURequête mémoireLimite CPULimite mémoire
api2250 m512 Mi11 Gi
web250 m64 Mi250 m256 Mi
worker2100 m256 Mi500 m512 Mi
controlplane (pooled)2250 m512 Mi11 Gi
postgresql (si embarqué)1250 m512 Mi11 Gi
redis (si embarqué)150 m64 Mi250 m256 Mi
minio (si embarqué)1100 m256 Mi500 m512 Mi
Chaque valeur est configurable par service via ses clés resources et replicaCount. Sur un environnement de test aux ressources contraintes, réduire les réplicas à 1 et abaisser les réservations. Pour environ 200 utilisateurs sous charge soutenue, augmenter les réplicas api et worker à 3 et relever leurs limites avant d'ajouter des nœuds.

Images · miroir et identifiants de pull

Trois images applicatives sont requises : vaks-pm/api, vaks-pm/web et vaks-pm/worker. Les recopier dans un registre que le cluster peut atteindre, puis noter l'adresse hôte de ce registre (par exemple registry.example.com). Définir l'hôte comme image.registry ; le chart ajoute le dépôt (vaks-pm/api et ainsi de suite), donnant registry.example.com/vaks-pm/api. Ne pas répéter le segment vaks-pm dans image.registry.

Si le registre stocke les images sous des noms différents, surcharger image.api.repository, image.web.repository et image.worker.repository. Les trois images partagent un seul tag (image.tag, par défaut l'appVersion du chart).

Créer le namespace et, pour un registre privé, un Secret de pull à référencer ensuite via imagePullSecrets :

kubectl create namespace vaks-pm

# Private registry only:
kubectl -n vaks-pm create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=<user> \
  --docker-password=<password>

Secrets

L'application requiert quatre secrets obligatoires. Les générer avec de fortes valeurs aléatoires :

openssl rand -hex 32      # jwtAccessSecret
openssl rand -hex 32      # jwtRefreshSecret
openssl rand -base64 32   # mfaEncryptionKey
openssl rand -hex 32      # workerApiToken
SecretSert à
jwtAccessSecret / jwtRefreshSecretSigner les jetons de session émis à la connexion. Doivent être de longues valeurs aléatoires.
mfaEncryptionKeyChiffrer au repos les secrets à deux facteurs par utilisateur. Également la KEK par défaut au niveau champ (voir Secrets au niveau champ).
workerApiTokenLe jeton que le worker utilise pour appeler l'API interne lors des callbacks.
smtpPassword (optionnel)S'authentifier auprès du relais de messagerie, uniquement lorsque les notifications utilisent l'authentification par mot de passe.
ssoOidcClientSecret (optionnel)Le secret client OIDC, uniquement lorsque l'authentification unique est configurée.
syslogClientKey (optionnel)Clé privée PEM pour le transfert syslog des journaux d'audit en mTLS ; montée dans le worker.
Production : gérer l'ensemble de ces éléments, avec les identifiants de base de données et de stockage, dans un Secret Kubernetes provisionné hors bande, et pointer le chart vers lui avec secrets.existingSecret. Ce Secret doit contenir les clés DATABASE_URL, REDIS_URL, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, MFA_ENCRYPTION_KEY, WORKER_API_TOKEN, S3_SECRET_KEY, et le cas échéant SMTP_PASSWORD et SSO_OIDC_CLIENT_SECRET. Cela tient le texte en clair hors du fichier de valeurs et de l'historique Helm.

Fichiers de valeurs

La configuration vit dans un fichier de valeurs passé avec -f. Deux exemples complets suivent : le Scénario A pour la production (dépendances externes, réplicas HA, anti-affinité) et le Scénario B pour l'évaluation (dépendances embarquées, réplicas uniques).

Scénario A · Production (PostgreSQL / Redis / S3 externes, HA)

Le Kubernetes propre à l'organisation, une base de données et un cache gérés, un service S3 existant, et des réplicas répartis entre les nœuds par anti-affinité. Mode mono-organisation (dedicated).

image:
  registry: registry.example.com   # host only; chart appends vaks-pm/api|web|worker
  tag: "1.0.0"
imagePullSecrets:
  - name: regcred

publicBaseUrl: https://pm.example.com
tenancy:
  mode: dedicated

# --- HA replicas ---
api:
  replicaCount: 3
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            topologyKey: kubernetes.io/hostname
            labelSelector:
              matchLabels: { app.kubernetes.io/component: api }
web:
  replicaCount: 2
worker:
  replicaCount: 3

# --- Database: managed / external ---
postgresql:
  enabled: false
externalDatabase:
  url: "postgresql://vakspm:<password>@pg.internal.example.com:5432/vakspm?schema=public"

# --- Cache: managed / external ---
redis:
  enabled: false
externalRedis:
  url: "redis://redis.internal.example.com:6379"

# --- Object storage: existing S3 service (e.g. MinIO / Ceph / AWS) ---
minio:
  enabled: false
s3:
  external:
    endpoint: "https://s3.internal.example.com:9000"
    region: us-east-1
    useSSL: true
    forcePathStyle: true        # true for MinIO/Ceph/Garage
    accessKey: "<access-key>"
    secretKey: "<secret-key>"   # prefer secrets.existingSecret
    bucketPrefix: ""
    createBuckets: true

ingress:
  enabled: true
  className: nginx
  host: pm.example.com
  tls:
    enabled: true
    secretName: pm-example-tls

# Prefer secrets.existingSecret in production (see Secrets section).
secrets:
  jwtAccessSecret: "<openssl rand -hex 32>"
  jwtRefreshSecret: "<openssl rand -hex 32>"
  mfaEncryptionKey: "<openssl rand -base64 32>"
  workerApiToken: "<openssl rand -hex 32>"

Scénario B · Évaluation (tout embarqué, réplicas uniques)

Toutes les dépendances à l'intérieur du cluster, adapté à une mise en place rapide ou hors ligne. Réplicas réduits à 1.

image:
  registry: registry.example.com   # host only; chart appends vaks-pm/api|web|worker
  tag: "1.0.0"

publicBaseUrl: https://pm.example.test
tenancy:
  mode: dedicated

api:    { replicaCount: 1 }
web:    { replicaCount: 1 }
worker: { replicaCount: 1 }

postgresql: { enabled: true }
redis:      { enabled: true }
minio:      { enabled: true }

ingress:
  enabled: true
  className: nginx
  host: pm.example.test
  tls: { enabled: false }

# Creates the demo organization and administrator account.
migrations:
  seedDemo: true

secrets:
  jwtAccessSecret: "<openssl rand -hex 32>"
  jwtRefreshSecret: "<openssl rand -hex 32>"
  mfaEncryptionKey: "<openssl rand -base64 32>"
  workerApiToken: "<openssl rand -hex 32>"
Les mots de passe des dépendances embarquées qui ne sont pas fournis (PostgreSQL, MinIO) sont générés automatiquement et stockés dans le Secret du chart, conservé d'une mise à niveau et d'une désinstallation à l'autre afin de préserver l'accès aux données.

Stockage S3

Vaks PM stocke les fichiers binaires (pièces jointes de tâches, logos de branding, modèles de documents, chartes de projet et archives d'audit compressées) dans un magasin d'objets compatible S3. Toute implémentation conforme convient (Amazon S3, MinIO, Ceph RGW, Garage, NetApp StorageGRID, Wasabi…). Le chart traduit la section de stockage en variables d'environnement ; seuls les paramètres ci-dessous changent d'un fournisseur à l'autre.

Paramètre (s3.external.*)Signification
endpointVide → Amazon S3 natif (l'adresse est dérivée de la région). Une URL complète ou un nom d'hôte pour MinIO, Ceph, Garage, etc.
port / useSSLUtilisés lorsque endpoint est un simple nom d'hôte. useSSL: true (par défaut) sélectionne HTTPS vers le magasin.
forcePathStyleStyle d'adressage des buckets. Définir true pour MinIO / Ceph / Garage ; false pour Amazon S3. Choisi automatiquement si laissé à null.
regionRégion du service (par exemple eu-west-1).
accessKey / secretKeyIdentifiants d'accès. Préférer fournir la clé secrète via secrets.existingSecret (S3_SECRET_KEY).
bucketPrefixPréfixe appliqué à tous les buckets. Indispensable sur Amazon S3, dont l'espace de noms des buckets est global ; permet aussi à plusieurs instances de partager un magasin.
createBucketsDéfinir à false lorsque les buckets sont précréés et que le compte n'a pas le droit de les créer.

L'application utilise cinq buckets (noms indiqués avant l'application du préfixe) : charter-blobs, document-templates, vaks-pm-attachments, vaks-pm-branding, vaks-pm-audit-archive. Lorsque createBuckets vaut false, créer ces cinq buckets (préfixés) au préalable et accorder au compte de service les droits de lecture, écriture, suppression et liste sur eux. Au démarrage, l'API journalise une ligne S3 backend: … résumant la configuration en vigueur.

Installation Helm

Avant d'installer, valider le rendu du chart par rapport au fichier de valeurs :

helm lint ./deploy/helm/vaks-pm -f values-production.yaml

helm template vaks-pm ./deploy/helm/vaks-pm -f values-production.yaml \
  | kubectl apply --dry-run=client -f -

Puis installer. Le --timeout laisse à la tâche de migration de schéma le temps de se terminer :

helm install vaks-pm ./deploy/helm/vaks-pm \
  -n vaks-pm --create-namespace \
  -f values-production.yaml \
  --timeout 5m

Attendre que les pods deviennent prêts :

kubectl -n vaks-pm get pods
kubectl -n vaks-pm rollout status deploy/vaks-pm-api

Vérification

Surveiller le hook de migration de schéma pendant son exécution (il se supprime automatiquement en cas de succès), puis valider chaque chemin réseau. Remplacer pm.example.com par le nom d'hôte configuré.

# Schema-migration job. It is a Helm hook DELETED automatically on success,
# so watch it while it runs rather than after:
kubectl -n vaks-pm get pods -w        # the vaks-pm-dbpush-* pod should reach Completed
# If it FAILS, the job is kept; read its logs:
kubectl -n vaks-pm logs job/vaks-pm-dbpush

# API readiness (expected response: {"status":"ok","db":"up"})
curl -s https://pm.example.com/api/v1/health/ready

# API liveness
curl -s https://pm.example.com/api/v1/health/live

# Home page (expected: HTTP 200)
curl -s -o /dev/null -w "%{http_code}\n" https://pm.example.com/
La tâche vaks-pm-dbpush s'exécute comme un hook post-installation/mise à niveau et est supprimée une fois réussie ; un NotFound à la lecture de ses logs après une installation réussie est donc attendu, et non une erreur. Une tâche en échec est conservée pour inspection.
Lorsque la réponse de santé affiche db:up, que la page d'accueil renvoie 200, que l'administrateur initialisé peut se connecter et que l'envoi d'une pièce jointe de tâche réussit (ce qui exerce le chemin S3 de bout en bout), le déploiement est fonctionnel. Si l'option de démonstration a été activée, un compte administrateur initial est créé (admin@vaks-pm.local, mot de passe ChangeMe123!) ; le changer immédiatement.

Chiffrement sur Kubernetes · vue d'ensemble

Un déploiement complet comporte trois préoccupations de chiffrement indépendantes. Elles sont orthogonales (en activer une n'active pas les autres), et seules les deux premières sont prises en charge par les valeurs de démarrage rapide.

Ce que le démarrage rapide laisse DÉSACTIVÉ. Les valeurs d'évaluation ne protègent que les secrets applicatifs au niveau champ et la bordure HTTPS externe. Le TLS entre services internes et le chiffrement au repos des volumes et des Secrets sont des contrôles au niveau du cluster que l'opérateur doit activer. Le tableau ci-dessous indique la valeur par défaut de chaque couche et comment l'activer ; le modèle de sécurité derrière le chiffrement au niveau champ est décrit dans Sécurité.
CoucheProtègeDéfaut (démarrage rapide)Activer avec
Secrets au niveau champ (enveloppe)Secrets SMTP / MFA / SSO dans la baseACTIVÉintégré ; garde via crypto.*
TLS de bordure externenavigateur ↔ clusterACTIVÉ (si TLS Ingress défini)ingress.tls + certificat
TLS app ↔ PostgreSQLtrafic base de donnéesDÉSACTIVÉtls.postgres
TLS app ↔ Redis / S3trafic cache / objetDÉSACTIVÉrediss:// / s3.external.useSSL
Réseau de pod à podtout le trafic intra-clusterDÉSACTIVÉCNI chiffré / mTLS de maillage de services
Volumes de données (PVC) au reposbase / cache / objets complets sur disqueDÉSACTIVÉStorageClass chiffrée
Secrets Kubernetes au repostrousseau de KEK, clés, identifiantsDÉSACTIVÉchiffrement du magasin de données / magasin externe
Planifier les lignes DÉSACTIVÉ avant de construire. Les deux premières lignes sont des contrôles applicatifs ou de configuration activables à tout moment. Les lignes marquées DÉSACTIVÉ sont fondationnelles : elles dépendent du magasin de données du cluster (chiffrement des Secrets), du plugin réseau (pod à pod) et de la classe de stockage (volumes au repos), tous établis au moment de la création du cluster et de son stockage. Les activer après coup signifie reposer ces fondations, ce qui est perturbateur ou impossible à chaud. Les décider avant de construire un cluster de production.

Secrets au niveau champ (chiffrement à enveloppe)

Les champs sensibles sont chiffrés avec une DEK par tenant enveloppée par une KEK. C'est intégré et fonctionne d'emblée : la KEK est par défaut MFA_ENCRYPTION_KEY (enregistrée sous la ref env), un chemin qui fonctionne toujours à la fois pour le chiffrement et le déchiffrement. Rien de plus n'est requis pour une configuration de base.

Le chiffrement au niveau champ protège les secrets listés, pas le gros des données métier (projets, tâches, utilisateurs). Protéger l'ensemble du jeu de données au repos relève de la couche de chiffrement des volumes dans Au repos.

Options de garde (crypto.*)

Deux modèles de garde existent ; tous deux sont portables entre les parcours Swarm et Kubernetes, car l'application ne fait que lire un répertoire de trousseau monté (crypto.keysDir, par défaut /etc/vaks/keys) et ne se soucie pas de qui l'a monté.

Le crypto.provider: local par défaut utilise le trousseau de fichiers (ou, à défaut, MFA_ENCRYPTION_KEY). Pour une séparation des tâches dans un déploiement réglementé, déplacer la garde vers un KMS ou un HSM. Le modèle et sa correspondance avec les contrôles DORA sont décrits dans Sécurité.

En transit

Bordure externe (Ingress)

Terminer HTTPS au niveau de l'Ingress en fournissant un certificat comme un Secret Kubernetes existant, ou en laissant un gestionnaire de certificats en émettre un :

ingress:
  enabled: true
  className: nginx          # or traefik
  host: pm.example.com
  tls:
    enabled: true
    secretName: pm-example-tls  # cert-manager or supplied

Services internes (base de données, cache, stockage objet)

Ces sauts transportent les données de l'application vers ses dépendances et sont en clair sauf s'ils sont activés. Chacun est un réglage distinct :

SautRéglageEffet
app ↔ PostgreSQLtls.postgres: trueDéfinit PG_TLS=1. Pour la vérification de certificat, ajouter tls.postgresCaSecret (un Secret avec ca.crt) et tls.postgresVerifyMode (verify-full ou verify-ca).
app ↔ RedisexternalRedis.url: rediss://…Une connexion Redis en TLS.
app ↔ S3s3.external.useSSL: true + un endpoint httpsUne connexion au stockage objet en TLS.
Les PostgreSQL / Redis / MinIO embarqués mono-nœud n'ont pas le TLS activé. Le TLS entre services internes est destiné aux dépendances gérées/externes qui terminent le TLS, une raison de plus pour que la production utilise des services externes. Note : la tâche de migration (prisma db push) ne lit pas PG_TLS ; si la base de données impose le TLS, ajouter aussi ?sslmode=require à la DATABASE_URL.

Réseau de pod à pod

La façon la plus simple de chiffrer tout le trafic intra-cluster d'un coup, y compris les sauts vers la base de données, le cache, le stockage objet et tout courtier KMS/HSM, est de chiffrer le réseau de pods lui-même plutôt que chaque service individuellement :

Un CNI chiffré est le contrôle global recommandé : il couvre chaque saut interne sans configuration TLS par service. Le CNI/backend est choisi à l'installation du cluster ; le changer sur un cluster en fonctionnement est perturbateur, donc le planifier dès le départ.

Au repos

Volumes de données (PVC)

Les volumes de la base de données, du cache et du stockage objet contiennent l'ensemble du jeu de données (pas seulement les secrets au niveau champ). Les chiffrer avec une StorageClass chiffrée ; seuls les volumes sont chiffrés, jamais le système d'exploitation du nœud :

postgresql:
  persistence:
    storageClass: gp3-encrypted   # a cloud-KMS or CSI-encrypted class
# do the same for redis.persistence.storageClass / minio.persistence.storageClass,
# or use managed services with provider-side encryption

Options : une classe de stockage en bloc chiffrée d'un fournisseur cloud (adossée à un KMS), un pilote Container Storage Interface (CSI) avec chiffrement, ou des nœuds dont les disques sont chiffrés (par exemple LUKS, Linux Unified Key Setup). Lorsque les dépendances sont externes/gérées, utiliser plutôt le chiffrement du fournisseur (chiffrement RDS, chiffrement côté serveur S3, et ainsi de suite).

Secrets Kubernetes

Le trousseau de KEK, les clés de signature et les identifiants de base de données/stockage vivent dans des Secrets Kubernetes. Un Secret n'est qu'encodé en base64 dans le magasin de données du cluster, non chiffré par défaut. Le protéger avec l'un des éléments suivants :

Sans chiffrement du magasin de données, quiconque peut lire le magasin de données du cluster (une copie volée d'etcd/SQLite, ou un accès root au nœud) peut décoder chaque Secret, y compris le trousseau de KEK. Activer le chiffrement du magasin de données ou un magasin externe avant de stocker de vrais secrets.

Exemple HA chiffré

Ce fichier de valeurs consolidé réunit la forme HA de production avec chaque contrôle de chiffrement au niveau applicatif activé. Il repose toujours sur des contrôles au niveau du cluster (CNI chiffré, chiffrement des Secrets du magasin de données) configurés en dehors de Helm, comme noté dans les commentaires.

image:
  registry: registry.example.com
  tag: "1.0.0"
imagePullSecrets:
  - name: regcred

publicBaseUrl: https://pm.example.com
tenancy:
  mode: dedicated

# --- HA replicas + anti-affinity ---
api:    { replicaCount: 3 }
web:    { replicaCount: 2 }
worker: { replicaCount: 3 }

# --- External, managed, TLS-terminating dependencies ---
postgresql:
  enabled: false
externalDatabase:
  url: "postgresql://vakspm:<password>@pg.internal.example.com:5432/vakspm?schema=public&sslmode=require"
redis:
  enabled: false
externalRedis:
  url: "rediss://redis.internal.example.com:6379"   # rediss:// = TLS
minio:
  enabled: false
s3:
  external:
    endpoint: "https://s3.internal.example.com:9000"
    region: us-east-1
    useSSL: true                # TLS to object storage
    forcePathStyle: true
    accessKey: "<access-key>"
    secretKey: "<secret-key>"
    createBuckets: true

# --- In-transit TLS to PostgreSQL with CA verification ---
tls:
  postgres: true                # PG_TLS=1
  postgresCaSecret: pg-ca       # Secret holding ca.crt
  postgresVerifyMode: verify-full

# --- External edge TLS ---
ingress:
  enabled: true
  className: nginx
  host: pm.example.com
  tls:
    enabled: true
    secretName: pm-example-tls

# --- Field-level KEK: versioned keyring with rotation ---
crypto:
  existingSecret: vaks-pm-keyring   # holds kek_v1, kek_v2, …
  activeKek: v2                     # new writes wrap with kek_v2

# --- All sensitive values from an out-of-band Secret ---
secrets:
  existingSecret: vaks-pm-secrets

# Cluster-level controls configured OUTSIDE Helm (plan before building):
#   * encrypted CNI (e.g. WireGuard) for pod-to-pod traffic
#   * datastore Secret encryption (EncryptionConfiguration / k3s secrets-encrypt)
#   * encrypted StorageClass for any in-cluster PVCs (none here: all deps external)
Liste de contrôle de durcissement pour la production. Secrets au niveau champ (choisir la garde) · TLS de bordure externe · en transit interne (CNI chiffré, ou par service tls.postgres + rediss:// + S3 useSSL) · volumes de données au repos (StorageClass chiffrée ou chiffrement du service géré) · Secrets Kubernetes au repos (chiffrement du magasin de données ou magasin externe) · garde des clés (déplacer la KEK vers un KMS/HSM pour la séparation des tâches).

Mise à niveau

Une mise à niveau pointe la release vers un tag d'image plus récent et réapplique le chart. Le client ne construit ni ne modifie jamais de code — seul le tag change. Comme les services applicatifs sont sans état et exécutent plusieurs réplicas, Kubernetes procède par rolling update (les nouveaux pods doivent passer leur sonde de readiness avant que les anciens ne soient retirés), donc l'opération est sans interruption. Le schéma de base de données voyage avec l'image et est appliqué par un Job de type hook Helm ; il n'y a donc pas de pack de migrations SQL à jouer séparément dans le cas courant.

En bref. (1) mettre le nouveau tag d'image en miroir dans votre registre ; (2) sur une base peuplée, sauvegarder d'abord ; (3) helm upgrade avec le nouveau image.tag — le hook vaks-pm-dbpush aligne le schéma avant que les pods applicatifs ne basculent ; (4) vérifier la santé. Les étapes détaillées suivent.

1 · Mettre le nouveau tag d'image en miroir

Le chart tire les trois images (api, web, worker) depuis le registre nommé dans image.registry, au tag indiqué dans image.tag. Mettre d'abord la nouvelle version en miroir dans ce registre (voir Images · miroir et identifiants de pull), pour que chaque nœud puisse la tirer. Épingler un tag de version unique (par ex. 1.4.0) plutôt qu'un tag mobile, afin que la révision déployée soit reproductible.

2 · Lancer la mise à niveau

Fixer le nouveau tag et réappliquer le même fichier de valeurs. Le --timeout laisse au hook de migration de schéma le temps de se terminer avant que Helm ne bascule les pods applicatifs :

helm upgrade vaks-pm ./deploy/helm/vaks-pm \
  -n vaks-pm -f values-production.yaml \
  --set image.tag=1.4.0 --timeout 5m

kubectl -n vaks-pm rollout status deploy/vaks-pm-api      # new pods Ready
kubectl -n vaks-pm rollout status deploy/vaks-pm-web

Helm exécute le hook vaks-pm-dbpush (ci-dessous) en étape pre-upgrade, de sorte que le schéma est aligné avant le démarrage des nouveaux pods api. Si le hook échoue, la mise à niveau s'arrête et les pods en fonctionnement continuent de servir l'ancienne version.

3 · Schéma de base de données

Le schéma voyage dans l'image. Le chart embarque un Job de type hook Helm, vaks-pm-dbpush, qui exécute prisma db push à l'installation et à la mise à niveau (conditionné par migrations.enabled, actif par défaut). Il s'auto-supprime en cas de succès et est conservé en cas d'échec pour inspection — donc pour une mise à niveau de routine, il n'y a aucune étape de migration manuelle.

# observer le hook pendant qu'il tourne (supprimé en cas de succès) :
kubectl -n vaks-pm get pods -w             # vaks-pm-dbpush-* doit atteindre Completed
kubectl -n vaks-pm logs job/vaks-pm-dbpush # présent uniquement s'il a ÉCHOUÉ
Pré-requis : le rôle de base doit posséder le schéma. Le push suppose que le rôle de la chaîne de connexion a le droit de modifier le schéma — vrai pour le PostgreSQL embarqué et pour une base dédiée dont le chart utilise le propriétaire. Si vous pointez externalDatabase.url vers un rôle à privilèges minimaux sans droit DDL sur le schéma public, le hook échoue (permission denied for schema public / must be owner of table …). Dans ce cas, appliquer d'abord le changement de schéma avec un rôle disposant des droits DDL, puis lancer la mise à niveau avec migrations.enabled=false :
kubectl -n vaks-pm run dbpush --rm -it --restart=Never \
  --image=<registre>/vaks-pm/api:1.4.0 \
  --env DATABASE_URL='postgresql://<owner>:…@<host>:5432/<db>' \
  -- sh -c 'cd /app/api && npx prisma db push --accept-data-loss'
Changements destructifs et retour arrière. prisma db push est forward-only : il n'existe pas de migration inverse, et certains changements de type ou de contrainte peuvent supprimer des données. migrations.acceptDataLoss (défaut true) laisse le hook appliquer de tels changements sans demander — l'examiner avant une mise à niveau sur une base peuplée, et faire une sauvegarde d'abord (voir Exploitation). helm rollback ramène les charges de travail au tag d'image précédent, mais il n'annule pas les changements de schéma ; le retour arrière du schéma passe par une restauration de sauvegarde.
Mode pooled — une base par tenant. Le hook aligne la base de la chaîne de connexion, pas l'ensemble du parc. Pour un déploiement multi-tenant, chaque base tenant (<dbName>_<slug>) doit recevoir le changement de schéma avec un rôle DDL avant la bascule des pods applicatifs.

4 · Retour arrière

Pour revenir à la release précédente (tag d'image, valeurs et manifestes) :

helm history vaks-pm -n vaks-pm            # trouver la révision saine précédente
helm rollback vaks-pm <revision> -n vaks-pm --timeout 5m
Le retour arrière restaure les charges de travail, pas les données : si la mise à niveau a appliqué un changement de schéma, l'ancienne image peut ne plus correspondre au schéma migré. Ne revenir en arrière au-delà d'un changement de schéma qu'accompagné d'une restauration de la base au point dans le temps correspondant.

5 · Vérifier après la mise à niveau

kubectl -n vaks-pm get pods                       # tous Running sur la nouvelle image
curl -s https://pm.example.com/api/v1/health/ready   # {"status":"ok","db":"up"}

Tous les pods applicatifs Running sur le nouveau tag, le hook de migration Completed, la connexion fonctionne, et un envoi de pièce jointe réussit (exerce le chemin S3). Ajouter une organisation (mode pooled) n'est pas une mise à niveau : provisionner un nouveau tenant avec sa propre base de données et son nom d'hôte ; les tenants existants ne sont pas perturbés.

Désinstallation

helm uninstall vaks-pm -n vaks-pm
Les volumes persistants (PVC) et le Secret généré par le chart sont conservés après la désinstallation, afin que les données et les mots de passe générés ne soient pas détruits. Les supprimer manuellement si une réinitialisation complète est souhaitée.

Référence des paramètres

Les paramètres les plus utilisés. Voir deploy/helm/vaks-pm/values.yaml pour la liste exhaustive et commentée.

ParamètreDéfautDescription
image.registry""Hôte du registre contenant les images (le chart ajoute le dépôt).
image.tagappVersion du chartTag commun aux trois images.
imagePullSecrets[]Secrets pour un registre privé.
publicBaseUrl(requis)URL publique de l'application (liens des courriels et CORS).
tenancy.modededicateddedicated ou pooled.
tenancy.baseDomain""Domaine racine. Requis en mode pooled.
api/web/worker.replicaCount2Nombre de réplicas par service (mettre 1 pour l'évaluation, 2 à 3 pour la HA).
controlplane.replicaCount2Réplicas du plan de contrôle (pooled uniquement).
api/web/worker.resourcesvoir DimensionnementRequêtes et limites CPU/mémoire par service.
api/web/worker.affinity{}Passe-plat de l'affinité de la spécification du pod ; définir l'anti-affinité pour la répartition HA.
secrets.existingSecret""Nom d'un Secret existant fournissant les clés sensibles. Recommandé en production.
secrets.jwtAccessSecret / jwtRefreshSecret""Clés de signature JWT (lorsque existingSecret n'est pas utilisé).
secrets.mfaEncryptionKey""Clé MFA au repos ; également la KEK par défaut au niveau champ.
secrets.workerApiToken""Jeton utilisé par le worker pour appeler l'API interne.
secrets.smtpPassword / ssoOidcClientSecret / syslogClientKey""Optionnel : authentification courriel, secret client OIDC, clé mTLS syslog d'audit.
postgresql.enabledtrueDéployer le PostgreSQL embarqué mono-nœud. Mettre false pour la production.
externalDatabase.url""Chaîne de connexion pour une base de données externe.
redis.enabledtrueDéployer le Redis embarqué mono-nœud. Mettre false pour la production.
externalRedis.url""Chaîne de connexion pour un Redis externe (utiliser rediss:// pour le TLS).
minio.enabledfalseDéployer le MinIO embarqué mono-nœud (évaluation).
s3.external.endpoint / region / useSSL / forcePathStyle / accessKey / secretKey / bucketPrefix / createBucketsvoir Stockage S3Connexion à un service S3 externe.
crypto.keysDir/etc/vaks/keysChemin de montage du trousseau au niveau champ (CRYPTO_KEYS_DIR).
crypto.activeKek""Version de KEK avec laquelle les nouvelles écritures enveloppent (auto si vide).
crypto.keks / crypto.existingSecret{} / ""Entrées de trousseau en ligne, ou un Secret existant de clés kek_v<N>.
crypto.providerlocallocal | kms | hsm : garde de la KEK pour les nouvelles enveloppes.
crypto.kms.* / crypto.hsm.*Réglages KMS (keyId/region/endpoint/existingSecret) ou HSM (brokerUrl/keyLabel/existingSecret).
tls.postgresfalseActiver le TLS vers PostgreSQL (PG_TLS=1).
tls.postgresCaSecret / tls.postgresVerifyMode"" / verify-fullSecret de CA (ca.crt) et mode de vérification pour le TLS PostgreSQL.
persistence.storageClass (par dépendance)""StorageClass chiffrée pour le chiffrement des volumes au repos.
ingress.enabled / className / hosttrue / "" / ""Bascule Ingress, classe du contrôleur, nom d'hôte public.
ingress.tls.enabled / secretNametrue / ""Bascule du TLS de bordure et nom du Secret TLS.
migrations.enabledtrueExécuter le hook de migration de schéma à l'installation/mise à niveau.
migrations.acceptDataLosstrueAutoriser prisma db push à appliquer des changements qui suppriment des données. Examiner avant d'activer sur une base peuplée.
migrations.seedDemofalseCréer l'organisation de démonstration et l'administrateur. Laisser false en production.

Dépannage

SymptômeCause probable et correctif
Un pod reste PendingRessources de nœud insuffisantes, ou aucune StorageClass par défaut pour satisfaire un PVC. Inspecter avec kubectl -n vaks-pm describe pod <name> et kubectl get storageclass.
Un pod en ImagePullBackOffImage introuvable ou registre injoignable. Vérifier image.registry, image.tag et les imagePullSecrets requis.
La santé affiche db:downL'API ne peut pas atteindre la base de données. Vérifier externalDatabase.url (ou le pod postgresql) et la connectivité réseau depuis le namespace. Si la base impose le TLS, ajouter ?sslmode=require à l'URL.
La tâche vaks-pm-dbpush échoueLa migration de schéma n'a pas pu être appliquée. Lire kubectl -n vaks-pm logs job/vaks-pm-dbpush ; généralement la base est injoignable au démarrage (la tâche réessaie).
L'envoi de pièce jointe ou la génération de rapport échoueS3 mal configuré. Vérifier s3.external.*, que les cinq buckets existent (ou createBuckets=true), et les droits du compte d'accès. L'API journalise une ligne S3 backend: … au démarrage.
Les réplicas tous sur un seul nœudAucune anti-affinité définie. Ajouter une podAntiAffinity sous api.affinity / web.affinity / worker.affinity (voir Mono vs HA).

Connexes : Le produit · Architecture · Déployer sur Docker Swarm · Sécurité · Exploitation · Démo