Aller au contenu principal
DataETLDevOpsBackup

Migrations Zero-Downtime : Flyway, Liquibase et Blue-Green DB

30 min de lecture Gestion des Données — Chapitre 4

Maîtrise les migrations de bases de données sans interruption de service avec Flyway, Liquibase, et les stratégies blue-green pour tes déploiements.

Pourquoi les migrations de schéma sont un enjeu critique

Migrer une base de données en production, c’est comme changer le moteur d’un avion en vol. Un ALTER TABLE qui verrouille une table de 50 millions de lignes pendant 10 minutes ? Ton application est down. Un changement de schéma incompatible avec l’ancienne version de l’app encore en cours de déploiement ? Des erreurs 500 en cascade.

En environnement DevOps moderne, les déploiements sont continus — parfois plusieurs fois par jour. Chaque migration de schéma doit être backward compatible, réversible et sans interruption de service. C’est un art qui demande des outils adaptés (Flyway, Liquibase) et surtout une méthodologie rigoureuse.

🧠 À retenir — Pendant un rolling deployment, l’ancienne et la nouvelle version de l’app coexistent pendant plusieurs minutes. La base doit être compatible avec les deux simultanément.


Le pattern Expand-Contract : la clé de tout

Avant d’aborder les outils, il faut comprendre la méthodologie qui rend les migrations safe. Le pattern expand-contract décompose chaque changement dangereux en étapes où rien ne casse.

Prenons l’exemple classique : renommer une colonne name en full_name. En une seule commande (ALTER TABLE users RENAME COLUMN name TO full_name), l’ancienne app cherche name et crash immédiatement. Avec expand-contract, on procède en phases :

-- Phase 1 : EXPAND — ajouter la nouvelle colonne (safe, pas de lock)
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

-- Phase 2 : BACKFILL — copier les données + synchroniser
UPDATE users SET full_name = name WHERE full_name IS NULL;
-- + trigger pour synchroniser les deux colonnes pendant la transition
CREATE OR REPLACE FUNCTION sync_name_columns() RETURNS TRIGGER AS $$
BEGIN
    IF NEW.name IS DISTINCT FROM OLD.name THEN NEW.full_name := NEW.name; END IF;
    IF NEW.full_name IS DISTINCT FROM OLD.full_name THEN NEW.name := NEW.full_name; END IF;
    RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER sync_names BEFORE UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION sync_name_columns();

-- Phase 3 : Déployer app v2 qui utilise full_name
-- (app v1 continue de fonctionner via name)

-- Phase 4 : CONTRACT — supprimer l'ancienne colonne (quand v1 est retirée)
DROP TRIGGER sync_names ON users;
DROP FUNCTION sync_name_columns();
ALTER TABLE users DROP COLUMN name;

Ce processus est plus long, mais il garantit zéro interruption. Chaque phase est safe individuellement et réversible.

💡 Tip DevOps — En règle générale : si une migration peut tenir dans un ALTER TABLE ADD COLUMN nullable, c’est safe. Dès que tu touches à un rename, un type change ou un NOT NULL, passe en expand-contract.

🔥 Cas réel — Une plateforme SaaS a fait un ALTER TABLE ... ALTER COLUMN type sur une table de 200M de lignes en production. PostgreSQL a réécrit toute la table pendant 25 minutes avec un lock exclusif. 25 minutes de downtime complet, des milliers d’utilisateurs impactés. Le pattern expand-contract aurait réduit l’impact à zéro.


Flyway : migrations SQL versionnées

Flyway est l’outil le plus populaire pour versionner les migrations de bases de données. Sa philosophie est simple : des fichiers SQL numérotés, exécutés dans l’ordre, tracés dans une table d’historique. Pas de DSL XML, pas de magie — juste du SQL que tu maîtrises.

La convention de nommage est stricte : V{version}__{description}.sql pour les migrations versionnées (exécutées une seule fois), R__{description}.sql pour les repeatable (recréées si le contenu change).

