DevOps
Blog
DevOps11 min

Zero-downtime : le guide du CTO pour dormir tranquille

Mattia Eleuteri23 octobre 2025

Zero-downtime : le guide du CTO pour dormir tranquille

"Deploying at 3 AM because we couldn't do it during business hours", cette phrase était universelle il y a 10 ans. Maintenant, elle devrait être un antipattern.

Zero-downtime deployments ne sont plus un luxe. C'est une attente. Clients, auditeurs, et SLA contrats l'exigent. Cet article couvre les stratégies que les organisations matures utilisent.

Comprendre le déploiement sans downtime

Zero-downtime ≠ zero risk. Ça veut dire :

  • Les utilisateurs ne voient pas d'interruption de service
  • Requests en vol complètent proprement ou se retry automatiquement
  • État persiste (pas de perte de données)
  • Rollback possible en secondes si issue

Stratégie 1 : Rolling Update (par défaut Kubernetes)

La plus simple. Mettre à jour un pod à la fois.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Max 1 pod nouveau en plus = total 6 pods temporaire
      maxUnavailable: 0  # Min 0 pods down = toujours 5+ pods disponibles
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: api
        image: api:v2.0
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 3
      terminationGracePeriodSeconds: 30

Timeline :

v1.0 v1.0 v1.0 v1.0 v1.0  (5 pods)
↓
v2.0 v1.0 v1.0 v1.0 v1.0 v1.0  (1 nouveau pod lancé)
Wait for readiness...
↓
v2.0 v1.0 v1.0 v1.0 v1.0  (1 v1.0 tué)
↓
v2.0 v2.0 v1.0 v1.0 v1.0 v1.0  (2ème nouveau pod)
Wait...
↓
... (procédé répète)
↓
v2.0 v2.0 v2.0 v2.0 v2.0  (Déploiement complet)

Durée : ~5 minutes (maxSurge=1 = lent)
Risk : Aucun (toujours 5 pods)
Coût : Tempo +20% ressources (1 pod extra)

Points clés :

  • maxUnavailable: 0 = zéro downtime garantí
  • maxSurge: 1 = économe en ressources
  • readinessProbe = critique (dit à Kubernetes si pod est ready)
  • terminationGracePeriodSeconds = temps pour graceful shutdown

Cas d'usage : Standard. 80% des déploiements.

Stratégie 2 : Blue-Green Deployment

Lancer une version complète en parallèle, puis basculer le traffic en une seconde.

# BLUE (production actuelle)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-blue
spec:
  replicas: 5
  template:
    spec:
      containers:
      - name: api
        image: api:v1.0
        labels:
          version: blue
---
# GREEN (nouvelle version)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-green
spec:
  replicas: 5
  template:
    spec:
      containers:
      - name: api
        image: api:v2.0
        labels:
          version: green
---
# Service qui route le traffic
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    version: blue  # ← Pointe BLUE actuellement
  ports:
  - port: 80
    targetPort: 8080

Process de déploiement :

# 1. Green est déployée, tournée, testée
# (Blue toujours actif, aucun traffic à green)

# 2. Une fois GREEN prête
kubectl patch service api -p '{"spec":{"selector":{"version":"green"}}}'
# Traffic bascule INSTANTANÉMENT vers GREEN

# 3. Si GREEN bug
kubectl patch service api -p '{"spec":{"selector":{"version":"blue"}}}'
# Rollback en 1 seconde

# 4. Après confiance
kubectl delete deployment api-blue

Caractéristiques :

  • RTO : < 1 secondes (traffic switch)
  • Ressource cost : +100% temporaire (run 2x full set)
  • Complexity : Moyenne (2 deployments parallèles)
  • Rollback : Instantané (juste switch selector)

Cas d'usage : Breaking changes, major version upgrades, quand rollback ultra-rapide est critical.

Stratégie 3 : Canary Deployment

Basculer 10% du traffic à la nouvelle version, monitorer, puis augmenter graduellement.

apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: api
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  progressDeadlineSeconds: 60
  service:
    port: 80
    targetPort: 8080
  analysis:
    interval: 1m
    threshold: 5
    metrics:
    - name: request-success-rate
      thresholdRange:
        min: 99
      interval: 1m
    - name: request-duration
      thresholdRange:
        max: 500
      interval: 1m
    webhooks:
    - name: slack
      url: http://slack-notifier
      timeout: 5s
  skipAnalysis: false
  deployment:
    spec:
      template:
        spec:
          containers:
          - name: api
            image: api:v2.0
  match:
  - uri:
      prefix: /api
  maxWeight: 50
  stepWeight: 5  # Augmenter 5% du traffic chaque min

Timeline :

Time 0min    : 0% v2.0, 100% v1.0
Time 1min    : 5% v2.0, 95% v1.0  → Monitorer metrics
Time 2min    : 10% v2.0, 90% v1.0 → OK ? Continue
Time 3min    : 15% v2.0, 85% v1.0
...
Time 10min   : 50% v2.0, 50% v1.0 → Threshold met ? Rouler
Time 11min   : 100% v2.0, 0% v1.0 → Complet

Si metrics bad (error rate > threshold) :
Time 5min    : 25% v2.0 → ERROR RATE 5% !
ROLLBACK     : 0% v2.0, 100% v1.0 (automatic)

Utiliser Flagger (open source) :

helm repo add flagger https://flagger.app
helm install flagger flagger/flagger -n istio-system --create-namespace

Caractéristiques :

  • RTO : Si bug détecté, ~5 min average
  • Risk : Très bas (10% de traffic au départ)
  • Automation : Très high (auto-rollback si metrics bad)
  • Cost : +10-50% temporaire
  • Complexity : Élevée (monitoring + tooling)

Cas d'usage : Production critiques, haute velocity, confiance basse dans le changement.

Rolling Update vs Blue-Green vs Canary : matrice

Aspect Rolling Blue-Green Canary
RTO Lent (5-10 min) Instantané (1 sec) Moyen (1-5 min)
Risk Bas (graduel) Moyen (big bang) Très bas (progressive)
Resource cost +20% +100% +10-50%
Complexity Basse Moyenne Élevée
Rollback speed Lent Instantané Rapide (si metrics fail)
Best for Standard deploys Major changes High-criticality

Graceful shutdown : termination

Déploiement ne veut rien dire sans shutdown élégant.

// Code application
package main

import (
	"context"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	server := &http.Server{Addr: ":8080"}

	// HTTP handler
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
	})

	// Health check (pour readiness/liveness)
	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("healthy"))
	})

	// Handle graceful shutdown
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)

	go func() {
		<-sigChan
		// Kuberenet lui demande de shutdown
		ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
		defer cancel()

		// 1. Stop accepting new requests
		server.SetKeepAlivesEnabled(false)

		// 2. Wait for in-flight requests (max 25 sec, gracePeriod - 5 sec headroom)
		if err := server.Shutdown(ctx); err != nil {
			os.Exit(1)
		}
		os.Exit(0)
	}()

	server.ListenAndServe()
}

Configuration Kubernetes :

apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  terminationGracePeriodSeconds: 30  # ← Give 30 sec for shutdown
  containers:
  - name: api
    image: api:2.0
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]  # Wait for load balancer to drain

Timeline de shutdown :

T=0s : SIGTERM signal sent
T=0-5s : preStop hook runs (drain load balancer)
T=5-25s : In-flight requests complete gracefully
T=25-30s : Force kill (if not done)
T=30s : Pod removed

Points clés :

  • terminationGracePeriodSeconds = suffisamment long (30-60s)
  • preStop hook = delay pour que load balancer drain
  • Application handle SIGTERM = graceful shutdown code
  • Aucune interruption client

Pod Disruption Budgets : empêcher les disruptions

Kubernetes peut tuer vos pods pour maintenance cluster. Protégez-vous.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 3  # ← Au moins 3 pods disponibles TOUJOURS
  selector:
    matchLabels:
      app: api
