Aller au contenu
English

Promouvoir en préproduction

  • Leçon 9
  • 50 min
  • Module 3 : Gérer les environnements et les secrets
Leçon 9 sur 15 · CI/CD, tests et mises en production sûresPromouvoir en préproduction

Ce que vous saurez faire

Transférez l’image testée en préproduction sans que personne tape une commande de déploiement. Vérifiez-la là, puis autorisez sa production seulement après une approbation.

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 (exécuteurs ubuntu-24.04) dans le dépôt public Nextia-Intelligence/ticket-api-ci, Docker Desktop 4.81.0 (Engine 29.6.1, Compose 5.2.0) sur macOS (Apple silicon), et Azure CLI 2.88.0 dans canadacentral. Les étapes pour Windows et Linux n’ont pas été testées.

La règle d’Amira pour cette leçon : après CI, personne ne construit d’image ni ne tape de commande de déploiement. Le pipeline prend l’empreinte dans les preuves de CI et la transfère, un environnement à la fois. Une promotion est ce transfert du même artefact vers l’environnement suivant.

CI réussit sur main. Find the image (trouver l'image) prend l'empreinte dans build-info.json. Préproduction : connexion avec OIDC, az containerapp update --image, attente de la révision, vérification sommaire. Si une étape échoue, la tâche échoue et la production ne démarre jamais. Si tout réussit, la question est : la personne responsable de la révision approuve-t-elle? Si elle refuse, la production garde l'ancienne empreinte. Si elle approuve, la production reçoit la même empreinte avec les mêmes étapes. À la fin, les deux environnements exécutent une seule empreinte.
Le flux de travail Deploy (déployer). Chaque échec arrête l'empreinte là où elle est, et la production continue d'exécuter son ancienne version.
Description textuelle du diagramme

Le flux de travail démarre seulement après la réussite de CI sur main. Sa première tâche lit l’empreinte dans le fichier build-info.json de CI. La préproduction la déploie, attend que la nouvelle révision soit prête, puis lance la vérification sommaire. Un échec à n’importe quelle étape termine l’exécution : la production ne démarre jamais. Si la préproduction réussit, la tâche de production attend une personne responsable de la révision. Une approbation envoie la même empreinte dans les mêmes étapes. Un refus laisse la production telle quelle.

Trois types de vérifications ont lieu en préproduction :

Vérification Ce qu’elle prouve ici Qui la fait
Vérification de santé La nouvelle révision démarre et répond : /health et /ready donnent 200 La plateforme (les sondes) et la vérification sommaire
Vérification d’intégration Cette image fonctionne avec les paramètres et le secret de cet environnement : la classification donne 200 avec la clé, 401 sans la clé La vérification sommaire
Vérification d’acceptation par les utilisateurs Le changement répond aux besoins des utilisateurs, selon le jugement d’une personne Une personne chargée des tests ou responsable du soutien, en préproduction, avant l’approbation

La vérification sommaire, dans Définir les vérifications : huit vérifications, huit objectifs, envoie quelques requêtes rapides sur les chemins essentiels d’un déploiement réel. Elle ne remplace pas les tests dans CI; elle prouve que ce déploiement fonctionne. Pour la vérification d’acceptation par les utilisateurs, la préproduction a SHOW_DOCS=true. Une personne chargée des tests ouvre /docs et essaie de vrais billets de soutien dans le navigateur. Pendant l’exécution de test, la personne responsable de la révision a approuvé uniquement les résultats des vérifications sommaires. Une seule personne faisait tout.

Le flux de travail comprend deux fichiers. deploy.yml décide quoi déployer et dans quel ordre. deploy-to.yml contient les étapes pour un environnement, afin que la préproduction et la production exécutent exactement les mêmes étapes.

.github/workflows/deploy.yml
# Continuous delivery: promote one image, by digest, to staging and then to
# production. It starts when CI succeeds on main, or by hand with a digest
# (for a hotfix or a rollback). Production waits for a reviewer's approval.
name: Deploy
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
workflow_dispatch:
inputs:
digest:
description: "The image digest to deploy, for example sha256:4f1c..."
required: true
reason:
description: "Why you deploy it by hand (hotfix, rollback, ...)"
required: true
permissions:
contents: read
concurrency:
group: deploy
cancel-in-progress: false
jobs:
resolve:
name: Find the image
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-24.04
permissions:
actions: read
outputs:
digest: ${{ steps.digest.outputs.digest }}
steps:
- name: Get build-info.json from the CI run
if: github.event_name == 'workflow_run'
uses: actions/download-artifact@v7
with:
name: build-info
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Choose the digest
id: digest
env:
INPUT_DIGEST: ${{ inputs.digest }}
run: |
if [ -n "$INPUT_DIGEST" ]; then digest="$INPUT_DIGEST"; else digest=$(jq -r .digest build-info.json); fi
case "$digest" in sha256:*) ;; *) echo "Not a digest: $digest"; exit 1;; esac
echo "digest=$digest" >> "$GITHUB_OUTPUT"
echo "Deploying ghcr.io/${{ github.repository }}@$digest" >> "$GITHUB_STEP_SUMMARY"
staging:
needs: resolve
uses: ./.github/workflows/deploy-to.yml
with:
environment: staging
image: ghcr.io/${{ github.repository }}@${{ needs.resolve.outputs.digest }}
secrets: inherit
permissions:
contents: read
id-token: write
production:
needs: [resolve, staging]
uses: ./.github/workflows/deploy-to.yml
with:
environment: production
image: ghcr.io/${{ github.repository }}@${{ needs.resolve.outputs.digest }}
secrets: inherit
permissions:
contents: read
id-token: write

