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 ressourcesreadinessProbe= 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)preStophook = 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 :
- Stratégie : Rolling par défaut, canary pour critique
- Implementation : Graceful shutdown, health checks, PDB
- Database : Expand-contract pattern
- Monitoring : Alerter si erreur rate spike
Combinées, ces pratiques donnent des déploiements confiants, aucune interruption client.
À lire aussi :
- Disaster Recovery Kubernetes : êtes-vous vraiment prêt ?
- Observabilité à l'échelle : au-delà des dashboards Grafana
Cet article vous a été utile ? Découvrez comment Hidora peut vous accompagner : Professional Services · Managed Services · SLA Expert