# Structure d'un projet Flyway
sql/migrations/
├── V1__create_users_table.sql       # Exécutée une fois
├── V2__add_profile_fields.sql       # Exécutée une fois
├── V3__create_orders_partitioned.sql # Exécutée une fois
├── R__create_reporting_views.sql     # Recréée si modifiée

Voici un exemple de migration bien structurée, avec des commentaires et des index appropriés :

-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(100) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);

-- V2__add_profile_fields.sql (expand phase — colonnes nullable)
ALTER TABLE users ADD COLUMN avatar_url TEXT;
ALTER TABLE users ADD COLUMN bio TEXT;
ALTER TABLE users ADD COLUMN location VARCHAR(100);

Les commandes Flyway au quotidien sont simples : info pour voir l’état, validate pour vérifier la cohérence, migrate pour appliquer :

# État des migrations
flyway info

# Vérifier que tout est cohérent (checksums, ordre)
flyway validate

# Appliquer les migrations en attente
flyway migrate

# En CI/CD via Docker
docker run --rm -v $(pwd)/sql:/flyway/sql flyway/flyway \
    -url=jdbc:postgresql://db:5432/myapp \
    -user=flyway -password="${FLYWAY_PASSWORD}" \
    migrate

⚠️ Attention — Active toujours cleanDisabled = true en production. La commande flyway clean supprime tout le schéma — un seul accident suffit pour perdre toutes tes données.


Liquibase : quand la flexibilité prime

Là où Flyway est simple et SQL-centric, Liquibase offre une flexibilité supérieure : rollback gratuit, préconditions, contextes (dev/staging/prod), et surtout la génération de diff entre deux bases de données.

Les changelogs Liquibase en YAML sont lisibles et déclaratifs. Chaque changeset inclut son rollback, ce qui permet de revenir en arrière proprement :

# changelog.yaml
databaseChangeLog:
  - changeSet:
      id: 1
      author: dany
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: BIGSERIAL
                  constraints: { primaryKey: true }
              - column:
                  name: email
                  type: VARCHAR(255)
                  constraints: { nullable: false, unique: true }
      rollback:
        - dropTable: { tableName: users }

  - changeSet:
      id: 2
      author: dany
      context: "!production"
      comment: "Données de test — pas exécuté en prod"
      changes:
        - insert:
            tableName: users
            columns:
              - column: { name: email, value: "test@lab.dev" }

Les commandes clés de Liquibase permettent un contrôle fin, notamment le diff entre bases qui est très utile pour vérifier qu’un staging est bien aligné avec la prod :

# Appliquer les changements
liquibase update

# Rollback du dernier changeset
liquibase rollback-count 1

# Générer le SQL sans exécuter (dry-run)
liquibase update-sql > pending.sql

# Comparer staging vs prod
liquibase diff \
    --reference-url=jdbc:postgresql://staging:5432/myapp \
    --url=jdbc:postgresql://prod:5432/myapp

💡 Tip DevOps — Flyway ou Liquibase ? Si ton équipe écrit du SQL et veut rester simple, Flyway. Si tu as besoin de rollback gratuit, de contextes multi-environnement ou de diff automatique, Liquibase. Les deux sont excellents — le pire choix est de ne pas versionner du tout.


Opérations dangereuses et alternatives safe

Certaines opérations DDL sont des pièges en production car elles verrouillent les tables. Il faut connaître les alternatives safe pour chaque cas courant.

La création d’index est le piège le plus fréquent. CREATE INDEX standard pose un lock en écriture pendant toute la durée de la construction. Sur une grosse table, ça peut durer des minutes :

-- ❌ Lock en écriture pendant toute la construction
CREATE INDEX idx_orders_user ON orders(user_id);

-- ✅ Création concurrente — pas de lock, mais plus lent
CREATE INDEX CONCURRENTLY idx_orders_user ON orders(user_id);

-- Vérifier que l'index est valide après création concurrente
SELECT indexrelid::regclass, indisvalid
FROM pg_index WHERE indexrelid = 'idx_orders_user'::regclass;

