Mise en production progressive
- Leçon 12
- 50 min
- Module 4 : Mettre en production les changements d'IA et de base de données
Leçon 12 sur 15 · CI/CD, tests et mises en production sûresMise en production progressive
1. Comprendre le chemin de livraison
2. Créer un pipeline CI reproductible
3. Gérer les environnements et les secrets
4. Mettre en production les changements d'IA et de base de données
5. Rétablir et maintenir le processus de mise en production
Ce que vous saurez faire
La barrière a évalué keywords-1.1 sur 33 billets de soutien. La production en compte des milliers, dont certains ne ressemblent à aucun billet du jeu de données. Cette leçon expose d’abord une petite part des requêtes réelles au nouveau classificateur, puis surveille cette part séparément. Ensuite, elle active le classificateur pour tout le monde, ou le désactive.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Le drapeau
CLASSIFIER_VERSIONet la barrière, présentés dans Barrière d’évaluation de l’IA : exemple détaillé : keywords-1.1 derrière un indicateur et La barrière dans la CI. - La version 1.2.0, ses migrations et deux versions sur une même base de données, présentées dans Migrations sûres : ajouter maintenant, retirer plus tard et Exercice guidé : deux versions sur une base de données.
- Les révisions et le mode à révision unique dans Azure Container Apps, présentés dans Configuration opérationnelle : exemple détaillé : appliquer le fichier (parcours réel) du cours L’essentiel de Docker et du nuage pour les développeurs en IA.
- Le flux de travail Deploy (déployer) et l’approbation de la production, présentés dans Promouvoir en préproduction : le flux de travail Deploy et Approuver le déploiement en production.
Ce qu’il vous faut sur votre ordinateur
- La version 1.2.0 fusionnée dans
mainet déployée. Parcours simulé : dans votre production locale. Parcours en ligne : dans vos deux applications conteneurisées. Consultez Migrations sûres : exercice guidé. Les deux utilisentCLASSIFIER_VERSION=1.0en production. - Parcours en ligne seulement : Azure CLI 2.88.0 avec l’extension containerapp, connecté à l’abonnement de vos applications. Un déploiement canari ajoute une deuxième révision pendant une courte période. Avec un minimum de 0 réplique et quelques centaines de requêtes, il reste dans l’allocation mensuelle gratuite, comme dans le cours Docker. Vous supprimez le groupe de ressources au module 5.
- Parcours simulé seulement : Python 3 (l’environnement virtuel du projet suffit) et Docker Desktop.
- Si votre projet manque ou ne fonctionne plus, copiez l’instantané
end-of-m04et promouvez-le comme dans Migrations sûres.
Les étapes en ligne ont été exécutées avec Azure CLI 2.88.0 (containerapp 1.3.0b5) dans canadacentral. Elles ont modifié l’application ticket-api-production de Nextia-Intelligence/ticket-api-ci. Les étapes simulées ont été exécutées le même jour avec Docker Desktop 4.81.0 et Python 3.14.6 sur macOS (Apple silicon). Les étapes pour Windows et Linux n’ont pas été testées.
Un drapeau et une petite part
Section intitulée « Un drapeau et une petite part »La version 1.2.0 est passée en préproduction, puis en production par le pipeline, après l’approbation : exécution Deploy 37667574234. Amira lui a donné l’étiquette v1.2.0, et le flux de travail Release (mise en production) a étiqueté la même image (exécution 37667587273). En production, CLASSIFIER_VERSION vaut encore 1.0, donc tous les utilisateurs reçoivent encore keywords-1.0. Le nouveau code est là, mais désactivé.
Voilà le premier outil d’une mise en production progressive : le drapeau de fonctionnalité de la première leçon. Activer un drapeau change les paramètres, sans nouvelle construction. L’image conserve la même empreinte que celle testée par la CI. La responsable du soutien demande : « Peut-on l’activer pour tout le monde aujourd’hui? » Amira répond : « D’abord pour un billet sur dix. »
Un canari est une nouvelle version qui reçoit d’abord une petite part du trafic réel. La version actuelle, la version stable, reçoit le reste. Le nom vient des oiseaux que les mineurs emportaient dans les mines : un problème touche le canari avant tout le monde. Cette part est le poids de trafic : un poids de 10 donne à chaque requête 10 % de chances d’atteindre le canari.
Azure Container Apps permet de le faire avec des révisions. Modifier une variable d’environnement crée une nouvelle révision (Configuration opérationnelle dans le cours Docker). En mode à révisions multiples, plusieurs révisions sont actives, et le point d’entrée répartit les requêtes selon leur poids.
Description textuelle du diagramme
Les deux révisions exécutent la même image, 1.2.0. Elles diffèrent par un seul paramètre : CLASSIFIER_VERSION. Le point d’entrée a choisi une révision pour chaque requête : 185 des 200 requêtes ont atteint keywords-1.0 et 15, keywords-1.1. La réponse elle-même indique quel classificateur a répondu (model_version, version du modèle). Log Analytics compte les requêtes par révision et statut. Vous pouvez donc surveiller chaque version séparément.
À quel point une requête sur dix est-elle aléatoire?
Section intitulée « À quel point une requête sur dix est-elle aléatoire? »Le point d’entrée ne compte pas « neuf à la version stable, une au canari ». Il choisit séparément pour chaque requête. Donc, 200 requêtes à 10 % donnent environ 20 requêtes au canari. L’exécution en ligne en a donné 15.
Explorer
Poids du canari : résultat attendu et un échantillon aléatoire de 200 requêtes
Prédisez d’abord : Amira règle le poids du canari à 10 % et envoie 200 requêtes. Combien atteignent keywords-1.1 : exactement 20? Écrivez le plus petit et le plus grand nombre que vous accepteriez comme normaux.
- Canari : attendu
- 20 sur 200
- Canari : cet échantillon
- 17 sur 200 (8,5 %)
- La plupart des échantillons de 200 (95 %)
- de 12 à 29
Poids 10 % : attendez-vous à 20 requêtes sur 200 vers le canari. L’échantillon 1 en a envoyé 17, 3 de moins que prévu. Chaque requête est choisie seule, donc la plupart des échantillons de 200 en donnent de 12 à 29. Le canari réel du 7 octobre 2026 avait le même poids et a reçu 15 requêtes sur 200 (7,5 %), aussi dans cette plage.
| Révision | Attendu | Cet échantillon | Part |
|---|---|---|---|
Stable keywords-1.0 | 180 | 183 | 91,5 % |
Canari keywords-1.1 | 20 | 17 | 8,5 % |
- Révision stable, keywords-1.0
- Révision canari, keywords-1.1 (avec un point)
Description textuelle de l’explorateur
L’explorateur a un seul curseur : le poids de trafic du canari, de 0 % à 100 %. Il envoie 200 requêtes et décide séparément pour chacune. Une requête va au canari lorsque son nombre aléatoire est inférieur au poids. L’affichage montre le nombre attendu de requêtes au canari (200 × poids) et le nombre dans cet échantillon. Il montre aussi l’intervalle contenant 95 % des échantillons de 200. Un tableau montre les nombres attendus et obtenus pour les deux révisions. Une grille représente les 200 requêtes par des carrés : bleus pour la révision stable, orange avec un point pour le canari.
Au départ (poids de 10 %), on attend 20 requêtes. L’échantillon 1 en envoie 17 au canari et 183 à la révision stable. La plupart des échantillons en donnent de 12 à 29. Le canari en ligne en a reçu 15, dans cet intervalle. Les échantillons suivants donnent 20, 18, 22 et 21. À 0 %, aucune requête n’atteint le canari, dans aucun échantillon; à 100 %, toutes l’atteignent. Une graine fixe rend l’échantillon identique à chaque visite. Une requête envoyée au canari à un poids faible y reste à un poids plus élevé.
C’est important quand vous analysez un canari. Avec 200 requêtes à 10 %, le canari en reçoit généralement de 12 à 29. Si une seule échoue, le taux d’erreur se situe entre 3 % et 8 %, pour une seule requête. Un petit canari trouve vite les gros problèmes. Pour voir de petites différences, il lui faut plus de requêtes ou plus de temps.
Exemple détaillé : le canari en ligne
Section intitulée « Exemple détaillé : le canari en ligne »Amira effectue le déploiement canari à la main avec Azure CLI, comme propriétaire de l’abonnement.
Elle examine d’abord les révisions actives (en ligne, tirées du journal d’exécution) :
az containerapp revision list -g rg-ticket-api-ci -n ticket-api-production \ --query "[?properties.active].{name:name, traffic:properties.trafficWeight, image:properties.template.containers[0].image}" -o tableName Traffic Image------------------------------ --------- -------------------------------------------------------ticket-api-production--0000003 0 ghcr.io/usmanahmed99/ticket-api-ci@sha256:e5157802...ticket-api-production--0000004 100 ghcr.io/usmanahmed99/ticket-api-ci@sha256:bd2316b5...0000004 est la version 1.2.0 et reçoit tout le trafic. Elle passe l’application en mode à révisions multiples et crée la révision canari : la même image, avec le drapeau activé. --revision-suffix kw11 donne à la révision un nom qui indique son rôle :
az containerapp revision set-mode -g rg-ticket-api-ci -n ticket-api-production --mode multipleaz containerapp update -g rg-ticket-api-ci -n ticket-api-production \ --set-env-vars CLASSIFIER_VERSION=1.1 --revision-suffix kw11 \ --query properties.latestRevisionName -o tsvticket-api-production--kw11Ensuite, elle vérifie le trafic et remarque un problème :
Name Traffic Health------------------------------ --------- --------ticket-api-production--0000004 0 Healthyticket-api-production--kw11 100 HealthyLa nouvelle révision a reçu 100 % du trafic d’un coup. L’erreur courante ci-dessous explique pourquoi et présente l’ordre qui l’évite. Elle règle immédiatement les poids :
az containerapp ingress traffic set -g rg-ticket-api-ci -n ticket-api-production \ --revision-weight ticket-api-production--0000004=90 ticket-api-production--kw11=10 -o tableRevisionName Weight------------------------------ --------ticket-api-production--0000004 90ticket-api-production--kw11 10Elle envoie maintenant 200 requêtes avec le même billet, « Package never arrived » (colis jamais arrivé). keywords-1.0 répond other (autre) et keywords-1.1 répond shipping (expédition), donc la réponse indique quelle révision a répondu. Elle compte les valeurs de model_version :
185 keywords-1.0 other 15 keywords-1.1 shippingLa réponse est une source. Les journaux de la plateforme en sont une deuxième, indépendante de l’application. Log Analytics compte les requêtes de la période canari par révision et statut. Ce résultat vient de l’exécution en ligne; le journal n’a conservé que la fin de la requête, summarize requests=count() by RevisionName_s, status :
RevisionName_s Requests Status------------------------------ ---------- --------ticket-api-production--kw11 15 200ticket-api-production--0000004 186 200ticket-api-production--0000004 1 401ticket-api-production--0000003 1 401ticket-api-production--0000003 1 200Le canari a répondu à ses 15 requêtes avec 200. Les lignes 401 sont des vérifications sommaires sans clé, qui doivent répondre 401. La ligne 0000004 compte une requête de plus que la boucle, provenant d’une autre vérification. Les deux lignes de 0000003 appartiennent à l’ancienne révision, dont le poids était 0 : elles ne font pas partie du canari. Aucune révision n’a répondu 500.
Quand arrêter un canari
Section intitulée « Quand arrêter un canari »Décidez des règles d’arrêt avant le démarrage du canari, et écrivez-les dans le plan de mise en production. Si vous décidez pendant le canari, chaque résultat semble « probablement correct ». Pour l’API de billets, Amira a écrit :
| À surveiller | Où | Arrêter le canari si |
|---|---|---|
| Erreurs | Journaux par révision : statuts 500 et 503 |
Le canari a un 500, ou plus d’erreurs que la révision stable |
| Latence | Journaux par révision, ou métriques de la plateforme | Le canari est nettement plus lent |
| Le modèle en production | Réponses par model_version : part de chaque catégorie et de la priorité 1 |
La part de priorité 1 ou de other du canari est loin des résultats de la barrière |
| Personnes | Le soutien : billets transférés manuellement à une autre équipe | Le soutien signale des catégories incorrectes provenant du canari |
Les métriques de la barrière nécessitent des étiquettes, et les billets de production n’en ont pas au départ. En production, vous surveillez donc les signes visibles immédiatement : les erreurs et la part de chaque réponse. Les corrections du soutien viennent plus tard. Ce cours a mesuré seulement les requêtes par révision et statut. Les autres lignes indiquent ce qu’une équipe ajoute pour un vrai modèle.
Pour arrêter, donnez 0 % au canari (--revision-weight ticket-api-production--0000004=100 ticket-api-production--kw11=0), puis revenez au mode à révision unique. Les utilisateurs retrouvent keywords-1.0 en quelques secondes, sans nouvelle construction.
Promouvoir, puis revenir au mode à révision unique
Section intitulée « Promouvoir, puis revenir au mode à révision unique »Le canari n’a montré aucun problème. Amira lui a donc donné tout le trafic et a remis l’application en mode à révision unique (en ligne) :
az containerapp ingress traffic set -g rg-ticket-api-ci -n ticket-api-production \ --revision-weight ticket-api-production--kw11=100 ticket-api-production--0000004=0 -o tableaz containerapp revision set-mode -g rg-ticket-api-ci -n ticket-api-production --mode singleaz containerapp show -g rg-ticket-api-ci -n ticket-api-production \ --query "properties.template.containers[0].env[?name=='CLASSIFIER_VERSION']" -o tableName Value------------------ -------CLASSIFIER_VERSION 1.1Ces lignes entraînent deux conséquences :
- Revenez au mode à révision unique après un canari. Le flux de travail Deploy exécute
az containerapp update --image. En mode à révisions multiples, cette nouvelle révision ne reçoit aucun trafic ou le prend entièrement, selon la règle de trafic. Aucun de ces comportements ne correspond à ce qu’attend le pipeline. - Le drapeau reste activé.
CLASSIFIER_VERSION=1.1fait maintenant partie du modèle de configuration de l’application. Le prochain déploiement d’une nouvelle image le conserve. Pour désactiver de nouveau le modèle, changez le drapeau. C’est un retour en arrière du modèle, sans retour en arrière de l’application (module 5).
Le parcours simulé : un canari sur votre ordinateur
Section intitulée « Le parcours simulé : un canari sur votre ordinateur »Docker n’a pas de poids de trafic. Vous reproduisez la même structure avec trois petites parties. La version stable est votre production locale sur le port 8082 (drapeau 1.0). Le canari est un conteneur de la même image sur le port 8084 (drapeau 1.1). split.py, sur le port 8090, envoie chaque requête à l’une des deux versions, au hasard, selon le poids. Les clients appellent seulement 8090, comme ils appellent seulement l’adresse de l’application dans le nuage.
- Vérifiez que la production locale exécute 1.2.0 :
scripts/smoke.sh http://127.0.0.1:8082 "$(cat secrets/production_api_key.txt)"affiche quatre lignesok(réussi). - Démarrez le canari : même empreinte, même réseau et même base de données, avec les mêmes paramètres que la production sauf le drapeau. La commande se trouve sous les étapes.
- Faites une vérification sommaire du canari sur son propre port :
scripts/smoke.sh http://127.0.0.1:8084 "$(cat secrets/production_api_key.txt)". - Enregistrez
split.pyà côté du dossier de votre projet, pas à l’intérieur : par exemple,~/projects/split.py. Le code se trouve ci-dessous. - Dans un deuxième terminal, démarrez le répartiteur avec un poids de canari de 10. Laissez-le fonctionner.
- Dans le premier terminal, envoyez 200 requêtes au port 8090 et comptez les réponses par
model_version. - Lisez le deuxième terminal : une ligne par requête, avec la version qui a répondu.
- Arrêtez le répartiteur avec Ctrl+C. Il affiche les requêtes par version et statut.
Le conteneur canari (étape 2). La production masque /docs, donc le canari aussi : un canari doit différer par un seul paramètre.
NEW=ghcr.io/usmanahmed99/ticket-api-ci@sha256:bd2316b56067c738199d6203a23845c97fb7b85b550379411eddd7cc6a2ddab8docker run -d --name ticket-api-canary --platform linux/amd64 \ --network ticket-api-production_default -p 127.0.0.1:8084:8000 \ -e API_KEY_FILE=/run/secrets/api_key -e REQUIRE_API_KEY=true -e SHOW_DOCS=false \ -e CLASSIFIER_VERSION=1.1 \ -v "$PWD/secrets/production_api_key.txt:/run/secrets/api_key:ro" \ -e DATABASE_URL="postgresql://tickets:$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2)@db:5432/tickets" \ "$NEW"Utilisez votre propre empreinte de 1.2.0 pour NEW. Le canari utilise le fichier de clé de production, donc les clients n’ont pas besoin d’une nouvelle clé. Il utilise aussi la base de données de production, donc son historique se trouve dans la même table.
Afficher split.py
"""A small traffic splitter for the simulated canary (C19-M04-L03).
python3 split.py --stable http://127.0.0.1:8082 --canary http://127.0.0.1:8084 --weight 10
Clients call http://127.0.0.1:8090 only. Each request is sent to the canarywith the chance --weight (in percent), chosen on its own, as in Azure.Only the Python standard library. For practice on your computer only: noTLS and no protection. Stop it with Ctrl+C."""
import argparseimport randomimport signalimport sysimport urllib.errorimport urllib.requestfrom collections import Counterfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOP_BY_HOP = {"connection", "keep-alive", "transfer-encoding", "host", "content-length"}
def make_handler(stable: str, canary: str, weight: int, choose, counts: Counter): class Split(BaseHTTPRequestHandler): def forward(self) -> None: target = "canary" if choose() < weight / 100 else "stable" base = canary if target == "canary" else stable length = int(self.headers.get("Content-Length") or 0) body = self.rfile.read(length) if length else None headers = {k: v for k, v in self.headers.items() if k.lower() not in HOP_BY_HOP} request = urllib.request.Request(base + self.path, body, headers, method=self.command) try: with urllib.request.urlopen(request, timeout=30) as answer: status, data, answer_headers = answer.status, answer.read(), answer.headers except urllib.error.HTTPError as error: status, data, answer_headers = error.code, error.read(), error.headers except OSError: status, data, answer_headers = 502, b'{"error":"upstream not reachable"}', {} counts[(target, status)] += 1 self.send_response(status) for k, v in answer_headers.items(): if k.lower() not in HOP_BY_HOP: self.send_header(k, v) self.send_header("X-Served-By", target) self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) print(f"{target:<6} {status} {self.command} {self.path}", flush=True)
do_GET = do_POST = forward
def log_message(self, *args) -> None: # one line per request is enough pass
return Split
def stop(*_) -> None: raise KeyboardInterrupt
def main() -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--stable", required=True, help="address of the current version") parser.add_argument("--canary", required=True, help="address of the new version") parser.add_argument("--weight", type=int, default=10, help="percent of requests for the canary") parser.add_argument("--port", type=int, default=8090) parser.add_argument("--seed", type=int, help="repeat the same random choices (for tests)") args = parser.parse_args() if not 0 <= args.weight <= 100: parser.error("--weight must be from 0 to 100")
choose = random.Random(args.seed).random counts: Counter = Counter() handler = make_handler(args.stable.rstrip("/"), args.canary.rstrip("/"), args.weight, choose, counts) server = ThreadingHTTPServer(("127.0.0.1", args.port), handler) print(f"127.0.0.1:{args.port}: {100 - args.weight}% to {args.stable}, " f"{args.weight}% to {args.canary}", flush=True) # Ctrl+C, or `kill` from a script (a background job ignores Ctrl+C). signal.signal(signal.SIGTERM, stop) try: server.serve_forever() except KeyboardInterrupt: pass print("\nrequests by version and status:", file=sys.stderr) for (target, status), n in sorted(counts.items()): print(f" {target:<6} {status} {n}", file=sys.stderr) return 0
if __name__ == "__main__": sys.exit(main())Le programme lit chaque requête et choisit une cible avec un nombre aléatoire. Il y envoie la même méthode, le même chemin, les mêmes en-têtes et le même corps. Il retourne la réponse avec un en-tête supplémentaire, X-Served-By. Il accomplit le travail du point d’entrée dans le nuage, en environ 60 lignes.
Étapes 5 et 6 :
python3 ~/projects/split.py --stable http://127.0.0.1:8082 --canary http://127.0.0.1:8084 --weight 10KEY=$(cat secrets/production_api_key.txt)for i in $(seq 200); do curl -s -X POST http://127.0.0.1:8090/v1/classify -H "X-API-Key: $KEY" \ -H "Content-Type: application/json" -d '{"subject": "Package never arrived"}' echodone | grep -o '"model_version":"[^"]*"' | sort | uniq -cLa sortie (simulée, première exécution de l’auteur) :
177 "model_version":"keywords-1.0" 23 "model_version":"keywords-1.1"Le deuxième terminal affiche 127.0.0.1:8090: 90% to http://127.0.0.1:8082, 10% to http://127.0.0.1:8084 (90 % vers le port 8082, 10 % vers le port 8084). Ensuite, il affiche une ligne par requête, comme stable 200 POST /v1/classify ou canary 200 POST /v1/classify (version stable ou canari, statut 200). Après Ctrl+C (simulation, exécution ultérieure) :
requests by version and status: canary 200 17 stable 200 183Cinq exécutions au poids de 10 ont donné 23, 27, 20, 24 et 17 requêtes au canari : la même dispersion que l’explorateur. Vos nombres diffèrent; ils devraient se situer entre 12 et 29 la plupart du temps. --seed 7 rend les choix répétables : deux exécutions avec cette option ont toutes deux donné 175 et 25.
Pour promouvoir dans le parcours local, définissez CLASSIFIER_VERSION=1.1 dans deploy/local/production.env. Exécutez de nouveau scripts/promote-local.sh production "$NEW" avec la même empreinte, puis supprimez le canari avec docker rm -f ticket-api-canary. La promotion de l’auteur a pris environ 10 secondes. « Package never arrived » a ensuite reçu la réponse shipping avec keywords-1.1. En équipe, la modification de production.env passe par une demande de tirage (pull request), comme tout autre changement. Le dépôt de référence a conservé 1.0 dans ce fichier.
Afficher le résultat attendu du canari simulé
Les étapes 1 et 3 affichent chacune quatre lignes ok. Une requête à chaque port montre la différence : 8082 répond {"category":"other","priority":3,"confidence":0.2,"score":0.2,"model_version":"keywords-1.0"}, et 8084 répond {"category":"shipping","priority":1,"confidence":0.9,"score":0.9,"model_version":"keywords-1.1"}. Le compte des 200 requêtes comporte deux lignes dont la somme est 200, avec environ 20 pour keywords-1.1. Le résumé de split.py donne les deux mêmes nombres, tous avec le statut 200.
À vous de jouer : un canari qui tombe en panne
Section intitulée « À vous de jouer : un canari qui tombe en panne »Changez une condition : pendant le canari, arrêtez son conteneur, comme si la nouvelle version avait planté. Gardez split.py en marche avec un poids de 10, et envoyez de nouveau 200 requêtes. Comptez les codes de statut plutôt que les versions, avec cette boucle :
for i in $(seq 200); do curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8090/v1/classify -H "X-API-Key: $KEY" \ -H "Content-Type: application/json" -d '{"subject": "Package never arrived"}'done | sort | uniq -cPrédisez d’abord : combien de requêtes échouent, et comment verriez-vous quelle version ne fonctionne plus?
Afficher la réponse
Après docker stop ticket-api-canary, l’exécution de l’auteur a donné (simulation) :
183 200 17 502et le résumé de split.py :
requests by version and status: canary 502 17 stable 200 183Environ une requête sur dix a échoué, et chaque échec venait du canari. Les codes de statut seuls disent : « quelque chose ne va pas pour 8,5 % des requêtes ». Le compte par version indique quoi : le canari. Voilà pourquoi il faut surveiller chaque révision séparément. Avec tout le trafic sur une version, la même panne ferait échouer toutes les requêtes. Arrêtez le canari : réglez son poids à 0 (ici, arrêtez split.py et appelez directement 8082). Ensuite, redémarrez le conteneur canari avec docker start ticket-api-canary, ou supprimez-le.
Tout le trafic d’un coup
Section intitulée « Tout le trafic d’un coup »Erreur courante
La révision canari reçoit 100 % du trafic
Symptôme (en direct) : juste après az containerapp update … --revision-suffix kw11, la liste des révisions affichait ticket-api-production--kw11 100 et ticket-api-production--0000004 0. Pendant un court moment, tous les utilisateurs ont reçu keywords-1.1 : précisément ce que le canari devait éviter.
Cause : la règle de trafic de l’application suivait la dernière révision. En mode unique, c’est le comportement voulu : une nouvelle révision reçoit le trafic quand elle est prête. C’est ce que vous avez vu dans le cours L’essentiel de Docker et du nuage pour les développeurs en IA. Passer en mode multiple ne change pas la règle. La nouvelle révision a donc reçu tout le trafic, comme chaque nouvelle révision auparavant.
Correction : fixez le trafic sur la révision actuelle par son nom, avant de créer le canari. La dernière révision commence alors avec 0 % :
az containerapp revision set-mode -g rg-ticket-api-ci -n ticket-api-production --mode multipleaz containerapp ingress traffic set -g rg-ticket-api-ci -n ticket-api-production \ --revision-weight ticket-api-production--0000004=100az containerapp update -g rg-ticket-api-ci -n ticket-api-production \ --set-env-vars CLASSIFIER_VERSION=1.1 --revision-suffix kw11az containerapp revision list -g rg-ticket-api-ci -n ticket-api-production \ --query "[?properties.active].{name:name, traffic:properties.trafficWeight}" -o tableaz containerapp ingress traffic set -g rg-ticket-api-ci -n ticket-api-production \ --revision-weight ticket-api-production--0000004=90 ticket-api-production--kw11=10Vérifiez la liste après la mise à jour : le canari doit afficher 0 avant que vous lui donniez 10. L’essai a suivi ces étapes dans l’autre ordre. Cet ordre découle donc de la cause, et non d’un deuxième essai.
Pour un vrai service d’IA : un modèle peut réussir sur les données d’évaluation, mais échouer sur des entrées absentes de ces données. C’est dans les premières minutes d’une mise en production que vous en savez le moins. Un canari qui passe discrètement à 100 % donne à tous les utilisateurs la version la plus risquée. Il ne reste alors rien pour la comparer. Vérifiez les poids après chaque étape et gardez cette vérification dans le plan de mise en production.
Vérifiez votre compréhension
Pourquoi Amira pouvait-elle utiliser la même empreinte d'image pour les deux révisions du canari? Pourquoi est-ce préférable à un canari utilisant une nouvelle image?
Afficher la réponse
La version 1.2.0 contenait déjà keywords-1.1, désactivé par l’indicateur CLASSIFIER_VERSION. Une révision avec l’indicateur activé constitue un changement de paramètres. Les deux révisions utilisent donc exactement l’image testée par la CI et vérifiée par la barrière. Leur seule différence est le modèle. Si le canari révèle un problème, la cause ne peut être que keywords-1.1. Pour l’arrêter, changez les poids ou l’indicateur, sans construction.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Après la promotion, Amira oublie set-mode single. La prochaine mise en production normale exécute le flux Deploy avec une nouvelle empreinte. Que peut-il arriver?
Afficher la réponse
En mode multiple, az containerapp update --image crée une nouvelle révision à côté de kw11. Selon la règle de trafic, elle ne reçoit aucun trafic ou reçoit tout le trafic d’un coup. Sans trafic, la mise en production semble réussir, mais les utilisateurs gardent l’ancienne version. Avec tout le trafic, il n’y a aucun canari. L’attente et le test de bon fonctionnement du pipeline ne distinguent pas ces cas, car ils vérifient la dernière révision. Revenez au mode unique après chaque canari et vérifiez le trafic des révisions actives.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Exercice du module
Section intitulée « Exercice du module »Exercice guidé → tâche autonome. Vous avez vu Amira mettre en production deux changements à la fois : un nouveau champ d’API (score, avec des migrations) et une nouvelle version du modèle (keywords-1.1, derrière un indicateur). Planifiez maintenant votre propre mise en production avant d’écrire le code. Elle doit changer un champ d’API et une version du modèle.
Choisissez votre propre changement, ou utilisez celui-ci. L’API reçoit un nouveau champ queue : l’équipe qui devrait prendre le billet, par exemple billing-team. Le classificateur reçoit une nouvelle version, keywords-1.2, avec une règle qui distingue « colis perdu » de « mot de passe perdu ». Le plan tient dans un fichier, docs/releases/<version>.md, et répond aux questions suivantes :
- Changements pour les clients : qu’est-ce qui est nouveau, qu’est-ce qui est obsolète, et jusqu’à quelle version?
- Le modèle : qu’est-ce qui change, comment l’activer et quel est le résultat de la barrière face à la référence de base (exactitude, rappel, cas obligatoires)? Quel billet obligatoire est nouveau?
- La base de données : quelles migrations, dans quel ordre, et quelle version fonctionne à chaque étape? Faut-il une sauvegarde?
- Le déploiement progressif : l’ordre des étapes, le poids du canari, la durée et les règles d’arrêt, écrites avant le début.
- Retour en arrière : pour chaque partie (application, modèle, données), ce que vous rétablissez et comment.
- Responsable et surveillance : qui est responsable de la mise en production, que surveille-t-on ensuite, et pendant combien de temps?
Afficher un exemple de réponse : le plan de la version 1.2.0
Voici le plan suivi par Amira, tiré du fichier docs/releases/1.2.0.md du dépôt de référence. Les étapes de déploiement progressif et de retour en arrière y ont été ajoutées :
## ticket-api 1.2.0
- **New for clients:** every classification has `score`, from 0 to 1. `confidence` has the same value and is **deprecated**: it stays until version 2.0, so clients have time to change. History items have `score` too.- **New, off by default:** classifier `keywords-1.1` (`CLASSIFIER_VERSION=1.1`). On the 33 holdout tickets: accuracy 0.909 (keywords-1.0: 0.636), every must-pass ticket right. We turn it on in production step by step.- **Database:** migrations replace the table that the app made at start-up. Run `python -m ticket_api.migrate` before the new version starts (Compose does it in the `migrate` service). Migration `002_add_score` adds a column; version 1.1.0 keeps working with it.- **Owner:** Amira. **Watch after release:** the smoke checks, the error rate in the logs, and `GET /v1/history`, for one day.- **Rollback:** deploy the 1.1.0 digest. The database needs no change: 1.1.0 ignores the new column.
### Rollout (in this order)1. PR #3: keywords-1.1 behind the flag, and the gate in CI (required check renamed).2. PR #5: migrations 001 and 002, `score` next to `confidence`, database tests.3. Deploy 1.2.0 with the flag off: staging, then production after approval. Migrations run before the app. Check old and new on one database.4. Canary in production: pin 0000004=100, make revision kw11 (flag 1.1), then 90/10. Stop if kw11 has any 500, or more errors than 0000004.5. Promote: kw11=100, then back to single revision mode.
### Rollback, by part- Model only: CLASSIFIER_VERSION=1.0 (no new build; during the canary: kw11=0).- Application: deploy the 1.1.0 digest. Users lose `score` and keywords-1.1.- Data: none needed for 002. A backfill (003) only after no 1.1.0 runs.- Contract (remove `confidence`): not in 1.2.x. In 2.0, after clients moved.Une personne qui révise le plan le juge démontré quand quatre conditions sont remplies. Chaque étape nomme la version qui doit encore fonctionner à ce moment. La base de données change avant le code qui en dépend, et rien n’est retiré dans la même mise en production. Le modèle a un résultat de barrière comparé à la référence de base et peut être désactivé sans construction. Le canari a un poids et des règles d’arrêt écrites avant le début. Un plan limité à « déployer, puis revenir en arrière au besoin » est pas encore démontré. Il ne précise pas ce que le retour en arrière retire.
Vérification du module
Section intitulée « Vérification du module »Vérifiez votre compréhension
Rappel : nommez les trois parties de la barrière d'évaluation et les quatre étapes d'expansion et de contraction du champ score.
Afficher la réponse
La barrière :
- Minimums : exactitude d’au moins 0,6, chaque rappel d’au moins 0,5.
- Aucune régression par rapport à la référence de base en production : baisse d’exactitude d’au plus 0,02, baisse de rappel d’au plus 0,1 par catégorie.
- Billets obligatoires correctement classés, avec la bonne priorité : E019, E026, E033.
Expansion et contraction :
- Expansion : la migration 002 ajoute
score, qui peut être vide. - Transition : la version 1.2.x écrit les deux champs, l’API marque
confidencecomme obsolète, et les clients s’adaptent. - Remplissage rétroactif : la migration 003 remplit
scoreà partir deconfidence, une fois qu’aucune version 1.1.0 ne tourne. - Contraction : la version 2.0 retire
confidence.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Prédiction : Amira lance un deuxième canari avec un poids de 5 % et envoie 100 requêtes. Le canari reçoit 3 requêtes, dont une renvoie 500. La révision stable en reçoit 97 sans erreur. Que conclut-elle et que fait-elle?
Afficher la réponse
Une erreur sur 3 requêtes représente un taux d’erreur élevé, mais sur très peu de requêtes. Il peut s’agir d’une seule requête problématique ou d’une véritable défaillance. Sa règle d’arrêt dit « tout code 500 sur le canari », alors elle l’arrête d’abord. Avec un poids de 0, les utilisateurs reviennent à la version stable en quelques secondes, sans construction. Elle lit ensuite le journal du canari pour cet identifiant de requête afin de trouver la cause. À 5 % de 100 requêtes, on en attend 5, et 95 % des échantillons donnent de 1 à 10 requêtes. L’explorateur montre le même type de dispersion pour 200. Un petit canari doit donc avoir des règles d’arrêt qui réagissent à chaque erreur, ou recevoir plus de requêtes avant une décision.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Vérification de fin : démontrez la compatibilité pendant que les anciennes et nouvelles versions de l'application coexistent. Quelles preuves présentez-vous?
Afficher la réponse
Présentez les preuves de l’exercice guidé de Migrations sûres :
- la sortie de
migrate(migrer), avant le démarrage de la nouvelle version; - les versions 1.2.0 et 1.1.0 qui fonctionnent sur la même base de données en même temps (
docker psavec les deux conteneurs); - une classification de chaque version, toutes deux avec
200: la nouvelle avecscore, l’ancienne sans ce champ; - la table, avec une ligne de chaque version (la ligne de la version 1.1.0 a un champ
scorevide).
Ajoutez le canari de cette leçon : une image avec deux valeurs d’indicateur sur une base de données, les deux répondant aux requêtes. Utilisez les décomptes de model_version, le résumé de split.py, ou le résultat en direct de 185/15 et les journaux par révision. Indiquez les parties que vous n’avez pas vérifiées : ici, le point de terminaison d’historique de chaque version. Chaque résultat porte la mention « en direct » ou « simulé ». La personne qui révise juge le résultat démontré seulement avec des sorties des deux versions obtenues en même temps, pas l’une après l’autre.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Un indicateur de fonctionnalité (
CLASSIFIER_VERSION) place le nouveau code du modèle en production, mais le laisse désactivé. L’activer change les paramètres de la même image. - Un canari donne à la nouvelle version une petite part du trafic réel. Dans Azure Container Apps : mode à révisions multiples, deuxième révision avec l’indicateur activé et poids de trafic. Localement : un conteneur canari et
split.py. - Chaque requête est choisie indépendamment : 10 % de 200 a donné 15 en direct, et de 17 à 27 localement. Attendez-vous à une plage, pas à un nombre exact.
- Fixez le trafic sur la révision actuelle par son nom avant de créer le canari. Sinon, la nouvelle révision peut recevoir 100 % d’un coup, comme le 7 octobre 2026.
- Surveillez chaque version séparément (réponses par
model_version, journaux par révision et statut), avec des règles d’arrêt écrites avant le début. Faites la promotion, puis revenez au mode à révision unique. - Le module : la barrière a bloqué un modèle moins bon (demande de tirage nº 4). L’expansion et la contraction ont permis aux versions 1.1.0 et 1.2.0 de fonctionner sur une même base de données. Le canari a d’abord présenté keywords-1.1 à une requête sur dix.
Réinitialisation. Parcours simulé : arrêtez split.py (Ctrl+C), supprimez le canari (docker rm -f ticket-api-canary) et tout conteneur ticket-api-old. Gardez ou arrêtez les environnements locaux (scripts/stop-local.sh production; ajoutez --volumes pour supprimer aussi leurs bases de données). Rétablissez CLASSIFIER_VERSION dans deploy/local/production.env à la valeur dans Git (git diff l’affiche). Parcours en direct : az containerapp revision list … -o table doit afficher une seule révision active avec un trafic de 100. La commande az containerapp show … --query properties.configuration.activeRevisionsMode doit afficher Single (unique). Gardez les applications pour le module 5, ou supprimez le groupe de ressources si vous arrêtez ici. Pour réinitialiser les fichiers, copiez l’instantané end-of-m04. Il contient le code de la version 1.2.0 tel que mis en production, avec le bogue découvert au module 5.
Leçon suivante : Stratégie de retour en arrière