Aller au contenu
English

Stratégie de retour en arrière

  • Leçon 13
  • 45 min
  • Module 5 : Rétablir et maintenir le processus de mise en production
Leçon 13 sur 15 · CI/CD, tests et mises en production sûresStratégie de retour en arrière

Ce que vous saurez faire

« Peut-on revenir en arrière? » a trois réponses différentes. Le code, le modèle et les données reviennent en arrière de façons différentes et à des vitesses différentes. L’un des trois ne revient jamais en arrière tout seul. Choisissez la bonne méthode avant d’en avoir besoin.

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

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Exécuté avec GitHub Actions (exécuteurs ubuntu-24.04) dans le dépôt public Nextia-Intelligence/ticket-api-ci, Docker Desktop 4.81.0 sur macOS (Apple silicon), et Azure CLI 2.88.0 dans canadacentral. Les étapes Windows et Linux ne sont pas testées.

Le 7 octobre 2026 à 18 h 41, Amira a trouvé un bogue dans la version 1.2.0. GET /v1/history répondait 500 (la prochaine leçon le montre). Le responsable du soutien a posé la question habituelle : « Peut-on revenir en arrière? »

Regardez ce qui fonctionnait en production à ce moment-là. Une réponse de POST /v1/classify montre trois parties distinctes :

One answer from production, 1.2.x with the flag on
{"category":"shipping","priority":1,"confidence":0.9,"score":0.9,"model_version":"keywords-1.1"}
Partie Où elle se trouve Ce qu’elle a fourni à cette réponse
L’application (le code) L’image, désignée par son condensat Le champ score existe seulement dans 1.2.x
Le modèle Le paramètre CLASSIFIER_VERSION=1.1 de l’application keywords-1.1, shipping (expédition) et la priorité 1
Les données Les lignes dans la base de données Chaque classification est enregistrée, avec ou sans score

Un retour en arrière ramène une partie à un état antérieur dont le bon fonctionnement était connu. Chaque partie a sa propre méthode :

  • Retour en arrière de l’application : déployez l’image précédente, par son condensat. Rien n’est construit.
  • Retour en arrière du modèle : changez seulement l’indicateur. L’image reste la même.
  • Données : un déploiement ne change jamais les lignes déjà écrites. Pour les corriger, vous faites une réparation en avant : une nouvelle migration remet les données dans le bon état. Une restauration (charger une sauvegarde de la base de données) est le dernier recours. Elle fait perdre toutes les lignes écrites depuis la sauvegarde.
Qu'est-ce qui a mal tourné? Trois colonnes. Le nouveau code est incorrect : ramenez l'application en arrière, déployez le condensat précédent avec le flux de travail Deploy; en réel, 120 s jusqu'à la production, en passant par la préproduction et l'approbation; cela retire aussi score et keywords-1.1, et la base de données reste inchangée. Le nouveau modèle est incorrect : ramenez seulement l'indicateur en arrière, CLASSIFIER_VERSION=1.0 sur la même image; en réel, 43 s; cela conserve l'image, score et le correctif. Des lignes ont été mal écrites : réparez les données en avant avec une nouvelle migration, 003_backfill_score, quand il ne reste aucune version 1.1.0; une migration, 1 score vide, puis 0; un retour en arrière ne change jamais les lignes, restaurez une sauvegarde seulement si des données sont perdues. Sous les trois : le retour en arrière fonctionne seulement parce que la migration 002 commençait par une expansion.
La décision de retour en arrière : ce qui a mal tourné détermine ce qui revient en arrière. Les durées viennent de l'exercice du 7 octobre 2026 dans le dépôt de référence.
Description textuelle du diagramme