Le fichier complet se trouve dans le dépôt de référence : deploy.yml à 92fff66. Les parties, dans l’ordre :

Partie Ce qu’elle fait, et pourquoi
workflow_run de CI, completed, branches: [main] Démarre quand une exécution de CI sur main se termine. Les demandes de tirage et les autres branches ne déploient jamais.
if: … conclusion == 'success' Une exécution de CI en rouge termine immédiatement l’exécution de Deploy : GitHub saute la tâche resolve et les suivantes.
workflow_dispatch avec digest et reason Un déploiement manuel d’une empreinte que CI a construite auparavant. Le module 5 l’utilise pour le correctif urgent et le retour en arrière.
concurrency: deploy, cancel-in-progress: false Un seul déploiement à la fois, et aucun déploiement en cours ne s’arrête à mi-chemin. Le deuxième attend.
download-artifact avec run-id Prend build-info.json dans l’exécution de CI qui a démarré cette exécution. actions: read permet au jeton de lire les artefacts de cette exécution.
case "$digest" in sha256:* Refuse tout ce qui n’est pas une empreinte, comme une étiquette. Une étiquette peut changer de cible; une empreinte, non.
uses: ./.github/workflows/deploy-to.yml Appelle le flux de travail réutilisable pour un environnement. needs: [resolve, staging] fait attendre la production après la préproduction.
secrets: inherit Transmet les secrets. La tâche appelée reçoit les secrets d’un environnement seulement après avoir satisfait à ses règles.

Autre fait sur workflow_run : GitHub exécute le fichier du flux de travail Deploy depuis la branche par défaut, main. C’est vrai même quand l’exécution de CI concernait une autre branche. Les étapes de déploiement sont donc toujours celles révisées sur main. Le module 5 en a besoin pour le correctif urgent.

.github/workflows/deploy-to.yml
# The same deployment steps for every environment. The environment's settings
# (app name, resource group, URL, identity) are variables of the GitHub
# environment; its API key for the smoke check is an environment secret.
name: Deploy to one environment
on:
workflow_call:
inputs:
environment:
required: true
type: string
image:
required: true
type: string
jobs:
deploy:
name: Deploy to ${{ inputs.environment }}
runs-on: ubuntu-24.04
timeout-minutes: 20
environment:
name: ${{ inputs.environment }}
url: ${{ vars.APP_URL }}
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
- name: Sign in to Azure with this environment's identity (no password)
uses: azure/login@v3
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- name: Deploy the image
env:
IMAGE: ${{ inputs.image }}
run: |
az config set extension.use_dynamic_install=yes_without_prompt --only-show-errors
az containerapp update --name "${{ vars.APP_NAME }}" --resource-group "${{ vars.RESOURCE_GROUP }}" \
--image "$IMAGE" --query "properties.latestRevisionName" -o tsv
- name: Wait for the new revision
run: |
for _ in $(seq 1 30); do
state=$(az containerapp show -n "${{ vars.APP_NAME }}" -g "${{ vars.RESOURCE_GROUP }}" \
--query "properties.latestReadyRevisionName == properties.latestRevisionName" -o tsv)
[ "$state" = "true" ] && echo "The new revision is ready." && exit 0
sleep 10
done
echo "The new revision did not become ready in 5 minutes."; exit 1
- name: Smoke check
env:
SMOKE_API_KEY: ${{ secrets.SMOKE_API_KEY }}
run: scripts/smoke.sh "${{ vars.APP_URL }}" "$SMOKE_API_KEY"
- name: Record the deployment
run: |
{
echo "### ${{ inputs.environment }}"
echo "Image: \`${{ inputs.image }}\`"
echo "Revision: $(az containerapp show -n "${{ vars.APP_NAME }}" -g "${{ vars.RESOURCE_GROUP }}" --query properties.latestRevisionName -o tsv)"
echo "Started by: ${{ github.actor }}, run ${{ github.run_id }}"
} >> "$GITHUB_STEP_SUMMARY"

deploy-to.yml à 92fff66. environment: name est la ligne qui relie cette tâche à tous les éléments de la dernière leçon. Elle apporte les variables, le secret, la politique de branche, la personne responsable de la révision et le sujet OIDC. Le même fichier sert aux deux environnements; seuls vars et secrets changent. Les quatre étapes :

  1. Connectez-vous avec l’identité de l’environnement, comme dans Identifiants et accès.
  2. Déployez avec az containerapp update --image. La commande crée une nouvelle révision sans rien changer d’autre dans l’application : SHOW_DOCS et le secret restent tels quels.
  3. Attendez que la révision la plus récente soit aussi la révision prête la plus récente : 30 essais, espacés de 10 secondes, au plus 5 minutes. Jusque-là, l’ancienne révision garde le trafic, comme dans le cours sur Docker.
  4. Faites une vérification sommaire à l’adresse de l’environnement avec sa propre clé, puis écrivez un résumé.

