Aller au contenu
English

Migrations sûres

  • Leçon 11
  • 50 min
  • Module 4 : Mettre en production les changements d'IA et de base de données
Leçon 11 sur 15 · CI/CD, tests et mises en production sûresMigrations sûres

Ce que vous saurez faire

À chaque déploiement, l’ancienne et la nouvelle version fonctionnent en même temps, avec la même base de données. Cette leçon modifie la base de données pour que les deux continuent de fonctionner. Ajoutez d’abord, retirez plus tard. Exécutez chaque changement une seule fois, dans l’ordre, avant le démarrage de la nouvelle version.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Exécuté avec GitHub Actions (ubuntu-24.04, PostgreSQL 18.6 comme conteneur de service) dans Nextia-Intelligence/ticket-api-ci. Aussi exécuté avec Docker Desktop 4.81.0 sur macOS (Apple silicon) et postgres:18.6-trixie. Les étapes pour Windows et Linux n’ont pas été testées.

La version 1.2.0 renomme un champ. Chaque classification a un champ confidence (confiance), un nombre de 0 à 1. Ce nom laisse croire aux clients qu’il s’agit d’une probabilité. Ce n’est pas le cas : il mesure la force de correspondance des mots-clés. L’équipe veut le nom neutre score.

L’API est la partie facile. La base de données est plus difficile. La table classifications a une colonne confidence, et deux versions de l’application l’utilisent en même temps :

  • Pendant le déploiement, Container Apps démarre la nouvelle révision à côté de l’ancienne. Les deux répondent aux requêtes pendant un moment (Configuration opérationnelle du cours L’essentiel de Docker et du nuage pour les développeurs en IA).
  • Lors d’un déploiement canari (Mise en production progressive), les deux fonctionnent volontairement pendant des heures.
  • Après un retour en arrière, l’ancienne version fonctionne de nouveau avec la base de données modifiée par la nouvelle version.

La base de données doit donc fonctionner avec les deux versions en tout temps. Faites une prédiction : Amira peut modifier la table de deux façons. Laquelle empêche la version 1.1.0 de fonctionner?

Option A: rename the column
ALTER TABLE classifications RENAME COLUMN confidence TO score;
Option B: add a column next to the old one
ALTER TABLE classifications ADD COLUMN IF NOT EXISTS score real;

L’option A empêche immédiatement 1.1.0 de fonctionner. Son code écrit et lit confidence, mais cette colonne n’existe plus. L’erreur courante à la fin de cette leçon montre les erreurs réelles. L’option B ne brise rien. La version 1.1.0 ne connaît pas score, donc elle ignore la nouvelle colonne. La version 1.2.0 écrit dans les deux.

L’option B est la première moitié d’une méthode appelée expansion et contraction :

  1. Ajouter : ajoutez le nouvel élément à côté de l’ancien. L’ancienne et la nouvelle version fonctionnent toutes les deux.
  2. Passer au nouveau champ : la nouvelle version écrit dans les deux champs. Les clients passent de confidence à score à leur rythme. L’API marque confidence comme obsolète : il fonctionne encore, mais disparaîtra dans une version annoncée.
  3. Remplir les données existantes : remplissez la nouvelle colonne pour les anciennes lignes. Ce remplissage modifie les données, pas la structure : UPDATE … SET score = confidence WHERE score IS NULL.
  4. Retirer : retirez l’ancien élément dans une mise en production ultérieure, quand plus rien ne l’utilise. Ici, c’est la version 2.0, une version majeure, car retirer un champ rend les clients incompatibles.

Chaque étape correspond à une mise en production distincte. Entre deux étapes, vous pouvez arrêter, attendre plusieurs jours ou revenir en arrière.