Commencez par la question « Qu’est-ce qui a mal tourné? ». Si le nouveau code est incorrect, ramenez l’application en arrière : déployez le condensat précédent avec le flux de travail Deploy. Dans l’exercice, cela a pris 120 secondes jusqu’à la production, en passant par la préproduction et l’approbation. Cela retire aussi le nouveau champ score et le nouveau modèle, sans changer la base de données. Si seul le nouveau modèle est incorrect, ramenez seulement l’indicateur en arrière : définissez CLASSIFIER_VERSION=1.0 sur la même image. Cela a pris 43 secondes et conserve tout le reste. Si des lignes ont été mal écrites, rien ne les ramène en arrière. Écrivez une nouvelle migration qui les corrige, comme 003_backfill_score. Restaurez une sauvegarde seulement si des données sont perdues. Tout cela fonctionne seulement parce que la migration 002 a ajouté une colonne que la version 1.1.0 peut ignorer.

Amira avait donc trois choix à 18 h 41. Un retour à 1.1.0 arrêterait les réponses 500, car 1.1.0 ne lit pas score. Mais les clients perdraient score, promis dans les notes de mise en production de 1.2.0. Le soutien perdrait keywords-1.1. Un retour en arrière de l’indicateur n’aiderait pas : le bogue était dans le code, pas dans le modèle. Elle a choisi un correctif urgent (leçon 2) : une petite nouvelle version qui corrige seulement le bogue. Le bogue touchait un seul point de terminaison. Tout le reste demeurait disponible en production.

Puis, avec 1.2.1 en production, l’équipe a fait un exercice de retour en arrière. Ce retour planifié mesure la durée et vérifie ce qui change. Ainsi, le vrai retour en arrière n’est pas le premier. Le reste de cette leçon présente cet exercice.

Revenir à une ancienne version retire tout ce qui est venu après. Vérifiez-le avant de commencer, pas après. Pour l’API de billets de soutien :

1.1.0 1.2.0 1.2.1
score dans la réponse non oui oui
Reconnaît CLASSIFIER_VERSION non : toujours keywords-1.0 oui oui
Exécute les migrations non : crée la table au démarrage oui, 001 et 002 oui, 001 et 002
GET /v1/history avec une ligne de 1.1.0 200 500 (le bogue) 200

Deux questions déterminent si un retour en arrière est sûr :

  1. L’ancienne version fonctionne-t-elle avec la base de données dans son état actuel? Ici, oui : la migration 002 a seulement ajouté une colonne, et 1.1.0 l’ignore. Voilà pourquoi Migrations sûres : ajouter maintenant, retirer plus tard faisait commencer chaque migration par une expansion. Si 002 avait renommé confidence, 1.1.0 échouerait à chaque requête. La restauration serait alors le seul moyen de revenir en arrière.
  2. Les clients utilisent-ils déjà quelque chose de nouveau? Un client qui lit score ne fonctionne plus quand le champ disparaît. Prévenez-les avant, ou choisissez un retour en arrière plus limité.

Exemple détaillé : le retour en arrière de l’application (réel)

Section intitulée « Exemple détaillé : le retour en arrière de l’application (réel) »

Trouvez le condensat précédent. Un retour en arrière déploie une image que la CI a déjà construite et testée. Ne reconstruisez jamais l’ancien code : une nouvelle construction produit un nouvel artefact, avec de nouveaux paquets, que personne n’a testé. Le condensat se trouve sur la page de mise en production (la dernière ligne de chaque version), ou dans le registre :

Terminal
docker buildx imagetools inspect ghcr.io/usmanahmed99/ticket-api-ci:1.1.0

La sortie (abrégée) :

Name: ghcr.io/usmanahmed99/ticket-api-ci:1.1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a

Utilisez votre propre nom de propriétaire au lieu de usmanahmed99. L’étiquette 1.1.0 peut changer si quelqu’un l’attribue de nouveau. Le condensat ne peut pas changer : il désigne la version que vous avez testée.