Amira a enregistré les deux fichiers de flux de travail avec les fichiers d’environnement local dans un commit (enregistrement Git) sur feature/deploy. Elle a ouvert la demande de tirage de feature/deploy vers main : demande de tirage nº 1. Les deux vérifications obligatoires ont réussi, puis elle a fait la fusion (commit de fusion 43de57b). CI s’est exécuté sur main, et sa réussite a démarré l’exécution de Deploy 37664760027.

La première tentative a échoué à la connexion, comme le montre Identifiants et accès. Après la correction, la tentative 2 s’est exécutée. La chronologie (réelle, tirée des tâches et du journal de l’exécution) :

Heure (UTC) Ce qui s’est passé
18 h 10 min 23 s « Find the image » (trouver l’image) a pris l’empreinte sha256:a8af875a… dans le fichier build-info.json de l’exécution de CI (conservé depuis la tentative 1).
18 h 11 min 32 s La tâche de préproduction a démarré. 18 h 11 min 57 s : Azure CLI login succeeds by using OIDC. (la connexion à Azure CLI réussit avec OIDC).
18 h 12 min 18 s az containerapp update a affiché la nouvelle révision ticket-api-staging--0000001.
18 h 12 min 42 s The new revision is ready. (la nouvelle révision est prête), puis les quatre vérifications sommaires ont réussi. La tâche a pris 1 minute 15 secondes.
18 h 12 min 47 s La tâche de production attendait la personne responsable de la révision.
18 h 13 min 4 s Approbation donnée. La tâche de production a démarré avec la même empreinte.
18 h 13 min 29 s Nouvelle révision ticket-api-production--0000001. 18 h 13 min 54 s : prête, et les quatre vérifications ont réussi. La tâche a pris 55 secondes.

Le journal de préproduction (réel, abrégé) :

Attempting Azure CLI login by using OIDC...
Azure CLI login succeeds by using OIDC.
ticket-api-staging--0000001
The new revision is ready.
ok health (200)
ok ready (200)
ok classify with the key (200)
ok classify without a key (401)
La page publique de l'exécution 1 de Deploy (37664760027), sans connexion. Nouvelle exécution déclenchée par usmanahmed99 sur le commit 43de57b; Status : Success (état : réussite); Total duration : 2m 34s (durée totale : 2 minutes 34 secondes). Le graphique de deploy.yml, on : workflow_run, montre trois tâches vertes à la suite : Find the image (trouver l'image, 4 secondes), staging / Deploy to staging (préproduction / déployer en préproduction, 1 minute 15 secondes) avec l'adresse de préproduction dessous, et production / Deploy to production (production / déployer en production, 55 secondes) avec l'adresse de production dessous. En bas, Deployment protection rules (règles de protection du déploiement) : usmanahmed99 a approuvé production avec le commentaire : les vérifications sommaires de préproduction ont réussi pour cette empreinte. Approuvé pour la production (exécution de test du cours).
Le premier déploiement après la correction (réel, capturé sans connexion). La préproduction, puis la production, et l'approbation avec son commentaire font partie du dossier public de l'exécution. L’image est en anglais.

La page montre l’ordre (préproduction avant production), les adresses tirées de environment: url, et l’approbation : qui, pour quel environnement, et pourquoi. Tout le monde peut l’ouvrir. C’est une approbation traçable.

Quand la tâche de production attend, la page de l’exécution montre un bouton Review deployments (réviser les déploiements) aux responsables de la révision. Les étapes :

  1. Ouvrez l’exécution (Actions → Deploy → l’exécution).
  2. Vérifiez la tâche de préproduction : les lignes de vérification sommaire et l’empreinte de l’image dans le résumé. Si une personne a testé la préproduction, lisez son résultat.
  3. Choisissez Review deployments.
  4. Sélectionnez production, puis écrivez un commentaire qui indique ce que vous avez vérifié.
  5. Choisissez Approve and deploy (approuver et déployer). Pour arrêter la mise en production, choisissez Reject (refuser).

L’exécution de test a obtenu l’approbation par l’API REST de GitHub, qui fait la même chose et enregistre le même commentaire. Pour approuver ainsi, vous avez besoin de l’identifiant de l’environnement et de celui de l’exécution. Faites-le seulement dans votre propre dépôt, car cela démarre un déploiement en production :

Terminal (optional, with gh)
gh api repos/<you>/ticket-api-ci/environments --jq '.environments[] | {name, id}'
gh api --method POST repos/<you>/ticket-api-ci/actions/runs/<run-id>/pending_deployments \
-F "environment_ids[]=<production id>" -f state=approved \
-f comment="Staging smoke checks passed for this digest."

L’enregistrement de l’approbation, relu depuis l’exécution (réel) :

Terminal
gh api repos/Nextia-Intelligence/ticket-api-ci/actions/runs/37664760027/approvals --jq '.[] | {state, comment, user: .user.login, environments: [.environments[].name]}'
{"comment":"Staging smoke checks passed for this digest. Approved for production (C19 test run).","environments":["production"],"state":"approved","user":"usmanahmed99"}

Chaque tâche avec environment: crée aussi un enregistrement de déploiement. Le dépôt en comptait 21 à la fin de la journée, par exemple (réel) :