# Alternative : maxUnavailable
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  maxUnavailable: 1  # ← Max 1 pod peut être disrupted
  selector:
    matchLabels:
      app: api

Avec PDB, Kubernetes refuse de tuer plus que minAvailable/maxUnavailable même en maintenance.

# Sans PDB : "sure, kill 3 pods for maintenance"
# Avec PDB : "sorry, only 1 pod can be disrupted, won't kill more"

Status : - PDB définis pour tous les services critiques (minAvailable ≥ 1)

Database migrations : le défi réel

Code peut déployer sans downtime, mais DB est difficile.

Pattern : Expand-Contract

-- Phase 1 (backward compatible)
ALTER TABLE users ADD COLUMN email VARCHAR(255);
-- v1.0 app n'écrit pas à email (null OK)

-- Phase 2 (v2.0 déploie)
-- v2.0 code utilise email (read + write)
-- v1.0 pods encore live (compatible car email nullable)

-- Phase 3 (migration data)
UPDATE users SET email = LOWER(email_old) WHERE email IS NULL;

-- Phase 4 (cleanup)
ALTER TABLE users DROP COLUMN email_old;

Autre exemple : Add index sans blocking

-- ❌ MAUVAIS : blocks writes
CREATE INDEX idx_users_email ON users(email);
-- ↑ Locks table pour 30+ minutes sur big tables

-- ✓ BON : CONCURRENTLY
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- ↑ Allows reads+writes, plus lent mais non-blocking

Checklist DB migrations :

    • Expand-contract pattern (backward compatible)
    • Indexes CONCURRENTLY (PostgreSQL) ou ALGORITHM=INPLACE (MySQL)
    • Test migrations sur replica avant production
    • Rollback plan (comment reverter ?)
    • Monitor replica lag (async replication)

Circuit Breaker : résilience

Même avec déploiement parfait, services dépendent les uns des autres.

// Avec Istio service mesh
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: api
spec:
  hosts:
  - api
  http:
  - route:
    - destination:
        host: api
        port:
          number: 8080
      weight: 100
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: api
spec:
  host: api
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        h2UpgradePolicy: UPGRADE
    outlierDetection:
      consecutiveErrors: 5
      interval: 30s
      baseEjectionTime: 30s
      maxEjectionPercent: 100
      minRequestVolume: 10

Si pod répond mal (5 erreurs en 30s), Istio le retire du pool temporairement.

Monitoring during deployment

# Alert sur error rate durant déploiement
alert: HighErrorRateDuringDeploy
expr: (rate(http_requests_total{status=~"5.."}[1m]) / rate(http_requests_total[1m])) > 0.01
for: 2m
annotations:
  summary: "Error rate {{ $value }} during deployment"

Dashboard :

  • Request latency (p50, p95, p99)
  • Error rate (5xx)
  • Pod ready count
  • Memory/CPU

Checklist Zero-downtime

    • Strategy choisie (rolling/blue-green/canary)
    • RollingUpdate avec maxUnavailable: 0
    • readinessProbe & livenessProbe configurés
    • Graceful shutdown implémenté (SIGTERM handler)
    • terminationGracePeriodSeconds suffisant (30-60s)
    • PDB configurés pour services critiques
    • DB migrations backward compatible
    • Monitoring/alertes pendant déploiement

Conclusion

Zero-downtime n'est pas magique. C'est une discipline :

  1. Stratégie : Rolling par défaut, canary pour critique
  2. Implementation : Graceful shutdown, health checks, PDB
  3. Database : Expand-contract pattern
  4. Monitoring : Alerter si erreur rate spike

Combinées, ces pratiques donnent des déploiements confiants, aucune interruption client.


À lire aussi :


Cet article vous a été utile ? Découvrez comment Hidora peut vous accompagner : Professional Services · Managed Services · SLA Expert

Cet article vous parle ?

Hidora peut vous accompagner sur ce sujet.

Besoin d'un accompagnement ?

Parlons de votre projet. 30 minutes, sans engagement.