Démarrez manuellement le flux de travail Deploy. Dans votre dépôt, ouvrez Actions → Deploy → Run workflow (actions → déployer → exécuter le flux de travail). Gardez la branche main. Collez le condensat dans digest (condensat), et écrivez la raison dans reason (raison). Sélectionnez ensuite Run workflow. Avec la CLI GitHub, faites la même chose :

Terminal (live)
gh workflow run deploy.yml \
-f digest=sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a \
-f reason="Rollback drill: production back to 1.1.0"

Un retour en arrière suit le même chemin que chaque mise en production. D’abord la préproduction, avec le test de bon fonctionnement, puis l’approbation, puis la production. C’est plus lent qu’un changement direct dans Azure. Mais ce chemin a été testé, il laisse une trace, et une ancienne image défectueuse s’arrête en préproduction. Quand la tâche de préproduction se termine, la tâche de production attend. La personne responsable sélectionne Review deployments (examiner les déploiements), puis production, écrit un commentaire et approuve. Dans l’exercice, le commentaire était « Rollback drill : production back to 1.1.0 » (exercice de retour en arrière : production ramenée à 1.1.0).

Le résultat : exécution 37671631615. Les tâches (réel) :

Find the image 19:03:55 → 19:04:00 success
staging / Deploy to staging 19:04:04 → 19:04:56 success
production / Deploy to production 19:05:10 → 19:06:01 success

L’exécution a commencé à 19 h 03 min 50 s. La production exécutait 1.1.0 environ 120 secondes plus tard. Les 14 secondes entre les deux tâches correspondaient à l’approbation. Les tests de bon fonctionnement ont réussi dans les deux environnements, y compris ok history (503) (historique réussi, code 503). Le nuage n’a pas de base de données.

Vérifiez ce qui a changé. Amira a envoyé un billet à la production. La sortie (réel) :

{"category":"other","priority":3,"confidence":0.2,"model_version":"keywords-1.0"}

Et les paramètres et l’image de l’application :

Terminal (live)
az containerapp show -g rg-ticket-api-ci -n ticket-api-production \
--query "{image: properties.template.containers[0].image, env: properties.template.containers[0].env[].name}"

La sortie (réel) :

{
"env": [
"API_KEY",
"REQUIRE_API_KEY",
"SHOW_DOCS",
"CLASSIFIER_VERSION"
],
"image": "ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a"
}

Lisez les deux sorties ensemble. CLASSIFIER_VERSION est toujours défini (à 1.1), mais la réponse indique keywords-1.0, et score a disparu. La version 1.1.0 ne reconnaît pas l’indicateur, alors elle l’ignore. Le retour en arrière de l’application a retiré trois choses à la fois : le correctif, le champ d’API et le modèle.

Revenez à la version plus récente. Un exercice se termine dans l’état où il a commencé. Amira a déployé le condensat de 1.2.1 de la même façon : exécution 37671961512, 116 secondes. La sortie (réel) :

{"category":"shipping","priority":1,"confidence":0.9,"score":0.9,"model_version":"keywords-1.1"}

L’indicateur a immédiatement fonctionné de nouveau, car il était toujours défini sur l’application.

Exemple détaillé : le retour en arrière du modèle seulement (réel)

Section intitulée « Exemple détaillé : le retour en arrière du modèle seulement (réel) »

Supposez maintenant que keywords-1.1 donne de mauvais résultats pour un type de billet, et que le code fonctionne bien. Changez seulement l’indicateur :

Terminal (live)
az containerapp update -g rg-ticket-api-ci -n ticket-api-production \
--set-env-vars CLASSIFIER_VERSION=1.0 --query properties.latestRevisionName -o tsv

La sortie (réel) :

ticket-api-production--0000010

Un paramètre modifié crée une nouvelle révision avec la même image. Après 43 secondes, la réponse était (réel) :

{"category":"other","priority":3,"confidence":0.2,"score":0.2,"model_version":"keywords-1.0"}