Terminal
gh api repos/Nextia-Intelligence/ticket-api-ci/deployments --jq '.[] | [.environment, .sha[0:7], .created_at] | @tsv'
production 92fff66 2026-10-07T18:18:57Z
staging 92fff66 2026-10-07T18:18:02Z
production 43de57b 2026-10-07T18:12:47Z
staging 43de57b 2026-10-07T18:11:28Z
staging 43de57b 2026-10-07T18:10:28Z

(Les plus récents sont omis.) La première ligne pour 43de57b en préproduction est la tentative 1, qui a échoué. La page des déploiements d’un dépôt exige une connexion. Ce cours montre donc la réponse de l’API plutôt qu’une capture d’écran.

L’image en production a une empreinte, mais les gens parlent de versions. La personne responsable du soutien demande « sommes-nous sur la version 1.1.0? », pas « sommes-nous sur sha256:4eb3db8d? ». Une étiquette Git v1.1.0 nomme le commit. L’image a aussi besoin de la version. Ce doit être la même image, pas une nouvelle construction. Séparer la configuration : une deuxième construction pour la mise en production explique pourquoi.

La deuxième demande de tirage d’Amira, de feature/release-tags vers main (demande de tirage nº 2), a supprimé le déclencheur d’étiquette de ci.yml. Elle a ajouté les notes de version docs/releases/1.1.0.md et ce flux de travail :

.github/workflows/release.yml
# A release: when a version tag (v1.2.0) is pushed, give the image that CI
# already built and tested for that commit its version tag, and create the
# GitHub release. Nothing is built again, so the release is the same digest
# that staging and production run.
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
IMAGE: ghcr.io/${{ github.repository }}
steps:
- uses: actions/checkout@v7
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Tag the tested image with the version
id: tag
run: |
version="${GITHUB_REF_NAME#v}"
source="$IMAGE:sha-${GITHUB_SHA::7}"
docker buildx imagetools inspect "$source" > /dev/null || {
echo "No image $source: CI must build and test this commit first."; exit 1; }
docker buildx imagetools create --tag "$IMAGE:$version" "$source"
digest=$(docker buildx imagetools inspect "$IMAGE:$version" --format '{{json .Manifest}}' | jq -r .digest)
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "digest=$digest" >> "$GITHUB_OUTPUT"
- name: Create the GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
notes="docs/releases/${{ steps.tag.outputs.version }}.md"
[ -f "$notes" ] || { echo "Write the release notes in $notes first."; exit 1; }
{
cat "$notes"
echo
echo "Image: \`$IMAGE@${{ steps.tag.outputs.digest }}\` (tag \`${{ steps.tag.outputs.version }}\`)"
} > notes.md
gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file notes.md

release.yml à 92fff66. docker buildx imagetools create écrit une nouvelle étiquette dans le registre qui pointe vers l’image existante. La commande ne copie aucune couche et ne construit rien. Si CI n’a jamais construit le commit, la publication s’arrête avec No image …: CI must build and test this commit first. (aucune image : CI doit d’abord construire et tester ce commit). Si les notes de version manquent, elle s’arrête aussi.

La fusion de la demande de tirage nº 2 a créé le commit 92fff66. CI l’a construit (sha-92fff66), et l’exécution de Deploy 37665743749 en a fait la promotion. La préproduction a pris 51 secondes, suivie de l’approbation (« Version 1.1.0 : les vérifications sommaires de préproduction ont réussi. Approuvé pour la production. »), puis la production a pris 1 minute 8 secondes. Amira a ensuite étiqueté le commit. Attendez d’abord que l’exécution de CI sur main soit verte, pour que l’image existe :

Terminal (on main)
git switch main
git pull
git tag -a v1.1.0 -m "ticket-api 1.1.0"
git push origin v1.1.0

L’étiquette a démarré l’exécution de Release 37665781129. Elle a pris 8 secondes. Son journal (réel, abrégé) :

Login Succeeded!
#1 [internal] pushing ghcr.io/usmanahmed99/ticket-api-ci
#1 0.000 copying sha256:73311f52b091f4e64e4990f351159f0af4e0735056173260377c21829869dd1b from ghcr.io/usmanahmed99/ticket-api-ci:sha-92fff66 to ghcr.io/usmanahmed99/ticket-api-ci
#1 0.000 copying sha256:34c3ddd916f50832e73a8a30877e3ae872a2c7e2510b352f585f2bb049c86f49 from ghcr.io/usmanahmed99/ticket-api-ci:sha-92fff66 to ghcr.io/usmanahmed99/ticket-api-ci
#1 0.359 pushing sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a to ghcr.io/usmanahmed99/ticket-api-ci:1.1.0
https://github.com/Nextia-Intelligence/ticket-api-ci/releases/tag/v1.1.0

« copying » (copie) signifie ici des pointeurs : les deux manifestes (l’image linux/amd64 et son attestation) reçoivent un deuxième nom. Vérifiez-le vous-même; tout le monde peut lire le paquet public :

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

La sortie (les deux résultats abrégés aux premières lignes) :

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

Une empreinte, trois noms : sha-92fff66, 1.1.0 et l’empreinte elle-même, que la préproduction et la production exécutaient déjà.

