Skip to content
Même tag, aucun déploiement : pourquoi cdk deploy avec une image inchangée ne fait rien, et le script de 300 lignes que nous avons écrit autour
← ← Retour aux Réflexions Cloud

Même tag, aucun déploiement : pourquoi cdk deploy avec une image inchangée ne fait rien, et le script de 300 lignes que nous avons écrit autour

Deux histoires sur le même pipeline de déploiement, racontées ensemble parce que la seconde existe à cause de la première.

La première est un bug qui n'en est pas un : nous avons reconstruit une image de conteneur, nous l'avons poussée sous le même tag, nous avons lancé cdk deploy, nous l'avons vu annoncer no changes, et nous avons passé une heure à comprendre pourquoi la production faisait encore tourner l'ancien code. La seconde est ce qui s'est passé quand notre CI a été indisponible un moment, pour des raisons de facturation hors de notre contrôle, et qu'il nous fallait un moyen de déployer depuis un portable plus sûr que celui par lequel nous avions un jour supprimé trois services depuis un portable.

Première partie : CloudFormation compare des chaînes, pas des octets

Le service App Runner est une ressource CloudFormation, et l'image qu'il exécute est une propriété de type chaîne : ImageIdentifier: 123456.dkr.ecr.us-east-1.amazonaws.com/api:abc1234. Quand vous lancez cdk deploy, CloudFormation compare le nouveau template au dernier qu'il a appliqué. Si la chaîne est identique, il n'y a pas de diff, pas de mise à jour, et App Runner continue de servir ce qu'il a tiré la dernière fois.

C'est un comportement correct, et il mord dans exactement une situation : le tag est le même mais l'image derrière ne l'est pas. Nous l'avons rencontré en reconstruisant le même commit avec un argument de build modifié. Même SHA git, même tag api:abc1234, des octets différents dans ECR, et CloudFormation, correctement, n'a rien vu à faire. Le tag du registre pointait maintenant vers un nouveau digest ; le service tournait encore sur l'ancien ; rien nulle part n'était faux, sauf notre attente.

épinglage par tag tag ECR api:abc1234sha256:1111 → rebuild → sha256:2222 templateImageIdentifier: …/api:abc1234 aucun diffmême chaîne que la dernière fois App Runnerexécute toujours sha256:1111 épinglage par digest script de déploiementrésout tag → sha256:2222 templateImageIdentifier: …/api@sha256:2222 diffle digest a changé App Runnerdéploie sha256:2222

Il y a deux solutions honnêtes. La première est une règle : chaque déploiement fait avancer le SHA. Si vous voulez une image différente, commitez quelque chose. C'est simple, c'est ce que nous avons fait un moment, et ça échoue dès que quelqu'un doit reconstruire une image pour une raison qui n'implique pas de changement de code, comme un correctif de sécurité de l'image de base.

La seconde est d'épingler par digest. Après le push de l'image, le script de déploiement demande à ECR vers quoi le tag se résout et passe le digest, pas le tag, dans le template :

digest=$(aws ecr describe-images --repository-name api --image-ids imageTag="$TAG" \
  --query 'imageDetails[0].imageDigest' --output text)
npx cdk deploy platform-prod --context apiImage="$REPO/api@$digest"
// dans le stack
imageRepository: { imageIdentifier: this.node.tryGetContext('apiImage'), imageRepositoryType: 'ECR' }

Maintenant le template change chaque fois que les octets changent, et seulement à ce moment-là. Un rebuild du même commit avec un contenu différent est un vrai déploiement. Une relance avec un contenu identique est un vrai no-op. Le comportement de CloudFormation n'a pas changé ; nous lui avons simplement donné la chaîne qui signifie réellement « l'image ».

Le digest apparaît dans la console CloudFormation et dans cdk diff comme un hash illisible de 64 caractères. Nous affichons tag → digest dans la sortie du déploiement pour qu'un humain puisse encore dire ce qui part.

Deuxième partie : déployer depuis un portable, en sécurité

La CI a disparu un moment. La plateforme avait toujours besoin de déploiements. Nous avions déjà appris, à prix fort, que cdk deploy depuis la machine d'un développeur avec le mauvais contexte supprime des ressources de production, donc « lance-le en local » n'était pas une option que nous accepterions à nouveau. L'alternative était un script dont le rôle est de faire qu'un déploiement depuis un portable se comporte comme un déploiement CI : mêmes entrées, mêmes garde-fous, aucun moyen de livrer ce qui n'est pas dans git.