score reste, le correctif reste, seul le modèle revient en arrière. Amira l’a réactivé avec --set-env-vars CLASSIFIER_VERSION=1.1, et la réponse indiquait de nouveau keywords-1.1.

Ce changement ne passe pas par le pipeline : il modifie directement la production. Le déploiement canari dans Mise en production progressive : exemple détaillé : le canari en ligne faisait la même chose. L’approbation de production ne s’applique donc pas. En cas d’urgence, c’est acceptable pour un indicateur. L’indicateur existe pour cela, et la barrière d’évaluation a testé les deux valeurs. Notez le changement (qui, quand, pourquoi), car le pipeline n’en garde aucune trace. En équipe, placez la commande dans une tâche de flux de travail avec environment: production, comme pour le déploiement canari. Une personne l’approuve alors, et l’exécution en garde une trace. La valeur de --set-env-vars reste aussi sur l’application pour les déploiements suivants.

Le même exercice sur votre ordinateur. L’environnement de production local fonctionne sur http://127.0.0.1:8082, avec sa propre base de données. Exécutez les commandes dans le dossier du projet.

Commencez avec 1.2.1 et l’indicateur activé. Dans deploy/local/production.env, définissez CLASSIFIER_VERSION=1.1. Déployez ensuite l’image 1.2.1 :

Terminal (simulated)
scripts/promote-local.sh production ghcr.io/usmanahmed99/ticket-api-ci@sha256:84937455beb63744755622d539748c9adb1772f379c9eb7e4f961c5b7f1bc924
curl -s -X POST http://127.0.0.1:8082/v1/classify -H 'Content-Type: application/json' \
-H "X-API-Key: $(cat secrets/production_api_key.txt)" -d '{"subject":"Package never arrived"}'

La sortie (simulation, abrégée) :

ok health (200)
ok ready (200)
ok classify with the key (200)
ok classify without a key (401)
ok history (200)
production now runs ghcr.io/usmanahmed99/ticket-api-ci@sha256:84937455beb63744755622d539748c9adb1772f379c9eb7e4f961c5b7f1bc924
{"category":"shipping","priority":1,"confidence":0.9,"score":0.9,"model_version":"keywords-1.1"}

Retour en arrière du modèle seulement. Dans deploy/local/production.env, définissez CLASSIFIER_VERSION=1.0. Exécutez la même commande promote-local.sh avec le même condensat. Compose détecte un paramètre modifié et recrée le conteneur api avec la même image. La sortie (simulation, abrégée) :

Container ticket-api-production-api-1 Recreate
Container ticket-api-production-api-1 Recreated
ok health (200)
...
{"category":"other","priority":3,"confidence":0.2,"score":0.2,"model_version":"keywords-1.0"}

Cela a pris 8 secondes. Les réponses sont les mêmes que celles du parcours réel. Remettez l’indicateur à 1.1, et exécutez de nouveau la commande.

Retour en arrière de l’application. L’environnement local ne peut pas utiliser promote-local.sh pour ce retour en arrière. L’erreur courante à la fin de cette leçon montre pourquoi. Démarrez seulement le service api avec l’ancienne image, sans toucher à la base de données ni aux migrations :

Terminal (simulated)
export IMAGE=ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a
docker compose -f deploy/local/compose.yaml --env-file deploy/local/production.env --env-file .env \
up -d --wait --no-deps api
scripts/smoke.sh http://127.0.0.1:8082 "$(cat secrets/production_api_key.txt)"

--no-deps démarre seulement le service que vous nommez. La sortie (simulation) :

Container ticket-api-production-db-1 Running
Container ticket-api-production-api-1 Starting
Container ticket-api-production-api-1 Started
Container ticket-api-production-api-1 Waiting
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)
ok history (200)

Cela a pris 6 secondes. Une classification n’a maintenant aucun score et indique keywords-1.0, comme dans l’exercice réel. Revenez à la version plus récente avec la commande promote-local.sh et le condensat de 1.2.1.