La page publique de la version v1.1.0 de Nextia-Intelligence/ticket-api-ci, sans connexion. github-actions l'a publiée; étiquette v1.1.0, commit 92fff66 avec une coche verte. Les notes : ticket-api 1.1.0. La première mise en production par le pipeline. Le code est le même qu'à la fin du cours sur Docker. Changements pour les clients : aucun. Vérifications : analyse statique, 25 tests, pip-audit, vérification sommaire de l'image, vérification sommaire de préproduction. Responsable : Amira. À surveiller après la mise en production : la vérification sommaire de production et les journaux, pendant une heure. Retour en arrière : déployer l'empreinte précédente avec le flux de travail Deploy (Run workflow, exécuter le flux de travail). Image : ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a (étiquette 1.1.0).
La version GitHub créée par release.yml (réelle, capturée sans connexion). La ligne de l'image nomme l'empreinte, donc la version indique exactement ce qui s'exécute en production. L’image est en anglais.

Le parcours simulé fait la même promotion avec deux scripts. promote-local.sh démarre un environnement avec une image, puis exécute le même smoke.sh que le pipeline :

scripts/promote-local.sh
#!/usr/bin/env bash
# Deploy one image to a local environment, then smoke-check it.
# scripts/promote-local.sh staging ghcr.io/<you>/ticket-api-ci@sha256:...
# scripts/promote-local.sh production ghcr.io/<you>/ticket-api-ci@sha256:...
# Needs .env (POSTGRES_PASSWORD) and secrets/<environment>_api_key.txt.
set -euo pipefail
environment=${1:?Give the environment: staging or production}
export IMAGE=${2:?Give the image with its digest}
settings="deploy/local/$environment.env"
[ -f "$settings" ] || { echo "No settings file $settings"; exit 1; }
docker compose -f deploy/local/compose.yaml --env-file "$settings" --env-file .env up -d --wait
port=$(grep '^PORT=' "$settings" | cut -d= -f2)
scripts/smoke.sh "http://127.0.0.1:$port" "$(cat "secrets/${environment}_api_key.txt")"
echo "$environment now runs $IMAGE"
scripts/stop-local.sh
#!/usr/bin/env bash
# Stop a local environment. Add --volumes to delete its database too.
# scripts/stop-local.sh staging [--volumes]
set -euo pipefail
environment=${1:?Give the environment: staging or production}
shift
# compose.yaml needs IMAGE to read the file, but "down" does not use it.
IMAGE=unused docker compose -f deploy/local/compose.yaml \
--env-file "deploy/local/$environment.env" --env-file .env down "$@"

up -d --wait retourne seulement quand chaque conteneur est en santé. La vérification sommaire ne s’exécute donc jamais sur une API à moitié démarrée. set -euo pipefail arrête le script au premier échec. Rendez les deux scripts exécutables : chmod +x scripts/promote-local.sh scripts/stop-local.sh. Git enregistre cela, donc le changement fait partie de votre commit.

L’ordre est celui du pipeline. Vous exécutez la préproduction d’abord. Vous exécutez la production seulement après la réussite de la vérification sommaire de préproduction : cette deuxième commande est votre approbation.

Amira a lancé la préproduction locale avec l’image de la version 1.1.0, sur son Mac avec puce Apple :

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

Erreur courante

no matching manifest for linux/arm64/v8 (aucun manifeste correspondant pour linux/arm64/v8)

Symptôme : le script s’est arrêté avant le démarrage du moindre conteneur (simulation) :

Image ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a Pulling
Image ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a Error no matching manifest for linux/arm64/v8 in the manifest list entries: no match for platform in manifest: not found
Error response from daemon: no matching manifest for linux/arm64/v8 in the manifest list entries: no match for platform in manifest: not found

Cause : la CI construit seulement linux/amd64, le type de processeur du nuage. Docker demande l’image adaptée à son ordinateur, soit linux/arm64/v8 sur une puce Apple. L’image n’en contient aucune. La commande imagetools inspect ci-dessus le montre : un manifeste linux/amd64 et l’attestation (unknown/unknown). Cette erreur ne se produit pas sur un ordinateur Intel ou AMD.

Solution : ne construisez pas une deuxième image arm64 pour votre ordinateur : ce serait encore un autre artefact. Demandez l’image du nuage et laissez Docker Desktop l’émuler. Ajoutez une ligne, avec son commentaire, au service api dans deploy/local/compose.yaml :

deploy/local/compose.yaml (part)
api:
image: ${IMAGE:?Set IMAGE to the image name with its digest}
# The pipeline builds for the cloud's processors. On Apple silicon (arm64),
# Docker Desktop emulates them, so you run the same build as the cloud.
platform: linux/amd64

Le dépôt de référence a ajouté cette ligne plus tard, dans la demande de tirage nº 6, après la version 1.2.0. Vous l’ajoutez maintenant. L’émulation est plus lente, mais l’API de test a démarré et réussi sa vérification de bon fonctionnement en environ 10 secondes.

Pour un vrai service d’IA : un serveur de modèles utilise souvent du code propre au processeur, comme des bibliothèques mathématiques plus rapides. Une construction arm64 pour votre portable peut produire des nombres ou des erreurs différents de la construction amd64 en production. Testez la construction utilisée en production, même si elle est plus lente sur votre ordinateur.

Exemple détaillé (simulation) : préproduction, puis production

Section intitulée « Exemple détaillé (simulation) : préproduction, puis production »

Avec la ligne de plateforme, la même commande donne ceci (simulation) :

