# ============================================================================= # Backend NestJS # ============================================================================= --- apiVersion: apps/v1 kind: Deployment metadata: name: xpeditis-backend namespace: xpeditis-prod labels: app.kubernetes.io/name: xpeditis-backend app.kubernetes.io/component: api app.kubernetes.io/part-of: xpeditis spec: replicas: 2 revisionHistoryLimit: 5 strategy: type: RollingUpdate rollingUpdate: # Aucun pod retire avant qu'un remplacant ne soit pret : zero coupure. maxUnavailable: 0 maxSurge: 1 selector: matchLabels: app.kubernetes.io/name: xpeditis-backend template: metadata: labels: app.kubernetes.io/name: xpeditis-backend app.kubernetes.io/component: api app.kubernetes.io/part-of: xpeditis annotations: # Force le redemarrage des pods quand la configuration change : sans # cela, modifier le ConfigMap ne produit aucun effet visible. # La valeur est recalculee par scripts/deploy.sh. xpeditis.com/config-checksum: "PLACEHOLDER" spec: imagePullSecrets: - name: regcred # 2 replicas sur 1 noeud : la contrainte est "preferred", sinon le second # pod resterait indefiniment en Pending. Elle deviendra effective le jour # ou un second noeud sera ajoute (phase 2 du plan de charge). topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app.kubernetes.io/name: xpeditis-backend securityContext: runAsNonRoot: true runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 seccompProfile: type: RuntimeDefault # 60 s : laisse le temps aux requetes en cours et aux connexions # WebSocket de se fermer proprement. terminationGracePeriodSeconds: 60 initContainers: - name: seed-rates image: rg.fr-par.scw.cloud/weworkstudio/xpeditis-backend:latest command: [sh, -ec, 'cp -R /app/src/infrastructure/storage/csv-storage/rates/. /rates/'] securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] resources: requests: { cpu: 50m, memory: 64Mi } limits: { cpu: 200m, memory: 128Mi } volumeMounts: - name: rates mountPath: /rates containers: - name: backend # Le tag est remplace au deploiement (kubectl set image). image: rg.fr-par.scw.cloud/weworkstudio/xpeditis-backend:latest imagePullPolicy: IfNotPresent ports: - name: http containerPort: 4000 protocol: TCP envFrom: - configMapRef: name: xpeditis-backend-config - secretRef: name: xpeditis-backend-secrets env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name # Le demarrage inclut l'attente de PostgreSQL puis la verification # des migrations : la sonde de demarrage laisse jusqu'a 150 s avant # de declarer le pod perdu, sans penaliser les redemarrages rapides. startupProbe: httpGet: path: /api/v1/health/live port: http initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 30 timeoutSeconds: 3 livenessProbe: httpGet: path: /api/v1/health/live port: http periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 3 # LIMITE CONNUE : /health/ready renvoie toujours "ready" sans tester # PostgreSQL ni Redis (health.controller.ts). Un pod incapable de # joindre la base sera donc declare pret. Correctif recommande apres # la mise en ligne : @nestjs/terminus avec des indicateurs reels. readinessProbe: httpGet: path: /api/v1/health/ready port: http initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 resources: requests: cpu: 300m memory: 512Mi limits: cpu: 1500m memory: 1536Mi securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] readOnlyRootFilesystem: true volumeMounts: - name: rates mountPath: /app/src/infrastructure/storage/csv-storage/rates - name: logs mountPath: /app/logs - name: tmp mountPath: /tmp lifecycle: preStop: exec: # Laisse Traefik retirer le pod de son pool avant que le # processus ne commence a refuser des connexions. command: ["sh", "-c", "sleep 10"] volumes: - name: rates emptyDir: sizeLimit: 1Gi - name: logs emptyDir: sizeLimit: 512Mi - name: tmp emptyDir: sizeLimit: 256Mi --- apiVersion: v1 kind: Service metadata: name: xpeditis-backend namespace: xpeditis-prod labels: app.kubernetes.io/name: xpeditis-backend annotations: # Sessions collantes : obligatoires pour Socket.IO. La negociation # long-polling echoue si deux requetes d'un meme handshake atterrissent sur # des replicas differents. # # ATTENTION -- limite non resolue par ce reglage : le gateway # (notifications.gateway.ts) garde la carte userId -> sockets EN MEMOIRE et # n'utilise pas @socket.io/redis-adapter. Une notification emise par le # replica A n'atteint donc pas un utilisateur connecte au replica B. # Cf. docs/mise-en-prod/15-exploitation-incidents.md, "Points de vigilance". traefik.ingress.kubernetes.io/service.sticky.cookie: "true" traefik.ingress.kubernetes.io/service.sticky.cookie.name: "xpd_be" traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true" traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true" traefik.ingress.kubernetes.io/service.sticky.cookie.samesite: "lax" spec: type: ClusterIP selector: app.kubernetes.io/name: xpeditis-backend ports: - name: http port: 4000 targetPort: http protocol: TCP --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: xpeditis-backend namespace: xpeditis-prod spec: minAvailable: 1 selector: matchLabels: app.kubernetes.io/name: xpeditis-backend --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: xpeditis-backend namespace: xpeditis-prod spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: xpeditis-backend minReplicas: 2 # Plafond a 4 : au-dela, le CPX41 sature. Passer a 3 noeuds avant d'augmenter # (cf. 15-exploitation-incidents.md, section montee en charge). maxReplicas: 4 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleUp: stabilizationWindowSeconds: 60 scaleDown: # Descente lente : evite de retirer un pod juste avant un nouveau pic. stabilizationWindowSeconds: 600