DevOps22 janvier 202615 min

Déploiements Sans Interruption avec Kubernetes

Implémentation de mises à jour progressives et déploiements blue-green pour des releases en production sans accroc.

Pourquoi le Zéro Interruption est Important

Aux débuts du déploiement logiciel, mettre un système hors ligne pour maintenance faisait simplement partie du processus. Vous planifiez une fenêtre de maintenance, informez les utilisateurs, mettez le système hors ligne, déployez la nouvelle version, et le remettiez en ligne. Pour les systèmes critiques, cela signifiait se réveiller à 2h du matin un dimanche.

Les plateformes SaaS modernes ne peuvent pas se permettre d'interruption. Quand vous gérez un service global avec des utilisateurs à travers les fuseaux horaires, il n'y a pas de "bon moment" pour une panne. Chaque minute d'interruption signifie revenus perdus, utilisateurs frustrés et confiance endommagée. Pour beaucoup d'entreprises, même quelques secondes d'interruption pendant les heures de pointe peuvent coûter des milliers d'euros.

Kubernetes fournit des primitives puissantes pour les déploiements sans interruption, mais elles doivent être utilisées correctement. Dans cet article, nous explorerons comment implémenter les mises à jour progressives, les déploiements blue-green et les releases canary—tout sans impact utilisateur.

Comprendre les Mises à Jour Progressives Kubernetes

Les mises à jour progressives sont la stratégie de déploiement par défaut de Kubernetes. Au lieu de fermer tous les pods en même temps et d'en démarrer de nouveaux, Kubernetes remplace progressivement les anciens pods par de nouveaux, assurant que votre service reste disponible tout au long du déploiement.

Voici comment cela fonctionne :

  1. Kubernetes crée un nouveau pod avec la version mise à jour
  2. Il attend que le nouveau pod soit prêt (passe les contrôles de santé)
  3. Seulement après que le nouveau pod soit sain, il termine un ancien pod
  4. Ce processus se répète jusqu'à ce que tous les pods soient mis à jour

Les paramètres clés qui contrôlent ce comportement sont :

spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%        # Nombre max de pods supplémentaires pendant la mise à jour
      maxUnavailable: 0    # Ne jamais permettre que des pods soient indisponibles

Définir maxUnavailable: 0 est critique pour zéro interruption. Cela garantit que Kubernetes ne termine jamais un ancien pod jusqu'à ce qu'un nouveau soit complètement prêt à gérer le trafic.

Le Rôle Critique des Sondes de Disponibilité

Les mises à jour progressives ne fonctionnent que si Kubernetes sait quand un pod est réellement prêt à servir le trafic. C'est là qu'interviennent les sondes de disponibilité. Une sonde de disponibilité indique à Kubernetes si un pod doit recevoir du trafic.

Voici une configuration de sonde de disponibilité prête pour la production :

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 3
  successThreshold: 1
  failureThreshold: 3

Votre endpoint /health/ready devrait vérifier :

  • Connexions base de données : L'app peut-elle se connecter à la base de données ?
  • Dépendances critiques : Redis, files de messages, etc. sont-ils accessibles ?
  • Initialisation : L'app a-t-elle fini de charger la configuration, réchauffer les caches ?
  • Disponibilité des ressources : L'app a-t-elle les ressources dont elle a besoin ?

Une erreur courante est de rendre la sonde de disponibilité trop simple (juste retourner 200 OK) ou identique à la sonde de vivacité. La sonde de disponibilité devrait être plus complète—c'est acceptable qu'un pod soit vivant mais pas prêt à servir le trafic.

Gérer les Migrations de Base de Données en Toute Sécurité

Les migrations de base de données sont l'un des aspects les plus délicats des déploiements sans interruption. Vous ne pouvez pas simplement exécuter les migrations dans le démarrage de votre application car :

  1. Plusieurs pods pourraient essayer d'exécuter les migrations simultanément
  2. Les migrations pourraient ne pas être rétrocompatibles avec l'ancien code
  3. Les migrations longues pourraient bloquer le déploiement

Voici un pattern sûr utilisant les Jobs Kubernetes :

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration-v2.5.0
spec:
  backoffLimit: 0  # Ne pas réessayer les migrations échouées
  template:
    spec:
      restartPolicy: Never
      initContainers:
      - name: wait-for-db
        image: postgres:15-alpine
        command: ['sh', '-c', 'until pg_isready -h $DB_HOST -U $DB_USER; do sleep 2; done']
      containers:
      - name: migrate
        image: myapp:v2.5.0
        command: ["./run-migrations.sh"]
        env:
        - name: DB_HOST
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: host

Le workflow :

  1. Exécuter la migration comme Job Kubernetes avant de déployer la nouvelle version
  2. Attendre que la migration se termine avec succès
  3. Déployer la nouvelle version de l'application
  4. Seulement après que la nouvelle version soit stable, nettoyer les anciens chemins de code