Regardez la base de données après le retour en arrière local. La version 1.1.0 a enregistré deux lignes pendant son exécution :

Terminal (simulated)
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 (simulation) :

id | request_id | model_version | confidence | score
----+--------------+---------------+------------+-------
1 | 4d10436731e8 | keywords-1.0 | 0.7 | 0.7
2 | a49f44a4effc | keywords-1.0 | 0.2 | 0.2
3 | ef19e98fb391 | keywords-1.0 | 0.2 |
4 | 119fbf2734a9 | keywords-1.0 | 0.7 | 0.7
5 | d7dc11adb8bf | keywords-1.0 | 0.2 | 0.2
6 | cd2ce474a397 | keywords-1.0 | 0.7 |
7 | a8276a5e60ee | keywords-1.0 | 0.2 |
(7 rows)

Les lignes 6 et 7 viennent de 1.1.0 pendant le retour en arrière. La ligne 3 venait déjà de 1.1.0, pendant le déploiement de 1.2.0. Quand la production est revenue à 1.2.1, ces lignes sont restées vides. Le retour en arrière n’a pas retiré la colonne score, et le retour à la version récente ne l’a pas remplie. C’est aussi vrai dans l’autre sens. Les lignes écrites par 1.2.x gardent leur score après un retour à 1.1.0. Un retour en arrière change le code exécuté. Il ne change jamais les données déjà écrites.

La version 1.2.1 lit ces lignes correctement. Elle utilise confidence quand score est vide : c’est le correctif urgent de la leçon 2. Mais d’autres lecteurs de la table, comme un rapport, verraient des valeurs vides. Corrigez les données en avant, avec une nouvelle migration :

migrations/003_backfill_score.sql
-- Forward repair: fill score for the rows that version 1.1.0 wrote during the
-- rollout of 1.2.0. Run it after no 1.1.0 instance is left, so no new empty
-- rows appear. It changes no structure, so every version keeps working.
UPDATE classifications SET score = confidence WHERE score IS NULL;

Une migration qui remplit une nouvelle colonne à partir d’anciennes données est un remplissage rétroactif (backfill). Trois règles rendent celle-ci sûre :

  • Exécutez-la quand il ne reste aucune version 1.1.0. Sinon, 1.1.0 écrit de nouvelles lignes vides après le remplissage rétroactif.
  • Elle ne change aucune structure. Les versions 1.1.0, 1.2.0 et 1.2.1 continuent toutes de fonctionner. Elle ne bloque donc pas un retour en arrière ultérieur.
  • Faites d’abord une sauvegarde, car elle change des lignes. Un ADD COLUMN (ajouter une colonne, 002) peut rester sans danger après un retour en arrière. Un UPDATE (mettre à jour) change des données que vous ne pouvez pas récupérer sans copie.

Dans le dépôt de référence, 003 est arrivé dans la demande de tirage nº 8 (feature/backfill-score vers main). Sa construction est l’image du commit (enregistrement des changements) 30e0c7e. L’exécution locale, dans l’ordre :

Terminal (simulated)
docker rm -f ticket-api-old
docker exec ticket-api-production-db-1 psql -U tickets -d tickets -At \
-c "SELECT count(*) FROM classifications WHERE score IS NULL"
docker exec ticket-api-production-db-1 pg_dump -U tickets -d tickets -t classifications --data-only -f /tmp/before-003.sql
scripts/promote-local.sh production ghcr.io/usmanahmed99/ticket-api-ci@sha256:1ec838b17cdeefb95620a308b99f31799f4fa10c5ddd9d46a17a2506a7e10947
docker logs ticket-api-production-migrate-1
docker exec ticket-api-production-db-1 psql -U tickets -d tickets -At \
-c "SELECT count(*) FROM classifications WHERE score IS NULL"

