Pendant un rolling update, v1 et v2 tournent en même temps. La base doit donc être compatible avec les deux — et c’est l’ordre des opérations qui décide s’il y aura une panne.
Changer le tag de l’image et lancer kubectl apply suffit à déployer une nouvelle version. C’est justement le problème : c’est trop facile, et rien ne vous prévient que la base de données n’était pas prête.
La contrainte à comprendre tient en une observation. Un rolling update ne remplace pas les Pods d’un coup : il les remplace un par un, en gardant l’application disponible. Pendant plusieurs secondes ou minutes, la v1 et la v2 servent donc du trafic en même temps, sur la même base.
v1 v1 v1
↓
v2 v1 v1
↓
v2 v2 v1
↓
v2 v2 v2C’est le comportement souhaité, et c’est ce qui évite la coupure. Mais il implique une conséquence dont découle tout le reste :
À un instant donné, une requête peut être servie par la v1 ou par la v2, et vous ne choisissez pas laquelle.
Une base migrée pour ne convenir qu’à la v2 casse donc les Pods v1 encore vivants. Et une base laissée telle quelle casse les Pods v2 qui arrivent. Il n’y a pas d’instant magique où basculer : il faut une période où les deux fonctionnent.
On ne laisse pas Kubernetes découvrir tout seul que la v2 a besoin d’une colonne ou d’un service qui n’existent pas. On prépare l’environnement pour que les deux versions cohabitent, puis on déploie.
Ce motif a un nom, utile à connaître pour chercher plus loin : expand and contract, ou changement parallèle. On étend d’abord, on rétracte plus tard, et jamais dans le même déploiement.
Au départ, la base convient à la v1 :
users
├── id
├── name
└── addressLa v2 veut écrire dans new_address. La migration ajoute la colonne sans toucher à l’ancienne :
users
├── id
├── name
├── address ← la v1 continue de la lire
└── new_address ← la v2 l’utiliseLa base devient compatible avec les deux versions. C’est la seule situation où le rolling update est sans danger :
BASE
compatible v1 + v2
↑
┌─────┴─────┐
│ │
Pod v1 Pod v2C’est ici que les migrations échouent en pratique, et aucune de ces règles n’est évidente.
Additive uniquement. Ajouter une colonne, une table, un index. Jamais supprimer, jamais renommer, jamais changer un type. Une suppression casse la v1 ; un renommage est une suppression déguisée.
Pas de contrainte que la v1 ne peut pas satisfaire. Une colonne NOT NULL sans valeur par défaut fait échouer tous les INSERT de la v1, qui ne connaît pas ce champ. Deux issues : rendre la colonne nullable, ou lui donner une valeur par défaut. Le passage en NOT NULL attendra le nettoyage.
Verrous et durée. Selon le moteur, ajouter une colonne avec valeur par défaut peut réécrire toute la table et la verrouiller. Sur une table de taille sérieuse, la migration devient elle-même l’incident. À vérifier sur le moteur concerné avant de la lancer en production, pas après.
Un Job est prévu pour exécuter une tâche ponctuelle jusqu’à sa réussite, puis s’arrêter — exactement le cycle de vie d’une migration.
apiVersion: batch/v1
kind: Job
metadata:
name: migration-db-v2
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: migration
image: demo-k8s:2.0
command: ['python', 'migrate.py']
envFrom:
- secretRef:
name: demo-dbkubectl apply -f migration-v2.yaml
kubectl wait --for=condition=complete job/migration-db-v2 --timeout=300s
kubectl logs job/migration-db-v2kubectl wait plutôt qu’un kubectl get jobs à l’œil : dans un enchaînement automatisé, il faut un ordre qui bloque jusqu’à la réussite et échoue sur un délai dépassé.
Les identifiants de connexion viennent d’un Secret et non d’un ConfigMap : une migration se connecte à la base avec un mot de passe, qui n’a pas à figurer dans le manifeste.
Trois détails qui comptent :
restartPolicy: Never avec backoffLimit : le Job réessaie un nombre borné de fois, puis renonce. Sans plafond, une migration cassée boucle indéfiniment.ADD COLUMN IF NOT EXISTS, ou une table de versions de schéma comme le font les outils de migration.migration-db-v2, pas migration. Un Job est immuable : réappliquer le même nom avec un contenu différent échoue, et le réappliquer à l’identique ne relance rien.Parce qu’un initContainer s’exécute une fois par Pod. Avec trois réplicas, ce sont trois migrations lancées en parallèle sur la même base — et à chaque redémarrage de Pod, à chaque montée en charge, une de plus.
Un Job s’exécute une fois pour le cluster. C’est la bonne granularité. L’initContainer reste utile pour attendre que la base soit joignable, ce qui est une vérification et non une écriture.
Une fois la base compatible, la mise à jour de l’application est le geste simple qu’on croyait faire depuis le début :
containers:
- name: demo-web
image: demo-k8s:2.0kubectl apply -f k8s/deployment.yaml
kubectl rollout status deployment/demo-web
kubectl get pods --watchDeux réglages gouvernent la façon dont Kubernetes procède :
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1maxUnavailable: 0 — jamais moins de Pods prêts que le nombre demandé. La capacité de service ne baisse pas pendant la mise à jour.maxSurge: 1 — un Pod supplémentaire au maximum pendant la transition.Cette paire donne le déploiement le plus prudent : Kubernetes crée un nouveau Pod, attend qu’il soit prêt, puis retire un ancien. Elle exige un Pod de capacité en plus, et elle est plus lente. Les valeurs par défaut, 25 % de chaque, vont plus vite en acceptant une baisse temporaire de capacité.
Le mot important est prêt. Sans readinessProbe, Kubernetes considère un Pod disponible dès que son conteneur démarre, et le Service lui envoie du trafic pendant son initialisation. Un rolling update sans sonde de disponibilité produit une coupure — silencieuse, brève, et bien réelle.
La logique est identique, et l’ordre encore plus contre-intuitif.
1. Déployer Redis
↓
2. Le tester
↓
3. La v1 fonctionne toujours
↓
4. Rolling update vers la v2
↓
5. Vérifier la v2
↓
6. Retirer l’ancienne dépendanceCe qu’il ne faut pas faire :
✗ Retirer l’ancienne dépendance
↓
Déployer la v2Parce que pendant la transition, des Pods v1 tournent encore et utilisent cette dépendance. La retirer avant, c’est casser la version qui sert actuellement les utilisateurs pour préparer celle qui ne sert encore personne.
Quand tous les Pods sont en v2 et que la version est vérifiée, l’ancienne colonne peut partir. Pas avant, et pour une raison précise :
kubectl rollout undo deployment/demo-webCe retour en arrière ramène la v1 en quelques secondes. C’est le filet de sécurité du Deployment — et il ne fonctionne que si la base sait encore parler à la v1. Une colonne supprimée dans le même déploiement rend le retour impossible au moment exact où on en a besoin.
Une bonne pratique est donc de laisser passer un délai : quelques jours en production, le temps que les vrais usages révèlent ce que les tests n’ont pas vu. Le nettoyage devient une migration à part entière, avec son propre Job et sa propre version.
Le point le plus subtil, et celui qui distingue une réponse d’ingénieur.
Pendant la coexistence, la v1 écrit dans address et la v2 dans new_address. À la fin de la mise à jour, les lignes créées par la v1 ont donc un new_address vide. La v2 doit les traiter — soit en lisant les deux champs, soit après un remplissage rétroactif.
Le schéma complet d’un renommage de colonne, qui est le cas où tout se joue :
new_address, nullable.Six étapes et quatre déploiements pour renommer une colonne. Cela paraît excessif jusqu’à la première fois où l’on essaie de le faire en une seule fois sur une base en service.
Si la base tourne elle-même dans Kubernetes, elle ne se déploie pas comme l’application. Un Deployment traite ses Pods comme interchangeables ; une base a besoin d’une identité réseau stable et d’un stockage persistant qui la suit. C’est le rôle du StatefulSet : des noms ordonnés et prévisibles, un volume attaché par instance, un démarrage et un arrêt dans l’ordre.
Et la question à se poser avant : faut-il vraiment héberger la base dans le cluster ? Une base gérée par le fournisseur d’infrastructure retire de vos épaules les sauvegardes, les bascules et les mises à jour de version. Le StatefulSet est un outil correct, pas une obligation.
Une phrase, si vous n’en gardez qu’une :
On rend d’abord l’infrastructure et la base compatibles avec la v1 et la v2, ensuite seulement on lance le rolling update, et on ne nettoie l’ancien que lorsque la v1 a complètement disparu.
Le raisonnement qui la produit vaut mieux que la règle : un rolling update crée nécessairement une période de coexistence, donc tout ce que partagent les deux versions doit convenir aux deux. Une fois cette phrase comprise, l’ordre des étapes se déduit tout seul.
Pour le reste du déploiement : le Deployment et ses sondes, le projet complet mis en pratique, et la répartition des rôles entre Docker et Kubernetes.
Développement et déploiement de solutions de données — les premiers modules sont en accès libre.