Il a grandi jusqu'à environ 300 lignes de bash. Voici ce que font ces lignes, dans l'ordre.

1 · preflightarbre sale ? mauvaise branche ? pas de --yes ? → exit 2 · worktree @ origin/mainjamais la copie de travail 3 · git archive → S3exactement les octets que voit CodeBuild 4 · 3 × CodeBuilden parallèle · ~3 min 5 · tags → digestsaffichés pour l'humain 6 · cdk diff, confirmertoute suppression → stop et lecture 7 · cdk deployles digests en contexte 8 · vérifier + notifiersanté · SHA · grep du bundle Un déploiement depuis un portable et un déploiement CI exécutent les mêmes étapes sur les mêmes octets. Le portable a juste un humain à l'étape 6.

Preflight. Le script refuse de s'exécuter si l'arbre de travail a des changements non commités, si --env prod est donné sans --yes, si la branche courante n'est pas main pour un déploiement de prod, ou si l'identité AWS qu'il s'apprête à utiliser n'est pas le rôle de déploiement attendu pour ce compte. Chaque refus affiche une phrase disant quoi faire. Environ 40 lignes, et les 40 les plus précieuses du fichier.

Worktree. Il crée un git worktree détaché à origin/main (après un git fetch) dans un répertoire temporaire et fait tout depuis là. Quoi qu'il y ait dans la copie de travail du développeur, en stash, à moitié édité ou expérimental, ça ne peut pas partir. Le SHA de ce worktree est l'identifiant de release pour tout ce qui suit.

Archive vers S3. git archive du worktree, envoyé dans un bucket de build sous le SHA. CodeBuild construit depuis cette archive, pas depuis une connexion GitHub, donc l'entrée du build est les mêmes octets qu'elle soit déclenchée par la CI ou par le script.

Trois builds en parallèle. Un projet CodeBuild par application, lancés ensemble, surveillés ensemble. Construire trois images Next.js sur le Docker d'un portable prendrait huit minutes et chaufferait la pièce ; sur trois instances CodeBuild, ça en prend trois. Les projets sont les mêmes que ceux qu'utilise la CI.

Digests. Comme dans la première partie. Le script résout chaque tag et affiche un tableau app → tag → digest.

Diff et confirmation. cdk diff contre l'environnement cible, affiché en entier. Si le diff contient une suppression de ressource, le script la met en évidence et exige que l'opérateur tape le nom du stack pour continuer, pas juste y. C'est l'étape qui aurait empêché l'incident.

Déploiement. cdk deploy avec les digests en contexte et --require-approval never, parce que l'approbation a déjà eu lieu à l'étape précédente, avec une meilleure information que celle que donne l'invite propre de CDK.

Vérifier et notifier. Frapper la route de santé jusqu'à ce qu'elle rapporte le nouveau SHA, grepper le bundle client pour le même SHA, et poster un message sur le canal de l'équipe avec l'environnement, le SHA, le nom de l'opérateur et le résumé du diff. Si la vérification échoue, le script le dit bruyamment et sort avec un code non nul ; il ne revient pas en arrière automatiquement, parce qu'une vérification échouée signifie généralement « regarde », pas « annule ».

À quoi ça ressemble à l'usage

./scripts/deploy.sh --env staging
./scripts/deploy.sh --env prod --yes
./scripts/deploy.sh --env prod --dry-run     # synth + diff, pas de build, pas de déploiement

Trois drapeaux. Tout le reste est décidé par le script à partir de git et du compte. Il n'y a aucun moyen de passer une image personnalisée, une branche personnalisée ou une valeur de contexte personnalisée, volontairement.

Le réécririons-nous ?

Oui, et nous l'écririons dès le premier jour plutôt qu'après l'incident et la panne de CI. Non parce que les déploiements depuis un portable sont bons, mais parce que le script est le pipeline. Quand la CI est revenue, son workflow est devenu : checkout, assumer le rôle OIDC, lancer ./scripts/deploy.sh --env prod --yes. Un seul chemin de code, exercé depuis les deux endroits, avec les garde-fous dans le code plutôt que dans un fichier YAML qui ne tourne que sur la machine de quelqu'un d'autre.

Les 300 lignes, c'est long pour un script shell. Elles ont remplacé une page de runbook qui disait « faites attention », ce qui est plus court et ne fonctionne pas.

Si votre déploiement est un cdk deploy que quelqu'un lance d'où il se trouve, nous pouvons vous aider à le rendre ennuyeux.