Pour les migrations de type de colonne (ex: TEXT → JSONB), le backfill par batch évite de surcharger la base et de poser un lock monolithique :

-- Backfill par batch de 5000 lignes avec pause
DO $$
DECLARE batch_size INT := 5000; rows_updated INT;
BEGIN
    LOOP
        UPDATE events SET payload_jsonb = payload::jsonb
        WHERE id IN (
            SELECT id FROM events WHERE payload_jsonb IS NULL LIMIT batch_size
        );
        GET DIAGNOSTICS rows_updated = ROW_COUNT;
        EXIT WHEN rows_updated = 0;
        PERFORM pg_sleep(0.1);  -- Pause pour laisser respirer la DB
    END LOOP;
END $$;

🧠 À retenir — Depuis PostgreSQL 11, ADD COLUMN ... DEFAULT value est safe (pas de rewrite de table). Mais ALTER COLUMN TYPE, ADD COLUMN NOT NULL sans default, et RENAME COLUMN restent dangereux.


Blue-Green Database et bonnes pratiques

Le blue-green pour les bases de données utilise la réplication logique PostgreSQL pour maintenir deux instances synchronisées. On applique les migrations sur la copie Green, puis on bascule le trafic une fois que tout est validé.

La procédure en résumé : configurer la réplication logique depuis Blue vers Green, attendre la synchronisation complète, passer Blue en read-only, appliquer les migrations sur Green, basculer le trafic applicatif, et vérifier la santé de l’application.

#!/bin/bash
# blue-green-switch.sh — Bascule safe avec vérification
set -euo pipefail

# 1. Vérifier que Green est synchronisé (lag < 1KB)
LAG=$(psql -h green-db -U monitor -d myapp -t -c \
  "SELECT COALESCE(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn), -1)
   FROM pg_replication_slots WHERE slot_name = 'green_sub';")
[ "$LAG" -gt 1000 ] && echo "Lag trop élevé: ${LAG} bytes" && exit 1

# 2. Blue en read-only → attendre sync → migrer Green → basculer
psql -h blue-db -U admin -d myapp -c \
  "ALTER DATABASE myapp SET default_transaction_read_only = on;"
sleep 5
flyway -url=jdbc:postgresql://green-db:5432/myapp migrate

# 3. Basculer et vérifier
sed -i "s/blue-db/green-db/g" /etc/app/database.yml && systemctl reload app
sleep 10
HTTP=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health)
[ "$HTTP" -ne 200 ] && echo "Rollback!" && sed -i "s/green-db/blue-db/g" /etc/app/database.yml \
  && systemctl reload app && exit 1
echo "✅ Bascule réussie"

Checklist pré-migration indispensable avant toute exécution en production :

  • Migration testée en staging avec un volume de données réaliste
  • Backward compatibility vérifiée (ancienne app fonctionne avec le nouveau schéma)
  • Temps d’exécution estimé sur une copie de production
  • Plan de rollback documenté et testé
  • Backup frais réalisé juste avant
  • Monitoring des locks et de la latence activé

⚠️ Attention — Ne fais jamais une migration en prod le vendredi après-midi. Si quelque chose tourne mal, tu veux une équipe disponible pour réagir, pas un week-end de panique.

🔥 Cas réel — GitHub utilise gh-ost pour toutes ses migrations MySQL. L’outil crée une table shadow, copie les données par batch, applique les changements en temps réel via le binlog, puis fait un swap atomique. Zéro downtime sur des tables de milliards de lignes.

🧠 À retenir — Les migrations zero-downtime ne sont pas un luxe, c’est une nécessité en production continue. Le pattern expand-contract est ta fondation. Flyway pour la simplicité, Liquibase pour la flexibilité. Et pour les gros changements de schéma, le blue-green database avec réplication logique te donne un filet de sécurité complet. Teste toujours en staging, garde un rollback prêt, et monitore pendant l’exécution.

Articles liés