ticket-api-old est le conteneur 1.1.0 du test du bogue dans la prochaine leçon. Si vous ne l’avez pas démarré, docker rm -f indique « No such container » (aucun conteneur de ce nom). C’est normal. La sauvegarde va dans le conteneur de base de données. Elle est donc supprimée avec l’environnement. Pour une vraie base de données, gardez-la ailleurs. La sortie (simulation, abrégée) :

3
ok health (200)
...
ok history (200)
production now runs ghcr.io/usmanahmed99/ticket-api-ci@sha256:1ec838b17cdeefb95620a308b99f31799f4fa10c5ddd9d46a17a2506a7e10947
applied 003_backfill_score
Database is up to date (3 migrations).
0

Trois lignes vides avant, aucune après. Le service migrate a exécuté 003 avant le démarrage du nouveau api, comme pour chaque mise en production. Dans l’exécution de référence (avec une ligne vide), le résultat était le même : 1, puis 0. Exécutez de nouveau les migrations, et seule la dernière ligne apparaît : Database is up to date (3 migrations). (la base de données est à jour, 3 migrations).

Quand faut-il restaurer? Seulement quand des données sont perdues ou détruites. Par exemple, après une migration qui a supprimé une colonne par erreur. Une restauration charge la sauvegarde et fait perdre toutes les lignes écrites depuis la sauvegarde. Indiquez combien avant de commencer. Ici, rien n’a été perdu : chaque score vide peut être calculé à partir de confidence. Une réparation en avant est donc le bon choix, et aucune ligne n’est perdue.

Exercice guidé : un exercice de retour en arrière sur votre ordinateur

Section intitulée « Exercice guidé : un exercice de retour en arrière sur votre ordinateur »
  1. Notez l’état initial : exécutez scripts/promote-local.sh production avec l’empreinte de la version 1.2.1 et CLASSIFIER_VERSION=1.1. Classez « Package never arrived » (le colis n’est jamais arrivé). Notez la réponse.
  2. Faites une prédiction : après un retour en arrière du drapeau seulement, quels champs changent dans la réponse?
  3. Définissez CLASSIFIER_VERSION=1.0 dans deploy/local/production.env. Exécutez de nouveau promote-local.sh avec la même empreinte.
  4. Classez le même billet de soutien. Comparez le résultat à votre prédiction. Puis remettez le drapeau à 1.1 et déployez de nouveau.
  5. Faites une prédiction : après un retour de l’application à la version 1.1.0, quels champs changent? Le drapeau fonctionne-t-il encore?
  6. Revenez à l’ancienne application avec up -d --wait --no-deps api et l’empreinte de la version 1.1.0. Exécutez scripts/smoke.sh.
  7. Classez le billet deux fois. Puis comptez les lignes dont le champ score est vide.
  8. Revenez à la version plus récente : exécutez promote-local.sh avec l’empreinte de la version 1.2.1. Comptez de nouveau les lignes vides.
  9. Faites une sauvegarde avec pg_dump. Déployez l’image contenant 003. Comptez les lignes vides une troisième fois.
Afficher les résultats attendus
  1. "score":0.9,"model_version":"keywords-1.1", shipping (expédition), priorité 1.
  2. à 4. Seul le modèle change : "score":0.2,"model_version":"keywords-1.0", other (autre). score reste présent, car l’image est la même.
  3. à 7. score disparaît et la réponse indique keywords-1.0, même si le drapeau vaut 1.1 : la version 1.1.0 ne connaît pas ce drapeau. Les cinq vérifications de fonctionnement réussissent (l’historique de la version 1.1.0 fonctionne). Le nombre de lignes vides augmente de 3 : vos deux billets et celui de la vérification de fonctionnement.
  4. Le nombre ne change pas. Revenir à la version plus récente ne remplit pas les lignes.
  5. applied 003_backfill_score (migration 003_backfill_score appliquée), Database is up to date (3 migrations). (la base de données est à jour, avec 3 migrations), puis 0.