Pour les migrations complexes, utilisez le pattern expand-contract :

  • Expand : Ajouter de nouvelles colonnes/tables sans supprimer les anciennes
  • Deploy : Déployer du code qui écrit à la fois dans l'ancien et le nouveau schéma
  • Migrate : Remplir rétroactivement les données de l'ancien vers le nouveau schéma
  • Contract : Supprimer les anciennes colonnes/tables dans une release ultérieure

Déploiements Blue-Green avec Kubernetes

Les mises à jour progressives sont excellentes, mais parfois vous avez besoin d'encore plus de contrôle. Les déploiements blue-green maintiennent deux environnements complets : blue (production actuelle) et green (nouvelle version). Le trafic est basculé instantanément de blue vers green une fois que l'environnement green est validé.

Dans Kubernetes, vous implémentez blue-green avec des labels et des sélecteurs de service :

# Déploiement Blue (production actuelle)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-blue
spec:
  replicas: 10
  selector:
    matchLabels:
      app: myapp
      version: blue
  template:
    metadata:
      labels:
        app: myapp
        version: blue
    spec:
      containers:
      - name: myapp
        image: myapp:v1.0.0
---
# Déploiement Green (nouvelle version)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-green
spec:
  replicas: 10
  selector:
    matchLabels:
      app: myapp
      version: green
  template:
    metadata:
      labels:
        app: myapp
        version: green
    spec:
      containers:
      - name: myapp
        image: myapp:v2.0.0
---
# Le service pointe vers blue initialement
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
    version: blue  # Changer vers 'green' pour basculer le trafic
  ports:
  - port: 80
    targetPort: 8080

Le processus de déploiement :

  1. Déployer l'environnement green à côté de blue
  2. Exécuter des tests smoke contre green (sans trafic production)
  3. Basculer le sélecteur de service de version: blue vers version: green
  4. Surveiller pour détecter des problèmes
  5. Si des problèmes surviennent, rollback instantané en rebasculant vers blue
  6. Après validation, démolir l'environnement blue

L'avantage ? Basculement de trafic instantané et rollback instantané. L'inconvénient ? Vous avez besoin de 2x les ressources pendant le déploiement.

Releases Canary : Atténuation Progressive du Risque

Les releases canary combinent le meilleur des deux mondes. Vous déployez la nouvelle version pour un petit pourcentage d'utilisateurs, surveillez les problèmes, et augmentez progressivement le trafic si tout va bien.

Bien que Kubernetes n'ait pas de support canary natif, vous pouvez l'implémenter avec plusieurs déploiements et division de trafic pondérée :

# Version stable (90% du trafic)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-stable
spec:
  replicas: 9
  selector:
    matchLabels:
      app: myapp
      track: stable
---
# Version Canary (10% du trafic)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
      track: canary
---
# Le service route vers les deux
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp  # Correspond à stable et canary
  ports:
  - port: 80
    targetPort: 8080

Avec 9 replicas stables et 1 replica canary, environ 10% du trafic va vers la nouvelle version. Augmentez progressivement les replicas canary tout en diminuant les replicas stables :

  • Début : 9 stable, 1 canary (10% trafic canary)
  • Après 1 heure : 7 stable, 3 canary (30% trafic canary)
  • Après 4 heures : 5 stable, 5 canary (50% trafic canary)
  • Après 8 heures : 0 stable, 10 canary (100% trafic canary)

Pour une division de trafic plus sophistiquée (pourcentages exacts, routage basé sur en-têtes, etc.), utilisez un service mesh comme Istio ou Linkerd.

Drainage de Connexions et Arrêt Gracieux

Quand Kubernetes termine un pod, votre application doit le gérer gracieusement. Sans gestion appropriée de l'arrêt, les requêtes en cours échoueront, causant des erreurs pour vos utilisateurs.

Kubernetes envoie un signal SIGTERM avant de tuer un pod. Votre application devrait :

  1. Arrêter d'accepter de nouvelles requêtes (faire échouer les contrôles de santé)
  2. Compléter les requêtes existantes (avec un timeout)
  3. Fermer les connexions base de données proprement
  4. Vider les logs et métriques
  5. Sortir avec code 0

Voici un exemple Node.js :

const express = require('express');
const app = express();
const server = app.listen(8080);

let isShuttingDown = false;

// Endpoint de contrôle de santé
app.get('/health/ready', (req, res) => {
  if (isShuttingDown) {
    res.status(503).send('Shutting down');
  } else {
    res.status(200).send('OK');
  }
});

// Gestionnaire d'arrêt gracieux
process.on('SIGTERM', () => {
  console.log('SIGTERM reçu, démarrage arrêt gracieux');
  isShuttingDown = true;

  // Arrêter d'accepter de nouvelles connexions
  server.close(() => {
    console.log('Serveur HTTP fermé');

    // Fermer les connexions base de données
    db.close().then(() => {
      console.log('Connexions base de données fermées');
      process.exit(0);
    });
  });

  // Forcer l'arrêt après 30 secondes
  setTimeout(() => {
    console.error('Arrêt forcé après timeout');
    process.exit(1);
  }, 30000);
});