Cinq étapes de gauche à droite. 1, avant : 1.1.0 en production, avec la colonne confidence. 2, ajout avec la migration 002 : ajouter score, qui peut être vide, avant le démarrage de 1.2.0. 3, déploiement : 1.1.0 et 1.2.x fonctionnent ensemble. Les étapes 2 et 3 constituent la mise en production 1.2.0, dans ce module. 4, plus tard, remplissage avec la migration 003 : score = confidence, après la disparition de 1.1.0. 5, plus tard, retrait dans 2.0 : retirer confidence, après la transition des clients. Sous les étapes, les versions qui fonctionnent. 1.1.0 : fonctionne aux étapes 1 à 4 (elle ignore la nouvelle colonne et écrit des lignes sans score), échoue à l'étape 5 parce que confidence a disparu. 1.2.x : échoue à l'étape 1 parce que la colonne score n'existe pas encore, fonctionne aux étapes 2 à 4 (elle doit lire les lignes sans score), échoue à l'étape 5 parce qu'elle écrit encore confidence. 2.0 : existe seulement à l'étape 5, où elle lit et écrit seulement score. Retour à 1.1.0 : possible aux étapes 2, 3 et 4, impossible à l'étape 5. Chaque étape permet à la version précédente de continuer à fonctionner. Jusqu'à l'étape 5, vous pouvez arrêter la mise en production ou revenir en arrière à n'importe quelle étape.
La séquence de mise en production de score, avec les versions qui fonctionnent à chaque étape. La mise en production 1.2.0 comprend les étapes 2 et 3. Les étapes 4 et 5 viennent plus tard, chacune dans sa propre mise en production.
Description textuelle du diagramme

Lisez le tableau par colonne. À chaque étape, au moins la version précédente et la version suivante fonctionnent. Le déploiement peut donc les faire fonctionner ensemble. La version 1.2.x échoue avant l’étape 2. La migration doit donc s’exécuter avant le démarrage de 1.2.0. La version 1.1.0 échoue seulement après l’étape 5. Le retrait attend donc qu’aucune version 1.1.0 ne fonctionne et qu’aucun client ne lise confidence. Remarquez la cellule de 1.2.x à l’étape 3. La version 1.1.0 écrit encore des lignes sans score, et 1.2.x doit aussi fonctionner avec ces lignes. Un retour à 1.1.0 reste possible jusqu’à l’étape 5, car chaque étape précédente fait seulement des ajouts.

Migrations : des fichiers numérotés, appliqués une seule fois

Section intitulée « Migrations : des fichiers numérotés, appliqués une seule fois »

Jusqu’ici, l’application créait sa propre table au démarrage (CREATE TABLE IF NOT EXISTS, présenté dans le cours sur Docker). Cela fonctionne pour une table qui ne change jamais. Cela ne permet pas d’ajouter une colonne à une table existante. Cela n’indique pas non plus les changements déjà appliqués à une base de données.

Une migration est un changement numéroté de la structure ou des données d’une base de données, dans un fichier. La version 1.2.0 en a deux :