Ajoutez la réparation vers l’avant à votre dépôt avec une demande de tirage (pull request) de feature/backfill-score vers main : le fichier migrations/003_backfill_score.sql. Avant de pousser vos changements, prédisez quelle vérification CI échouera et pourquoi. (Dans le dépôt de référence, c’était la demande de tirage no 8, après le correctif urgent de la leçon 2. Vous pouvez le faire maintenant ou après la leçon 2.)

Afficher la réponse

Le test de base de données test_migrations_apply_once échoue, car il attend la ligne Database is up to date (2 migrations). (la base de données est à jour, avec 2 migrations). Il y en a maintenant 3. Dans tests/test_database.py, remplacez la ligne attendue par (3 migrations) : une nouvelle migration modifie volontairement ce test. Les autres tests réussissent : 003 ne change aucune structure. Dans la description de la demande de tirage, précisez que 003 s’exécute seulement quand aucune instance 1.1.0 ne reste. Après la fusion, le flux de travail Deploy (déployer) déploie la nouvelle construction, et son étape migrate (migrer) applique 003. Le parcours réel n’a pas de base de données, donc 003 n’y fait rien. Comparez avec la demande de tirage no 8, qui ajoute aussi la section Rollback (retour en arrière) de docs/release-process.md.

Erreur courante

Le retour en arrière local a arrêté la production

Symptôme (simulé) : Amira revient à l’ancienne version en production locale avec le script habituel et l’empreinte de la version 1.1.0 :

Terminal (simulated)
scripts/promote-local.sh production ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a

La sortie (simulée, abrégée) :

Container ticket-api-production-migrate-1 Recreate
Container ticket-api-production-api-1 Recreate
Container ticket-api-production-api-1 Recreated
Container ticket-api-production-migrate-1 Started
Container ticket-api-production-migrate-1 Error service "migrate" didn't complete successfully: exit 1
service "migrate" didn't complete successfully: exit 1

Après 2 secondes, le script s’arrête avec le code de sortie 1. curl http://127.0.0.1:8082/health ne reçoit aucune réponse. Le conteneur 1.2.1 a disparu, et le nouveau conteneur api est seulement à l’état « Created » (créé). Le retour en arrière n’a pas rétabli l’ancienne version : il a arrêté la production.

Cause : l’étape de déploiement de la version 1.2.x exécute d’abord les migrations, avec l’image qu’elle déploie. docker logs ticket-api-production-migrate-1 affiche :

/usr/local/bin/python: No module named ticket_api.migrate

La version 1.1.0 n’a pas d’exécuteur de migrations : elle est antérieure aux migrations. Compose a supprimé les anciens conteneurs avant que les nouveaux soient prêts. Il ne restait donc aucune solution de repli.

Solution :

  1. Démarrez seulement l’application avec l’ancienne image : docker compose … up -d --wait --no-deps api, comme dans le parcours simulé. La base de données possède déjà le schéma nécessaire à la version 1.1.0.
  2. Vérifiez avec scripts/smoke.sh, comme après chaque déploiement.
  3. Avant le prochain vrai retour en arrière, faites un exercice. Un exercice révèle ce problème à un moment calme.

Pourquoi le parcours réel n’a pas échoué : les applications infonuagiques n’ont pas de base de données. Le flux de travail Deploy n’a donc aucune étape de migration. De plus, Container Apps garde l’ancienne révision jusqu’à ce que la nouvelle soit prête. Un déploiement Compose local n’offre pas cette protection.

Pour un vrai service d’IA : un retour en arrière exécute l’ancienne version avec les nouvelles étapes de déploiement. Certaines étapes sont apparues après l’ancienne version, comme une migration, un téléchargement de modèle ou un nouveau secret. Si l’ancienne version ne peut pas exécuter une telle étape, le retour en arrière échoue exactement quand vous en avez besoin. Gardez les étapes de déploiement rétrocompatibles et exercez-vous au retour en arrière avant l’incident.