Image ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a Pulled
Network ticket-api-staging_default Created
Volume ticket-api-staging_pgdata Created
Container ticket-api-staging-db-1 Created
Container ticket-api-staging-api-1 Created
Container ticket-api-staging-db-1 Healthy
Container ticket-api-staging-api-1 Healthy
ok health (200)
ok ready (200)
ok classify with the key (200)
ok classify without a key (401)
staging now runs ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a

(Les lignes Creating (création), Starting (démarrage) et Waiting (attente) sont omises.) Le téléchargement et le démarrage ont pris 10 secondes au total. Dans le conteneur, Python indique le processeur émulé : docker exec ticket-api-staging-api-1 python -c "import platform; print(platform.machine())" a affiché x86_64.

La préproduction a réussi. Amira approuve donc sa propre production locale : scripts/promote-local.sh production, avec la même image. La sortie contient les mêmes lignes avec ticket-api-production et se termine par production now runs ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d… (la production exécute maintenant cette image). L’image était déjà sur l’ordinateur. Aucun téléchargement n’était donc nécessaire.

Comparons maintenant les deux environnements, qui utilisent la même image :

Terminal (simulated route)
docker inspect --format '{{.Name}} {{.Image}}' ticket-api-staging-api-1 ticket-api-production-api-1
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8081/docs
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8082/docs

La sortie (simulation) :

/ticket-api-staging-api-1 sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a
/ticket-api-production-api-1 sha256:4eb3db8d4ea249fb9b44b27d96deb68aba6da3e3fbc9cf32311a9cbadf4d857a
200
404

L’empreinte est la même que dans le nuage, et la différence reste la même : /docs est activé en préproduction, désactivé en production. Chaque environnement possède aussi ses propres données. La requête de classification de la vérification de bon fonctionnement a écrit une ligne dans chaque base de données. En préproduction, /v1/history affiche seulement les lignes de préproduction, avec des identifiants de requête absents de la production.

Choisissez votre méthode. Vous avez besoin de l’empreinte de votre dernière exécution CI réussie sur main, tirée de build-info.json.

  1. Ajoutez platform: linux/amd64 au service api dans deploy/local/compose.yaml.
  2. Ajoutez scripts/promote-local.sh et scripts/stop-local.sh, puis rendez-les exécutables.
  3. Simulation : exécutez scripts/promote-local.sh staging ghcr.io/<you>/ticket-api-ci@<digest>. Résultat attendu : quatre lignes ok (réussite) et staging now runs … (la préproduction exécute maintenant…).
  4. Simulation : exécutez la même commande avec production. Résultat attendu : les mêmes lignes pour la production.
  5. Simulation : comparez les deux conteneurs api avec docker inspect et /docs, comme dans l’exemple détaillé.
  6. En conditions réelles : ajoutez .github/workflows/deploy.yml et deploy-to.yml. Créez un commit (enregistrement des changements) avec tous les fichiers sur feature/deploy.
  7. En conditions réelles : ouvrez la demande de tirage de feature/deploy vers main. Attendez les deux vérifications obligatoires, puis fusionnez.
  8. En conditions réelles : ouvrez Actions → Deploy (actions → déployer). Attendez la réussite de la préproduction, puis approuvez la production avec un commentaire.
  9. Dans les deux cas : sur une nouvelle branche feature/release-tags, retirez le déclencheur par étiquette de ci.yml. Suivez Séparer la configuration : une deuxième construction pour la mise en production. Créez un commit avec ce changement, docs/releases/1.1.0.md et release.yml. Fusionnez la branche dans main par demande de tirage. (En simulation, la demande de tirage de l’étape 7 ne contient aucun fichier de flux de travail. Fusionnez-la avec les fichiers locaux.)
  10. Dans les deux cas : quand la CI sur main réussit, attribuez l’étiquette v1.1.0 au commit de fusion et poussez l’étiquette. Vérifiez que l’exécution Release (mise en production) réussit. Vérifiez aussi que imagetools inspect donne la même empreinte pour :1.1.0 et :sha-<7>.
Afficher le résultat attendu

Simulation : les deux scripts se terminent par … now runs ghcr.io/<you>/ticket-api-ci@sha256:… (exécute maintenant cette image), avec votre empreinte. docker inspect montre le même identifiant d’image pour les deux conteneurs api. /docs renvoie 200 sur 8081 et 404 sur 8082.

En conditions réelles : l’exécution Deploy montre trois tâches réussies, comme dans la capture d’écran ci-dessus. Le journal de la tâche de préproduction contient The new revision is ready. (la nouvelle révision est prête) et les quatre lignes ok. La page de l’exécution affiche votre approbation et son commentaire. Le chemin /docs de vos applications renvoie 200 en préproduction et 404 en production.

Dans les deux cas : l’exécution Release réussit. Votre dépôt contient la mise en production v1.1.0 avec la ligne d’image. L’étiquette :1.1.0 a la même empreinte que :sha-<7> du commit étiqueté.

Prédisez le résultat, puis essayez en simulation. Arrêtez la base de données de préproduction locale, mais laissez son API fonctionner :

Terminal (simulated route)
docker stop ticket-api-staging-db-1
scripts/smoke.sh http://127.0.0.1:8081 "$(cat secrets/staging_api_key.txt)"