Configurer Kubernetes pour donner à votre app assez de temps :

spec:
  terminationGracePeriodSeconds: 60  # Donner 60s à l'app pour s'arrêter
  containers:
  - name: myapp
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]  # Laisser k8s mettre à jour les endpoints

Le hook preStop avec un sleep de 5 secondes est crucial. Il donne à Kubernetes le temps de retirer le pod des endpoints de service avant que votre app arrête d'accepter des connexions. Sans cela, vous pourriez encore recevoir du trafic après le début de l'arrêt.

Stratégies de Surveillance et Rollback

Le déploiement sans interruption n'est pas complet sans surveillance appropriée et rollback automatisé. Vous devez détecter les problèmes rapidement et faire un rollback automatiquement.

Métriques clés à surveiller pendant le déploiement :

  • Taux d'erreur : réponses 5xx, erreurs applicatives
  • Latence : temps de réponse p50, p95, p99
  • Débit : requêtes par seconde
  • Utilisation des ressources : CPU, mémoire, pools de connexions
  • Métriques métier : taux de conversion, succès checkout, etc.

Implémenter le rollback automatisé avec le statut de rollout Kubernetes :

#!/bin/bash
# Déployer nouvelle version
kubectl apply -f deployment.yaml

# Attendre le rollout
if ! kubectl rollout status deployment/myapp --timeout=5m; then
  echo "Déploiement échoué, rollback en cours"
  kubectl rollout undo deployment/myapp
  exit 1
fi

# Surveiller le taux d'erreur pendant 5 minutes
for i in {1..30}; do
  ERROR_RATE=$(curl -s "http://metrics/api/error-rate?service=myapp")
  if (( $(echo "$ERROR_RATE > 1.0" | bc -l) )); then
    echo "Taux d'erreur trop élevé ($ERROR_RATE%), rollback en cours"
    kubectl rollout undo deployment/myapp
    exit 1
  fi
  sleep 10
done

echo "Déploiement réussi"

Pièges du Monde Réel et Leçons Apprises

1. Drainage de Connexions Load Balancer

Si vous utilisez un load balancer cloud (AWS ALB, GCP Load Balancer), configurez le drainage de connexions. Le load balancer doit arrêter d'envoyer du trafic à un pod avant que Kubernetes ne le termine.

service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: "true"
service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: "60"

2. Problèmes d'Affinité de Session

Si votre app utilise des sessions sticky, les mises à jour progressives peuvent casser les sessions utilisateur. Solutions :

  • Utiliser un store de sessions externe (Redis) au lieu de sessions en mémoire
  • Implémenter une logique de migration de session
  • Rendre les sessions optionnelles (dégradation gracieuse)

3. Connexions WebSocket

Les connexions WebSocket ne se drainent pas automatiquement. Vous devez :

  • Envoyer un message de fermeture aux clients avant l'arrêt
  • Implémenter une logique de reconnexion côté client
  • Utiliser un terminationGracePeriodSeconds plus long (120s+)

4. Verrous Distribués et Élection de Leader

Si votre app utilise l'élection de leader (un seul pod traite certaines tâches), assurez un transfert approprié pendant le déploiement. Utilisez des leases Kubernetes ou des implémentations de verrous distribués qui gèrent les changements de nœuds gracieusement.

Tout Assembler

Les déploiements sans interruption avec Kubernetes nécessitent une orchestration minutieuse de plusieurs composants :

  1. Choisissez votre stratégie : Mise à jour progressive pour la plupart des cas, blue-green pour les releases critiques, canary pour les rollouts graduels
  2. Implémentez des contrôles de santé appropriés : Sondes de disponibilité et vivacité qui reflètent fidèlement l'état de l'application
  3. Gérez les migrations avec soin : Utilisez des jobs pour les migrations, implémentez le pattern expand-contract
  4. Implémentez un arrêt gracieux : Gérez SIGTERM, drainez les connexions, définissez des timeouts appropriés
  5. Surveillez activement : Suivez le taux d'erreur, la latence et les métriques métier pendant le déploiement
  6. Automatisez le rollback : Ne comptez pas sur l'intervention manuelle pendant les incidents

L'investissement initial dans la mise en place de déploiements sans interruption est rentabilisé immédiatement. Vous pouvez déployer plusieurs fois par jour sans vous soucier de l'impact utilisateur, répondre aux incidents plus rapidement, et mieux dormir la nuit en sachant que vos déploiements ne vous réveilleront pas avec des pannes de production.

Besoin d'aide pour implémenter des déploiements sans interruption pour votre infrastructure ? Discutons-en. Nous aidons les équipes à concevoir des pipelines de déploiement robustes, migrer vers Kubernetes et construire des systèmes de production fiables.

Besoin d'Aide Pour Vos Systèmes de Production ?

Si vous rencontrez des défis similaires dans votre infrastructure de production, nous pouvons vous aider. Réservez un audit technique ou discutez directement avec notre CTO.