Vérifiez votre compréhension

Après le retour de l'application à la version 1.1.0, CLASSIFIER_VERSION=1.1 était encore défini, mais la réponse indiquait keywords-1.0. Pourquoi? Qu'est-ce que cela signifie pour un plan de retour en arrière?

Afficher la réponse

La version 1.1.0 ne connaît pas le paramètre CLASSIFIER_VERSION : elle est antérieure au drapeau. Elle l’ignore donc et utilise toujours keywords-1.0. Un retour en arrière de l’application retire tout ce qui est apparu après l’ancienne version. Ici, il retire le correctif, le champ score et le nouveau modèle. Un plan de retour en arrière doit énumérer ces pertes avant le début de l’opération. Informez les clients et le service d’assistance. Si seul le modèle pose problème, un retour en arrière du drapeau est plus limité et plus rapide (43 secondes). Il conserve le reste.

Vérifiez votre compréhension

Après un retour de la version 1.2.1 à la version 1.1.0, puis à la version 1.2.1, certaines lignes ont un score vide. Amira dit que le retour à la version 1.2.1 les remplira. A-t-elle raison? Que faites-vous?

Afficher la réponse

Non. Un déploiement change le code exécuté, jamais les lignes déjà écrites. Les lignes écrites par la version 1.1.0 restent sans score, même après le retour à la version 1.2.1. (La version 1.2.1 les affiche correctement, car elle utilise confidence quand score est vide, mais les lignes restent vides.) Réparez-les vers l’avant avec une migration qui remplit la colonne : 003_backfill_score. Exécutez-la quand aucune instance 1.1.0 ne reste. Faites d’abord une sauvegarde, car elle modifie des lignes. Une restauration de sauvegarde sert seulement à récupérer des données perdues : elle supprimerait aussi toutes les lignes écrites depuis la sauvegarde.

Vérifiez votre compréhension

Pourquoi faut-il déployer l'ancienne empreinte plutôt que reconstruire l'ancien commit pour revenir à une ancienne version de l'application?

Afficher la réponse

L’empreinte désigne l’image exacte que la CI a testée et qui a déjà fonctionné en production. Une nouvelle construction du même commit (enregistrement des changements) produit un artéfact différent. L’image de base et les paquets peuvent être plus récents, et aucune vérification n’a été exécutée sur cet artéfact. Le retour en arrière déploierait alors un élément non testé, au pire moment. L’empreinte accélère aussi le retour en arrière, car rien n’est construit. Elle assure également la traçabilité : la page de mise en production et build-info.json indiquent le commit et l’exécution qui l’ont produite.

Récapitulatif

  • Trois parties peuvent revenir en arrière, chacune à sa façon : l’application (déployer l’empreinte précédente, environ 2 minutes avec préproduction et approbation), le modèle (changer seulement le drapeau, 43 secondes) et les données (jamais par un retour en arrière).
  • Avant un retour en arrière, énumérez ce qu’il retire. De la version 1.2.x à la version 1.1.0 : le correctif, score et keywords-1.1, car la version 1.1.0 ne connaît pas le drapeau.
  • Un retour en arrière fonctionne seulement si l’ancienne version fonctionne avec la base de données actuelle. Les migrations qui élargissent d’abord le schéma (002 a ajouté une colonne) gardent cette possibilité.
  • Corrigez les données erronées vers l’avant, avec une migration comme 003_backfill_score : quand aucune ancienne instance ne reste, et après une sauvegarde. Restaurez une sauvegarde seulement si des données sont perdues. Précisez aussi ce qui est perdu depuis la sauvegarde.
  • Faites un exercice avant d’avoir besoin du vrai retour en arrière. Localement, l’exercice a révélé que l’ancienne version ne peut pas exécuter la nouvelle étape de migration.

Leçon suivante : Procédure de correctif urgent