migrations/001_create_classifications.sql
-- The table that C18 made at start-up. IF NOT EXISTS: a database from C18
-- already has it, and this migration then only records that it is there.
CREATE TABLE IF NOT EXISTS classifications (
id bigserial PRIMARY KEY,
request_id text NOT NULL,
category text NOT NULL,
priority smallint NOT NULL,
confidence real NOT NULL,
model_version text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
migrations/002_add_score.sql
-- Expand: add the new column next to the old one. It may be empty, because
-- version 1.1.0 of the app still writes rows without it during the rollout.
ALTER TABLE classifications ADD COLUMN IF NOT EXISTS score real;

001 décrit la table qui existe aujourd’hui. Une base de données du cours sur Docker l’a déjà. IF NOT EXISTS rend la migration sûre dans ce cas : elle enregistre la table sans rien changer. 002 n’a pas de NOT NULL. C’est volontaire : 1.1.0 insère des lignes sans score, et une colonne NOT NULL les refuserait.

L’exécuteur de migrations, ticket_api/migrate.py, les applique. Il garde la liste des migrations appliquées dans sa propre table, schema_migrations. Le cœur tient en 10 lignes :

ticket_api/migrate.py (the core)
def pending(conn: psycopg.Connection) -> tuple[list[Path], set[str]]:
conn.execute(CREATE_LOG)
applied = {row[0] for row in conn.execute("SELECT version FROM schema_migrations")}
files = sorted(MIGRATIONS.glob("*.sql"))
return [f for f in files if f.stem not in applied], applied
# in main():
for f in todo:
with conn.transaction():
conn.execute(f.read_text(encoding="utf-8"))
conn.execute("INSERT INTO schema_migrations (version) VALUES (%s)", (f.stem,))
print(f"applied {f.stem}")

Ces lignes contiennent trois règles. Les fichiers s’exécutent dans l’ordre de leurs noms. Le numéro au début détermine donc l’ordre. Un fichier dans schema_migrations ne s’exécute jamais de nouveau. Chaque fichier s’exécute dans sa propre transaction, avec la ligne qui l’enregistre : soit tout se réalise, soit rien. Le fichier complet se trouve dans ticket_api/migrate.py à c3c7135. Plusieurs équipes utilisent un outil comme Alembic ou Flyway pour cela. L’idée reste la même : des fichiers numérotés, une table des versions appliquées et une transaction par fichier.

Exemple détaillé : migrer une base de données du cours sur Docker

Section intitulée « Exemple détaillé : migrer une base de données du cours sur Docker »

Amira teste l’exécuteur sur une copie d’une base de données du cours sur Docker. Elle contient la table créée par l’application et une ligne de la version 1.1.0. Elle demande d’abord ce qui reste à appliquer :

Terminal (in the project, with DATABASE_URL set to the test database)
python -m ticket_api.migrate --status

La sortie (simulée) :

pending 001_create_classifications
pending 002_add_score

Elle applique ensuite les migrations, puis exécute de nouveau la même commande :

Terminal
python -m ticket_api.migrate
python -m ticket_api.migrate

La sortie (simulée) :

applied 001_create_classifications
applied 002_add_score
Database is up to date (2 migrations).
Database is up to date (2 migrations).

La deuxième exécution n’a rien appliqué. Un pipeline a besoin d’un exécuteur qui peut s’exécuter deux fois sans causer de problèmes. Chaque déploiement l’exécute, et la plupart n’ont rien à appliquer. La table a maintenant la nouvelle colonne, et l’ancienne ligne n’a pas de score :

id | request_id | model_version | confidence | score
----+--------------+---------------+------------+-------
1 | c18row000001 | keywords-1.0 | 0.9 |

SELECT version FROM schema_migrations énumère 001_create_classifications et 002_add_score, chacune avec l’heure de son application.

La nouvelle version a besoin de score. La migration doit donc se terminer avant le démarrage de cette version. Amira intègre cet ordre au déploiement, plutôt que d’en faire une étape qu’une personne doit se rappeler.

L’image contient ses migrations. Une ligne du Dockerfile les copie. L’image de 1.2.0 peut ainsi migrer sa propre base de données : COPY migrations ./migrations.

Compose les exécute d’abord. Un service migrate utilise la même image, lance l’exécuteur, puis se termine. Le service api démarre seulement après sa fin sans erreur :

deploy/local/compose.yaml (the new parts)
services:
api:
# ...
depends_on:
migrate:
condition: service_completed_successfully
migrate:
image: ${IMAGE}
platform: linux/amd64
command: ["python", "-m", "ticket_api.migrate"]
environment:
DATABASE_URL: postgresql://tickets:${POSTGRES_PASSWORD}@db:5432/tickets
depends_on:
db:
condition: service_healthy

Si une migration échoue, migrate se termine avec le code de sortie 1, et api ne démarre pas. L’ancienne version continue de fonctionner. Ajoutez le même service migrate à compose.yaml pour votre ordinateur. La ligne platform vient de Promouvoir en préproduction : l’image qui ne démarrait pas sur un Mac. Le dépôt de référence l’a ajoutée à migrate dans la demande de tirage no 6, après 1.2.0.

L’application cesse de créer la table. Dans main.py, l’appel à history.setup() disparaît. La table créée au démarrage et celle créée par les migrations ne peuvent pas toutes deux servir de référence.

La CI teste les migrations. La tâche de vérification reçoit un conteneur de service PostgreSQL 18.6. GitHub Actions démarre cette base de données à côté de la tâche, puis la supprime après celle-ci. Deux nouveaux tests exécutent les migrations deux fois et enregistrent une classification :

.github/workflows/ci.yml (the checks job, new lines)
services:
postgres:
image: postgres:18.6-trixie
env:
POSTGRES_USER: tickets
POSTGRES_PASSWORD: ci-only-not-secret
POSTGRES_DB: tickets
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U tickets -d tickets"
--health-interval 2s --health-timeout 3s --health-retries 15
# ...
- name: Unit, contract and database tests
env:
TEST_DATABASE_URL: postgresql://tickets:ci-only-not-secret@localhost:5432/tickets
run: python -m pytest --junitxml=reports/junit.xml

Le mot de passe n’est pas un secret : la base de données existe seulement pendant les quelques secondes de la tâche. Les tests s’ignorent quand TEST_DATABASE_URL n’est pas défini. Ainsi, pytest fonctionne encore sur un ordinateur sans base de données. Dans la demande de tirage no 5, de feature/score-field vers main, l’exécution 37667028262 a recueilli 30 tests. Ils ont tous réussi en 0,51 seconde.

L’API conserve les deux champs. models.py reçoit score et marque confidence comme obsolète dans le contrat. routes.py leur attribue la même valeur :

ticket_api/models.py (Classification, the two fields)
confidence: float = Field(
ge=0,
le=1,
description="Deprecated: use score. The same value. Removed in version 2.0.",
json_schema_extra={"deprecated": True},
)
score: float = Field(ge=0, le=1, description="How sure the classifier is, from 0 to 1.")

La version devient 1.2.0 : le nombre du milieu change, car les clients reçoivent un nouveau champ sans incompatibilité. Tous les fichiers de cette mise en production se trouvent dans le commit aa8b83e.

Exercice guidé : deux versions sur une base de données

Section intitulée « Exercice guidé : deux versions sur une base de données »

Voici la vérification de fin du module, à réaliser une fois avec les valeurs d’Amira. Utilisez le parcours local, même si vous avez Azure. Les applications infonuagiques de ce cours n’ont pas de base de données. /v1/history y répond donc 503 par conception. La migration s’exécute dans les tests de CI et dans les environnements locaux.

  1. Créez la branche : git switch -c feature/score-field (à partir de main).
  2. Ajoutez migrations/001_create_classifications.sql, migrations/002_add_score.sql et ticket_api/migrate.py. Faites les autres changements du commit aa8b83e : Dockerfile, les deux fichiers Compose, main.py, history.py, models.py, routes.py, contract/openapi.json, la version dans pyproject.toml, ci.yml et les tests.
  3. Exécutez python -m pytest -q. Attendez-vous à 28 tests réussis et 2 ignorés : les tests de base de données sont ignorés sans TEST_DATABASE_URL.
  4. Poussez la branche et ouvrez une demande de tirage : feature/score-field vers main. Attendez-vous à 30 tests dans le journal de CI et à une barrière franchie. Fusionnez la demande.
  5. Trouvez l’empreinte de la nouvelle image. Elle se trouve dans l’artefact build-info de l’exécution de CI sur main, comme dans Publier les preuves : build-info.json : empreinte, commit et exécution dans un seul fichier. Ensuite, étiquetez le commit de fusion v1.2.0 et poussez l’étiquette. Procédez comme pour v1.1.0 dans Promouvoir en préproduction : une version sans nouvelle construction. Le module 5 crée le correctif urgent à partir de cette étiquette.
  6. Promouvez l’image en production locale. Le service migrate s’exécute avant l’API.
  7. Démarrez la version 1.1.0 à côté, sur le même réseau et la même base de données. Utilisez votre empreinte de 1.1.0.
  8. Classez un billet avec chaque version. Regardez ensuite la table.
  9. Arrêtez l’ancienne version.

Les commandes des étapes 6 à 9, avec les empreintes du dépôt de référence (utilisez les vôtres) :

Terminal (in the project, on main)
NEW=ghcr.io/usmanahmed99/ticket-api-ci@sha256:bd2316b56067c738199d6203a23845c97fb7b85b550379411eddd7cc6a2ddab8
OLD=ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a
scripts/promote-local.sh production "$NEW"
docker run -d --name ticket-api-old --platform linux/amd64 \
--network ticket-api-production_default -p 127.0.0.1:8083:8000 -e API_KEY=old-key \
-e DATABASE_URL="postgresql://tickets:$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2)@db:5432/tickets" \
"$OLD"

La sortie de la promotion (simulée, abrégée) :

Container ticket-api-production-migrate-1 Exited
Container ticket-api-production-db-1 Healthy
Container ticket-api-production-api-1 Healthy
ok health (200)
ok ready (200)
ok classify with the key (200)
ok classify without a key (401)
production now runs ghcr.io/usmanahmed99/ticket-api-ci@sha256:bd2316b5...

docker logs ticket-api-production-migrate-1 montre les trois lignes de l’exécuteur, comme dans l’exemple détaillé. Dans votre base de données du module 3, 001 trouve la table créée par 1.1.0 et l’enregistre seulement. Attendez quelques secondes que l’ancienne version démarre (elle fonctionne par émulation sur Apple silicon). Envoyez ensuite le même billet aux deux :

Terminal
TICKET='{"subject": "Parcel never came", "body": "The courier lost it."}'
curl -s -X POST http://127.0.0.1:8082/v1/classify -H "X-API-Key: $(cat secrets/production_api_key.txt)" \
-H "Content-Type: application/json" -d "$TICKET"; echo
curl -s -X POST http://127.0.0.1:8083/v1/classify -H "X-API-Key: old-key" \
-H "Content-Type: application/json" -d "$TICKET"; echo
docker exec ticket-api-production-db-1 psql -U tickets -d tickets \
-c "SELECT id, request_id, model_version, confidence, score FROM classifications ORDER BY id"

La sortie (simulée) :

{"category":"shipping","priority":1,"confidence":0.9,"score":0.9,"model_version":"keywords-1.0"}
{"category":"shipping","priority":1,"confidence":0.9,"model_version":"keywords-1.0"}
id | request_id | model_version | confidence | score
----+--------------+---------------+------------+-------
1 | e3357fb137f9 | keywords-1.0 | 0.7 | 0.7
2 | 169da47b5cdb | keywords-1.0 | 0.9 | 0.9
3 | a533224672fb | keywords-1.0 | 0.9 |
(3 rows)

Les deux versions classent le billet et enregistrent leur résultat dans la même table. La version 1.2.0 répond avec score; la version 1.1.0 répond sans ce champ, comme avant. La ligne 1 correspond à la requête du test de bon fonctionnement. La ligne 2 vient de 1.2.0. La ligne 3 vient de 1.1.0, donc son score est vide. Dans votre table, les anciennes lignes de 1.1.0 ont aussi un score vide.

Supprimez ensuite l’ancienne version avec docker rm -f ticket-api-old. La production locale continue de fonctionner avec 1.2.0.

Afficher le résultat attendu

pytest -q sur votre ordinateur : 28 passed, 2 skipped (28 réussis, 2 ignorés). Dans la CI : collected 30 items (30 éléments recueillis) et 30 passed (30 réussis), puis Gate passed. (barrière franchie). La promotion affiche les quatre lignes ok (réussite). Les deux réponses de classification diffèrent seulement par score. La table a une ligne par classification. Chaque ligne de 1.2.0 a un score égal à confidence, et chaque ligne de 1.1.0 a un score vide. Si l’ancien conteneur ne répond pas, attendez 10 secondes et envoyez la requête de nouveau. S’il ne répond toujours pas après une minute, lisez docker logs ticket-api-old.

Amira vérifie la classification avec les deux versions et regarde la table. Les deux versions fonctionnent. Les lignes avec un score vide sont celles que 1.1.0 écrit pendant un déploiement. Le schéma indiquait que 1.2.x devait fonctionner avec elles. Le module 5 revient sur cette table.

À vous de jouer : la nouvelle version avant sa migration

Section intitulée « À vous de jouer : la nouvelle version avant sa migration »

Changez une condition : démarrez la version 1.2.0 sur une base de données vide, sans exécuter les migrations. Utilisez une base de données temporaire, pas votre production locale. Faites d’abord une prédiction : /ready indique-t-il que l’application est prête? Que répond POST /v1/classify?

Afficher les commandes et la réponse
Terminal (throwaway objects)
docker network create try-order
docker run -d --name try-db --network try-order -e POSTGRES_USER=tickets -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=tickets postgres:18.6-trixie
docker run -d --name try-new --platform linux/amd64 --network try-order -p 127.0.0.1:8085:8000 \
-e API_KEY=k -e DATABASE_URL=postgresql://tickets:pw@try-db:5432/tickets "$NEW"
curl -s http://127.0.0.1:8085/ready; echo
curl -s -X POST http://127.0.0.1:8085/v1/classify -H "X-API-Key: k" -H "Content-Type: application/json" -d '{"subject": "Refund"}'; echo
docker logs try-new 2>&1 | grep UndefinedTable

La sortie (simulée) :

{"status":"ready"}
{"error":{"code":"internal_error","message":"Something went wrong on the server.","request_id":"dbd7955e7ed6","fields":[]}}
psycopg.errors.UndefinedTable: relation "classifications" does not exist

/ready indique que l’application est prête, car il vérifie seulement que la base de données répond. Chaque classification échoue avec 500. Une plateforme enverrait du trafic à cette version. Exécutez maintenant la migration avec la même image, puis réessayez :

Terminal
docker run --rm --platform linux/amd64 --network try-order \
-e DATABASE_URL=postgresql://tickets:pw@try-db:5432/tickets "$NEW" python -m ticket_api.migrate

Les trois lignes de l’exécuteur apparaissent. La requête de classification répond ensuite 200 avec "score":0.7. C’est pourquoi Compose démarre api seulement après la fin de migrate sans erreur. Supprimez les objets : docker rm -f try-new try-db, puis docker network rm try-order.

Erreur courante

L'étape de retrait s'exécute pendant que l'ancienne version fonctionne encore

Symptôme (simulé) : une base de données est modifiée en une seule étape. La colonne confidence est renommée score, alors que la version 1.1.0 utilise encore cette base. Chaque requête à la version 1.1.0 échoue alors :

POST /v1/classify 500 {"error":{"code":"internal_error",...}}
GET /v1/history 500 {"error":{"code":"internal_error",...}}
psycopg.errors.UndefinedColumn: column "confidence" of relation "classifications" does not exist
psycopg.errors.UndefinedColumn: column "confidence" does not exist

Les vérifications de santé et de disponibilité réussissent encore, car elles n’utilisent pas la colonne.

Cause : l’ajout et le retrait ont eu lieu en même temps. La version 1.1.0 insère des valeurs dans confidence et les sélectionne. Après le renommage, aucune version antérieure à 1.2.0 ne peut utiliser cette base de données. Un retour à la version 1.1.0 n’aiderait pas non plus : l’ancien code se retrouve devant la nouvelle table.

Solution :

  1. Si cela s’est produit, redonnez son ancien nom à la colonne (ALTER TABLE classifications RENAME COLUMN score TO confidence). La version en cours fonctionnera de nouveau.
  2. Faites le changement par étapes : ajoutez score (migration 002), déployez la version qui écrit dans les deux colonnes, puis remplissez les données manquantes.
  3. Supprimez confidence seulement dans une version majeure ultérieure, quand aucune version en cours ni aucun client ne l’utilise.
  4. Testez aussi chaque migration avec la version précédente. La vérification de coexistence de l’exercice guidé le fait.

Pour un vrai service d’IA : les résultats des modèles se trouvent souvent dans des tables comme celle-ci : scores, étiquettes, version du modèle. Un nouveau modèle demande souvent une nouvelle colonne ou un nouveau sens. Chaque déploiement et chaque retour en arrière font coexister des versions pendant un certain temps. La table doit donc servir les deux modèles. Un renommage qui semble propre dans une demande de tirage (pull request) peut arrêter la production pour tous les clients.

Vérifiez votre compréhension

La migration 002 ajoute score sans NOT NULL. Une personne qui révise le code demande NOT NULL avec une valeur par défaut de 0, pour que la colonne ne soit jamais vide. Qu'est-ce qui échoue ou pose problème?

Afficher la réponse

Avec NOT NULL et sans valeur par défaut, la version 1.1.0 ne pourrait insérer aucune ligne : elle n’envoie pas score. Avec une valeur par défaut de 0, les insertions fonctionnent. Mais chaque ligne de la version 1.1.0 indique alors un score de 0, ce qui est faux. Ces billets de soutien avaient une vraie valeur dans confidence. Un score vide signifie « pas encore connu ». Le remplissage des données manquantes (003) le complète ensuite à partir de confidence. Pendant un déploiement, la colonne doit accepter ce que l’ancienne version écrit.

Vérifiez votre compréhension

La version 1.2.0 échoue en production après l'exécution de la migration 002. Amira devrait-elle restaurer la sauvegarde de la base de données faite avant la mise en production?

Afficher la réponse

Non. 002 a seulement ajouté une colonne vide, et la version 1.1.0 l’ignore. Elle déploie l’image de la version 1.1.0 par son empreinte et laisse la colonne. Une restauration remettrait la base de données dans l’état qu’elle avait au moment de la sauvegarde. Elle supprimerait chaque classification effectuée depuis : de vraies données perdues pour annuler un changement qui n’a causé aucun dommage. Gardez des sauvegardes pour les migrations qui modifient ou suppriment des données. Restaurez-les seulement en cas de perte de données.

Vérifiez votre compréhension

Prédisez : Amira exécute python -m ticket_api.migrate sur une base de données où 001 est appliquée. La migration 002 contient une erreur dans sa deuxième instruction. La première instruction de 002 est-elle conservée?

Afficher la réponse

Non. Chaque migration s’exécute dans une seule transaction, avec sa ligne dans schema_migrations. Quand la deuxième instruction échoue, toute la migration 002 est annulée. La colonne créée par la première instruction disparaît, et --status (état) indique encore pending 002_add_score (migration 002 en attente). Dans le test de l’auteur, l’exécuteur a affiché applied 001_create_classifications (migration 001 appliquée), puis une SyntaxError (erreur de syntaxe). Il s’est terminé avec le code de sortie 1; 001 est restée appliquée. Après une correction, l’exécution suivante applique 002 depuis le début.

Récapitulatif

  • Les anciennes et nouvelles versions partagent la base de données pendant chaque déploiement, déploiement canari et retour en arrière. Chaque migration doit permettre à la version précédente de continuer à fonctionner.
  • Ajout et retrait : ajoutez score à côté de confidence maintenant (002), écrivez dans les deux colonnes, remplissez les données manquantes plus tard (003), puis supprimez confidence dans la version 2.0.
  • Les migrations sont des fichiers numérotés, appliqués une seule fois chacun, dans l’ordre, avec une transaction par fichier. Elles sont enregistrées dans schema_migrations. L’exécuteur peut fonctionner à chaque déploiement.
  • Migrez d’abord : l’image contient ses migrations, et Compose démarre api seulement après que migrate s’est terminé sans erreur. Le pipeline CI exécute les migrations sur PostgreSQL 18.6.
  • Une colonne ajoutée ne nécessite aucune restauration pour revenir en arrière. Faites une sauvegarde avant une migration qui modifie ou supprime des données.
  • Les versions 1.1.0 et 1.2.0 ont effectué des classifications sur la même base de données. Les lignes de la version 1.1.0 ont un score vide.

Leçon suivante : Mise en production progressive