Quelles vérifications échouent? Que ferait le flux de travail Deploy avec la production si cela arrivait dans la tâche de préproduction? Redémarrez la base de données avec docker start ticket-api-staging-db-1.

Afficher la réponse

La sortie (simulation) :

ok health (200)
FAIL ready: expected 200, got 503
ok classify with the key (200)
ok classify without a key (401)

/health répond toujours 200 : le processus fonctionne. /ready vérifie la base de données. Il répond donc 503 pendant son arrêt, et smoke.sh se termine avec 1. La classification réussit encore, car le classificateur n’a pas besoin de la base de données. Une seule vérification échouée suffit. Dans le pipeline, une vérification de bon fonctionnement échouée fait échouer la tâche de préproduction. production contient needs: [resolve, staging]. Cette tâche ne démarre donc jamais, et personne ne reçoit même de demande d’approbation. La production conserve son ancienne révision. Voilà le rôle de la préproduction : arrêter un problème avant qu’il atteigne les utilisateurs. Après docker start, la même vérification de bon fonctionnement a réussi de nouveau pendant le test.

Vérifiez votre compréhension

Où le flux de travail Deploy prend-il l'empreinte à déployer, et pourquoi refuse-t-il une valeur qui ne commence pas par sha256:?

Afficher la réponse

Dans build-info.json, l’artefact de l’exécution CI sur main qui a déclenché l’exécution Deploy. Pour un déploiement manuel, il utilise plutôt l’entrée digest. La CI a construit et testé exactement cette image. Le flux de travail refuse toute valeur qui n’est pas une empreinte. Une étiquette comme 1.1.0 ou latest peut être déplacée vers un autre contenu. Une empreinte désigne toujours les mêmes octets. La préproduction et la production déploient donc ce que la CI a testé, et rien d’autre.

Vérifiez votre compréhension

Prédisez : la vérification de bon fonctionnement en préproduction échoue dans une exécution Deploy. Qu'arrive-t-il à la tâche de production, et que fait fonctionner la production ensuite?

Afficher la réponse

La tâche de préproduction échoue. La tâche de production dépend de celle de préproduction. GitHub l’ignore donc. Elle n’attend jamais d’approbation et ne se connecte jamais. La production continue d’exécuter sa révision actuelle, avec l’ancienne empreinte. La nouvelle empreinte reste en préproduction jusqu’à ce qu’un correctif passe de nouveau par la CI.

Vérifiez votre compréhension

Pourquoi release.yml utilise-t-il docker buildx imagetools create plutôt que docker build pour l'étiquette de version? Comment vérifier le résultat?

Afficher la réponse

imagetools create donne une deuxième étiquette, 1.1.0, à l’image que la CI a déjà construite et testée (sha-92fff66). Cette commande ne construit rien. La mise en production utilise donc le même artefact que la préproduction a vérifié et que la production exécute. Une construction créerait un nouvel artefact que personne n’a testé. Vérifiez les deux étiquettes avec docker buildx imagetools inspect. Les lignes Digest: (empreinte) doivent être identiques (sha256:4eb3db8d… pour 1.1.0).

Tâche : déployez le même artefact dans deux environnements d’exercice, locaux ou hébergés. Prouvez que seuls les paramètres diffèrent.

  1. Prenez l’empreinte d’une exécution CI réussie sur main.
  2. Déployez-la en préproduction et vérifiez-la avec le flux de travail Deploy réel ou scripts/promote-local.sh staging.
  3. Approuvez-la pour la production (en conditions réelles : Review deployments (examiner les déploiements), avec un commentaire). Sinon, exécutez scripts/promote-local.sh production en simulation.
  4. Rassemblez les preuves dans un court tableau. Indiquez l’empreinte, le résultat de la vérification de bon fonctionnement et le statut de /docs pour chaque environnement. Ajoutez qui a approuvé et pourquoi, ainsi que l’emplacement de chaque paramètre et secret.
  5. Faites ensuite l’exercice sans le nuage ou sans le pipeline, selon la méthode que vous n’avez pas utilisée. Lisez les preuves de l’autre méthode ci-dessus. Écrivez une phrase sur ce qu’elle prouve que la vôtre ne prouve pas.
Afficher une réponse modèle

Les preuves de référence pour la mise en production 1.1.0 :

Preuve Préproduction Production
Image (conditions réelles, exécution 37665743749) ghcr.io/usmanahmed99/ticket-api-ci@sha256:4eb3db8d… la même
Image (simulation, docker inspect) sha256:4eb3db8d… la même
Vérification de bon fonctionnement 4 × ok 4 × ok
/docs 200 404
Paramètre différent SHOW_DOCS=true (paramètre de l’application; deploy/local/staging.env) SHOW_DOCS=false
Clé Propre à la préproduction : secret d’application api-key, secret d’environnement GitHub SMOKE_API_KEY; secrets/staging_api_key.txt Propre à la production, aux mêmes endroits
Identité (conditions réelles) id-deploy-staging, Container Apps Contributor (contributeur Container Apps) sur ticket-api-staging id-deploy-production, sur ticket-api-production
Approbation aucune requise usmanahmed99 : « Mise en production 1.1.0 : les vérifications de bon fonctionnement en préproduction ont réussi. Approuvée pour la production. »
Mise en production Étiquette v1.1.0 = sha-92fff66 = sha256:4eb3db8d…

