# 09 — Manifests Kubernetes complets Tous les fichiers YAML de déploiement de Xpeditis. Créez un dossier `k8s/` à la racine du projet. --- ## Structure des fichiers ``` k8s/ ├── 00-namespaces.yaml ├── 01-secrets.yaml # ← À remplir avec vos valeurs (ne pas committer) ├── 02-configmaps.yaml ├── 03-backend-deployment.yaml ├── 04-backend-service.yaml ├── 05-frontend-deployment.yaml ├── 06-frontend-service.yaml ├── 07-ingress.yaml ├── 08-hpa.yaml └── 09-pdb.yaml ``` --- ## 00 — Namespaces ```yaml # k8s/00-namespaces.yaml --- apiVersion: v1 kind: Namespace metadata: name: xpeditis-prod labels: environment: production app.kubernetes.io/managed-by: hetzner-k3s ``` ```bash kubectl apply -f k8s/00-namespaces.yaml ``` --- ## 01 — Secrets (⚠️ ne jamais committer ce fichier dans Git) Ajoutez `k8s/01-secrets.yaml` à votre `.gitignore`. ```yaml # k8s/01-secrets.yaml ← AJOUTER AU .gitignore --- apiVersion: v1 kind: Secret metadata: name: backend-secrets namespace: xpeditis-prod type: Opaque stringData: # Application NODE_ENV: "production" PORT: "4000" API_PREFIX: "api/v1" APP_URL: "https://app.xpeditis.com" FRONTEND_URL: "https://app.xpeditis.com" # Base de données (choisir Option A ou B) # === Option A : Neon.tech === DATABASE_HOST: "ep-xxx.eu-central-1.aws.neon.tech" DATABASE_PORT: "5432" DATABASE_USER: "xpeditis" DATABASE_PASSWORD: "" DATABASE_NAME: "xpeditis" DATABASE_SSL: "true" DATABASE_SYNC: "false" DATABASE_LOGGING: "false" # === Option B : Self-hosted === # DATABASE_HOST: "10.0.1.100" # IP privée Hetzner du serveur PG # DATABASE_PORT: "6432" # PgBouncer # DATABASE_USER: "xpeditis" # DATABASE_PASSWORD: "" # DATABASE_NAME: "xpeditis_prod" # DATABASE_SYNC: "false" # DATABASE_LOGGING: "false" # Redis (choisir Option A ou B) # === Option A : Upstash === REDIS_HOST: "your-redis.upstash.io" REDIS_PORT: "6379" REDIS_PASSWORD: "" REDIS_DB: "0" # === Option B : Self-hosted === # REDIS_HOST: "redis.xpeditis-prod.svc.cluster.local" # REDIS_PORT: "6379" # REDIS_PASSWORD: "" # REDIS_DB: "0" # JWT JWT_SECRET: "" JWT_ACCESS_EXPIRATION: "15m" JWT_REFRESH_EXPIRATION: "7d" # OAuth2 Google GOOGLE_CLIENT_ID: "" GOOGLE_CLIENT_SECRET: "" GOOGLE_CALLBACK_URL: "https://api.xpeditis.com/api/v1/auth/google/callback" # OAuth2 Microsoft MICROSOFT_CLIENT_ID: "" MICROSOFT_CLIENT_SECRET: "" MICROSOFT_CALLBACK_URL: "https://api.xpeditis.com/api/v1/auth/microsoft/callback" # Email (Brevo SMTP — remplace SendGrid) SMTP_HOST: "smtp-relay.brevo.com" SMTP_PORT: "587" SMTP_SECURE: "false" SMTP_USER: "" SMTP_PASS: "" SMTP_FROM: "noreply@xpeditis.com" # Hetzner Object Storage (remplace MinIO) AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com" AWS_ACCESS_KEY_ID: "" AWS_SECRET_ACCESS_KEY: "" AWS_REGION: "eu-central-1" AWS_S3_BUCKET: "xpeditis-prod" # Carrier APIs MAERSK_API_KEY: "" MAERSK_API_URL: "https://api.maersk.com/v1" MSC_API_KEY: "" MSC_API_URL: "https://api.msc.com/v1" CMACGM_API_URL: "https://api.cma-cgm.com/v1" CMACGM_CLIENT_ID: "" CMACGM_CLIENT_SECRET: "" HAPAG_API_URL: "https://api.hapag-lloyd.com/v1" HAPAG_API_KEY: "" ONE_API_URL: "https://api.one-line.com/v1" ONE_USERNAME: "" ONE_PASSWORD: "" # Stripe STRIPE_SECRET_KEY: "sk_live_<...>" STRIPE_WEBHOOK_SECRET: "whsec_<...>" STRIPE_SILVER_MONTHLY_PRICE_ID: "price_<...>" STRIPE_SILVER_YEARLY_PRICE_ID: "price_<...>" STRIPE_GOLD_MONTHLY_PRICE_ID: "price_<...>" STRIPE_GOLD_YEARLY_PRICE_ID: "price_<...>" STRIPE_PLATINIUM_MONTHLY_PRICE_ID: "price_<...>" STRIPE_PLATINIUM_YEARLY_PRICE_ID: "price_<...>" # Sécurité BCRYPT_ROUNDS: "12" SESSION_TIMEOUT_MS: "7200000" RATE_LIMIT_TTL: "60" RATE_LIMIT_MAX: "100" # Monitoring SENTRY_DSN: "" --- apiVersion: v1 kind: Secret metadata: name: frontend-secrets namespace: xpeditis-prod type: Opaque stringData: NEXT_PUBLIC_API_URL: "https://api.xpeditis.com" NEXT_PUBLIC_APP_URL: "https://app.xpeditis.com" NEXT_PUBLIC_API_PREFIX: "api/v1" NEXTAUTH_URL: "https://app.xpeditis.com" NEXTAUTH_SECRET: "" GOOGLE_CLIENT_ID: "" GOOGLE_CLIENT_SECRET: "" MICROSOFT_CLIENT_ID: "" MICROSOFT_CLIENT_SECRET: "" NODE_ENV: "production" ``` ```bash # Générer les secrets aléatoires echo "JWT_SECRET=$(openssl rand -base64 48)" echo "NEXTAUTH_SECRET=$(openssl rand -base64 24)" # Appliquer (après avoir rempli les valeurs) kubectl apply -f k8s/01-secrets.yaml # Vérifier (sans voir les valeurs) kubectl get secret backend-secrets -n xpeditis-prod -o jsonpath='{.data}' | jq 'keys' ``` --- ## 02 — ConfigMaps (variables non-sensibles) ```yaml # k8s/02-configmaps.yaml --- apiVersion: v1 kind: ConfigMap metadata: name: backend-config namespace: xpeditis-prod data: # Ces valeurs ne sont pas sensibles LOG_LEVEL: "info" TZ: "Europe/Paris" --- apiVersion: v1 kind: ConfigMap metadata: name: frontend-config namespace: xpeditis-prod data: TZ: "Europe/Paris" ``` --- ## 03 — Deployment Backend NestJS ```yaml # k8s/03-backend-deployment.yaml --- apiVersion: apps/v1 kind: Deployment metadata: name: xpeditis-backend namespace: xpeditis-prod labels: app: xpeditis-backend version: "latest" spec: replicas: 2 selector: matchLabels: app: xpeditis-backend strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 # Zero downtime deployment template: metadata: labels: app: xpeditis-backend version: "latest" annotations: prometheus.io/scrape: "true" prometheus.io/port: "4000" prometheus.io/path: "/api/v1/health" spec: # Anti-affinité : pods sur nœuds différents affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - xpeditis-backend topologyKey: kubernetes.io/hostname # Temps de grâce pour les connexions WebSocket terminationGracePeriodSeconds: 60 containers: - name: backend # L'image est mise à jour par le CI/CD (doc 11) image: ghcr.io//xpeditis-backend:latest imagePullPolicy: Always ports: - containerPort: 4000 name: http protocol: TCP # Variables d'environnement depuis les Secrets envFrom: - secretRef: name: backend-secrets - configMapRef: name: backend-config # Resources (MVP — ajuster selon les métriques réelles) resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "2000m" memory: "1.5Gi" # Health checks startupProbe: httpGet: path: /api/v1/health port: 4000 initialDelaySeconds: 20 periodSeconds: 5 failureThreshold: 12 # 60 secondes max au démarrage readinessProbe: httpGet: path: /api/v1/health port: 4000 initialDelaySeconds: 5 periodSeconds: 10 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /api/v1/health port: 4000 initialDelaySeconds: 60 periodSeconds: 30 failureThreshold: 3 # Lifecycle hook pour graceful shutdown lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 10"] # Laisse le temps au LB de retirer le pod # Pull depuis GHCR (GitHub Container Registry) imagePullSecrets: - name: ghcr-credentials # Redémarrage automatique restartPolicy: Always ``` --- ## 04 — Service Backend ```yaml # k8s/04-backend-service.yaml --- apiVersion: v1 kind: Service metadata: name: xpeditis-backend namespace: xpeditis-prod labels: app: xpeditis-backend spec: selector: app: xpeditis-backend ports: - name: http port: 4000 targetPort: 4000 protocol: TCP type: ClusterIP ``` --- ## 05 — Deployment Frontend Next.js ```yaml # k8s/05-frontend-deployment.yaml --- apiVersion: apps/v1 kind: Deployment metadata: name: xpeditis-frontend namespace: xpeditis-prod labels: app: xpeditis-frontend spec: replicas: 1 selector: matchLabels: app: xpeditis-frontend strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: xpeditis-frontend spec: terminationGracePeriodSeconds: 30 containers: - name: frontend image: ghcr.io//xpeditis-frontend:latest imagePullPolicy: Always ports: - containerPort: 3000 name: http envFrom: - secretRef: name: frontend-secrets - configMapRef: name: frontend-config resources: requests: cpu: "250m" memory: "256Mi" limits: cpu: "1000m" memory: "768Mi" startupProbe: httpGet: path: / port: 3000 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 12 readinessProbe: httpGet: path: / port: 3000 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: / port: 3000 initialDelaySeconds: 30 periodSeconds: 30 failureThreshold: 3 lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 5"] imagePullSecrets: - name: ghcr-credentials restartPolicy: Always ``` --- ## 06 — Service Frontend ```yaml # k8s/06-frontend-service.yaml --- apiVersion: v1 kind: Service metadata: name: xpeditis-frontend namespace: xpeditis-prod labels: app: xpeditis-frontend spec: selector: app: xpeditis-frontend ports: - name: http port: 3000 targetPort: 3000 protocol: TCP type: ClusterIP ``` --- ## 07 — Ingress (Traefik + TLS) ```yaml # k8s/07-ingress.yaml --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: xpeditis-ingress namespace: xpeditis-prod annotations: # TLS via cert-manager cert-manager.io/cluster-issuer: "letsencrypt-prod" # Traefik config traefik.ingress.kubernetes.io/router.entrypoints: "websecure" traefik.ingress.kubernetes.io/router.tls: "true" # Sticky sessions pour WebSocket Socket.IO traefik.ingress.kubernetes.io/service.sticky.cookie: "true" traefik.ingress.kubernetes.io/service.sticky.cookie.name: "XPEDITIS_BACKEND" traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true" traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true" # Timeout pour les longues requêtes (carrier APIs = jusqu'à 30s) traefik.ingress.kubernetes.io/router.middlewares: "xpeditis-prod-ratelimit@kubernetescrd" # Headers de sécurité traefik.ingress.kubernetes.io/router.middlewares: "xpeditis-prod-headers@kubernetescrd" spec: ingressClassName: traefik tls: - hosts: - api.xpeditis.com - app.xpeditis.com secretName: xpeditis-tls-prod rules: # API Backend NestJS - host: api.xpeditis.com http: paths: - path: / pathType: Prefix backend: service: name: xpeditis-backend port: number: 4000 # Frontend Next.js - host: app.xpeditis.com http: paths: - path: / pathType: Prefix backend: service: name: xpeditis-frontend port: number: 3000 --- # Middleware : headers de sécurité apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: headers namespace: xpeditis-prod spec: headers: customRequestHeaders: X-Forwarded-Proto: "https" customResponseHeaders: X-Frame-Options: "SAMEORIGIN" X-Content-Type-Options: "nosniff" X-XSS-Protection: "1; mode=block" Referrer-Policy: "strict-origin-when-cross-origin" Permissions-Policy: "geolocation=(), microphone=(), camera=()" contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" stsSeconds: 31536000 stsIncludeSubdomains: true stsPreload: true --- # Middleware : rate limiting Traefik (en plus du rate limiting NestJS) apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: ratelimit namespace: xpeditis-prod spec: rateLimit: average: 100 burst: 50 period: 1m sourceCriterion: ipStrategy: depth: 1 ``` --- ## 08 — Horizontal Pod Autoscaler ```yaml # k8s/08-hpa.yaml --- # HPA Backend apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: backend-hpa namespace: xpeditis-prod spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: xpeditis-backend minReplicas: 2 maxReplicas: 15 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 policies: - type: Pods value: 2 periodSeconds: 60 scaleDown: stabilizationWindowSeconds: 300 # 5 min avant de réduire policies: - type: Pods value: 1 periodSeconds: 120 --- # HPA Frontend apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: frontend-hpa namespace: xpeditis-prod spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: xpeditis-frontend minReplicas: 1 maxReplicas: 8 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 behavior: scaleDown: stabilizationWindowSeconds: 300 ``` --- ## 09 — PodDisruptionBudget ```yaml # k8s/09-pdb.yaml --- # Garantit qu'au moins 1 pod backend est toujours disponible pendant les maintenances apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: backend-pdb namespace: xpeditis-prod spec: minAvailable: 1 selector: matchLabels: app: xpeditis-backend --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: frontend-pdb namespace: xpeditis-prod spec: minAvailable: 1 selector: matchLabels: app: xpeditis-frontend ``` --- ## Secret GHCR (GitHub Container Registry) Pour que Kubernetes puisse pull les images depuis GHCR : ```bash # Créer un Personal Access Token GitHub avec scope: read:packages # https://github.com/settings/tokens/new kubectl create secret docker-registry ghcr-credentials \ --namespace xpeditis-prod \ --docker-server=ghcr.io \ --docker-username= \ --docker-password= \ --docker-email= ``` --- ## Déploiement complet ```bash # Appliquer tous les manifests dans l'ordre kubectl apply -f k8s/00-namespaces.yaml kubectl apply -f k8s/01-secrets.yaml # Après avoir rempli les valeurs kubectl apply -f k8s/02-configmaps.yaml kubectl apply -f k8s/03-backend-deployment.yaml kubectl apply -f k8s/04-backend-service.yaml kubectl apply -f k8s/05-frontend-deployment.yaml kubectl apply -f k8s/06-frontend-service.yaml kubectl apply -f k8s/07-ingress.yaml kubectl apply -f k8s/08-hpa.yaml kubectl apply -f k8s/09-pdb.yaml # Ou tout d'un coup kubectl apply -f k8s/ # Suivre le déploiement kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod kubectl rollout status deployment/xpeditis-frontend -n xpeditis-prod # Voir les pods kubectl get pods -n xpeditis-prod -w # Voir les logs kubectl logs -f deployment/xpeditis-backend -n xpeditis-prod kubectl logs -f deployment/xpeditis-frontend -n xpeditis-prod # Vérifier le certificat TLS kubectl get certificate -n xpeditis-prod # NAME READY SECRET AGE # xpeditis-tls-prod True xpeditis-tls-prod 2m ``` --- ## Migration des jobs TypeORM Le déploiement inclut automatiquement les migrations via le `startup.js` dans le Dockerfile. Si vous avez besoin de lancer les migrations manuellement : ```bash # Job de migration one-shot cat > /tmp/migration-job.yaml << 'EOF' apiVersion: batch/v1 kind: Job metadata: name: xpeditis-migrations namespace: xpeditis-prod spec: template: spec: restartPolicy: OnFailure containers: - name: migrations image: ghcr.io//xpeditis-backend:latest command: ["node", "dist/migration-runner.js"] envFrom: - secretRef: name: backend-secrets imagePullSecrets: - name: ghcr-credentials EOF kubectl apply -f /tmp/migration-job.yaml kubectl wait --for=condition=complete job/xpeditis-migrations -n xpeditis-prod --timeout=300s kubectl logs job/xpeditis-migrations -n xpeditis-prod kubectl delete job xpeditis-migrations -n xpeditis-prod ```