Seule la méthode en conditions réelles prouve ces éléments : connexion sans mot de passe stocké, rôles limités, approbation enregistrée et HTTPS sur une vraie adresse. La simulation prouve sans frais ces éléments : un projet Compose par environnement, sa propre base de données et l’image exacte du nuage, par émulation.

Vérifiez votre compréhension

Rappel : nommez les cinq éléments qui peuvent différer entre préproduction et production dans ce module, et celui qui doit rester identique.

Afficher la réponse

Peuvent différer : les paramètres, comme SHOW_DOCS, le secret, soit la clé d’API de chaque environnement, et l’identité de déploiement avec son rôle. Le stockage des données et les règles peuvent aussi différer : la production exige une personne responsable de la révision. Doit rester identique : l’artefact, soit l’empreinte de l’image, et donc le code et les paquets qu’elle contient.

Vérifiez votre compréhension

Expliquez ou prédisez : un collègue désactive la révision obligatoire en production pour gagner du temps. Une fusion dans main contient un bogue que la vérification de bon fonctionnement ne détecte pas. Qu'est-ce qui change dans le chemin vers la production, et quelles protections fonctionnent encore?

Afficher la réponse

Sans révision obligatoire, la tâche de production démarre dès que la préproduction réussit. L’empreinte atteint la production environ une minute après la préproduction, sans que personne examine d’abord celle-ci. Les autres protections fonctionnent encore : l’ensemble de règles et les vérifications obligatoires avant la fusion, ainsi que l’empreinte construite une seule fois. La vérification de bon fonctionnement en préproduction fonctionne aussi : un échec bloque toujours la production. La politique de branche et l’identité aux droits limités restent également actives. Mais le bogue non détecté par la vérification de bon fonctionnement atteint directement les utilisateurs. L’approbation permet une vérification d’acceptation par les utilisateurs en préproduction. Conservez-la.

Vérifiez votre compréhension

Vérification finale : montrez que la préproduction et la production se comportent différemment uniquement à cause de leurs paramètres, sans modifier le code de l'application.

Afficher la réponse

Montrez la même empreinte dans les deux environnements. Utilisez les lignes IMAGE: (image) du journal Deploy en conditions réelles. En simulation, utilisez docker inspect --format '{{.Name}} {{.Image}}' sur les deux conteneurs api. Montrez ensuite une différence causée par un paramètre : /docs renvoie 200 en préproduction et 404 en production. Puis modifiez seulement le paramètre. Écrivez SHOW_DOCS=false dans deploy/local/staging.env et exécutez scripts/promote-local.sh staging avec la même image. L’exécution de test a affiché Container ticket-api-staging-api-1 Recreate (recréation du conteneur), puis les quatre lignes ok. Ensuite, /docs a renvoyé 404 en préproduction. Avec SHOW_DOCS=true de nouveau, il a renvoyé 200. L’historique de préproduction contenait toujours ses 4 lignes : la base de données n’avait pas été modifiée. Aucun code, aucune image et aucune construction n’ont changé. (En conditions réelles, az containerapp update -n ticket-api-staging -g rg-ticket-api-ci --set-env-vars SHOW_DOCS=false modifie le paramètre de la même façon. Cette commande n’a pas été exécutée pendant le test.)

Récapitulatif

  • Le flux de travail Deploy démarre après la réussite de la CI sur main et prend l’empreinte dans build-info.json. Il la promeut : préproduction, vérification de bon fonctionnement, approbation, puis production avec les mêmes étapes.
  • deploy-to.yml contient les étapes d’un environnement : connexion (OIDC), az containerapp update --image, attente de la révision prête, vérification de bon fonctionnement, résumé. environment: fournit les variables, le secret et les règles.
  • Un échec à n’importe quelle étape arrête l’empreinte là où elle est. La production conserve son ancienne révision.
  • L’approbation est publique et traçable : qui, quel environnement et le commentaire.
  • release.yml attribue l’étiquette de version à l’image testée, sans construction : 1.1.0 = sha-92fff66 = sha256:4eb3db8d….
  • Localement : promote-local.sh staging, puis production. Sur une puce Apple, platform: linux/amd64 exécute l’image du nuage par émulation.

Réinitialisation. Supprimez les environnements locaux, leurs données et l’image :

Terminal
scripts/stop-local.sh staging --volumes
scripts/stop-local.sh production --volumes
docker image rm ghcr.io/<you>/ticket-api-ci@sha256:<digest>

L’exécution de test a affiché Removed (supprimé) pour les deux conteneurs api, les deux conteneurs db, les deux réseaux et les deux volumes. En conditions réelles : le module 4 réutilise les deux applications. Elles ne coûtent presque rien au repos. Si vous arrêtez ici, supprimez tout avec az group delete --name rg-ticket-api-ci --yes --no-wait. Les environnements, variables et mises en production GitHub ne coûtent rien et peuvent rester. Pour commencer le module 4 dans un état connu, utilisez l’instantané end-of-m03. Recréez ensuite les paramètres GitHub de ce module.

Ensuite, nous modifierons le modèle d’IA lui-même. Le pipeline doit décider si un nouveau classificateur est assez bon avant de le transmettre à un environnement.

Leçon suivante : Barrière d'évaluation de l'IA