Le 30 avril 2026, une simple exécution de cdk deploy depuis un ordinateur portable a supprimé tous les services App Runner de notre environnement de dev. Aucune donnée n'a été perdue, personne en dehors de l'équipe ne s'en est aperçu, et les services étaient de nouveau opérationnels en moins d'une heure. Ce fut malgré tout l'incident le plus instructif de l'année, car l'outil a fait exactement ce qu'on lui demandait et la personne qui l'exécutait n'a rien fait qui paraissait incorrect.
Voici le post-mortem, écrit pour les ingénieurs qui utilisent AWS CDK et CloudFormation et qui n'ont jamais rencontré ce mode de défaillance. La plateforme est un système client que nous construisons et exploitons : trois applications Next.js sur App Runner, un GraphQL adossé à Lambda, DynamoDB, Aurora, le tout dans une seule pile CDK par environnement. Nous ne nommons pas le client ; ce qui compte, c'est le mécanisme.
Ce qui s'est passé, étape par étape
Notre pile CDK comportait un indicateur de contexte, hostingReady, introduit lors de la mise en service initiale. Le premier déploiement d'un nouvel environnement doit créer les dépôts ECR avant qu'un service App Runner puisse référencer une image qui s'y trouve. La pile était donc écrite ainsi : si hostingReady vaut false, tout est synthétisé sauf les services App Runner ; si c'est true, ils sont inclus.
Le workflow GitHub Actions passait toujours -c hostingReady=true. La valeur par défaut de l'indicateur était false, ce qui était la valeur « sûre » pour un premier déploiement et la mauvaise valeur pour tous les déploiements suivants.
Un ingénieur a exécuté cdk deploy PlatformStack-dev en local pour pousser une petite modification. Aucun indicateur de contexte. CDK a synthétisé un template sans les trois services App Runner. CloudFormation a comparé le nouveau template avec la pile déployée, a vu trois ressources qui n'étaient plus déclarées, et a fait ce qu'un outil déclaratif est censé faire : il les a supprimées.
La récupération a été simple : relancer le déploiement avec l'indicateur, attendre qu'App Runner récupère les images et passe les contrôles de santé. Les images de conteneurs étaient toujours dans ECR, les bases de données n'avaient pas été touchées, Cognito n'avait pas été touché. Le rayon de l'impact se résumait à « les applications ont été indisponibles pendant environ une heure en dev ».
Pourquoi il s'agit d'une défaillance de conception, pas d'une erreur humaine
La conclusion tentante est : « l'ingénieur aurait dû passer l'indicateur ». Nous avons rejeté cette idée, pour trois raisons.
Premièrement, CDK n'indique pas à votre application quelle commande est en cours d'exécution. Au moment où votre TypeScript s'exécute, cdk synth, cdk diff et cdk deploy sont indiscernables. On ne peut pas écrire « si c'est un deploy, refuser ». Toute défense doit agir au niveau du template ou au niveau de CloudFormation.
Deuxièmement, la valeur par défaut d'un indicateur est une décision de conception. Un indicateur dont la valeur par défaut est destructrice est une arme chargée posée sur la table. Le cas de mise en service ponctuelle aurait dû être l'option à activer explicitement, pas le cas courant.
Troisièmement, le comportement de suppression de CloudFormation est correct et ne changera pas. Une infrastructure déclarative signifie que « le template fait foi ». Si une ressource est absente du template, elle disparaît. La seule question est de savoir si vous avez indiqué à CloudFormation que certaines ressources sont trop importantes pour être supprimées sans résistance.
Les quatre filets de sécurité
Nous avons livré les quatre en l'espace d'une semaine. Ce sont des couches indépendantes ; chacune d'elles aurait, à elle seule, empêché l'incident, et ensemble elles couvrent des cas que les autres ne couvrent pas.
1. Des indicateurs sûrs par défaut
hostingReady a désormais pour valeur par défaut true. Le cas de mise en service, où il faut créer les dépôts ECR avant les services, devient -c skipHosting=true, que personne ne tape par accident. Chaque indicateur de contexte de la pile a été revu avec la même question : « si quelqu'un l'oublie, dans quel sens penche l'erreur ? » Un indicateur oublié ne doit jamais supprimer une ressource.
2. Protection contre la suppression de la pile
new PlatformStack(app, `PlatformStack-${config.stage}`, {
config,
env: { account: config.account, region: config.region },
terminationProtection: true,
});
Cela bloque cdk destroy et le bouton « Delete stack » de la console jusqu'à ce qu'un opérateur le désactive explicitement. Cela n'aurait pas empêché l'incident d'avril à lui seul, car la pile avait été mise à jour, pas supprimée, mais cela ferme la porte voisine. Cela ne coûte rien.
3. Un aspect RETAIN sur les types de ressources critiques
C'est celle qui répond directement à l'incident. Un Aspect CDK parcourt chaque construct de l'arbre après la synthèse et fixe la DeletionPolicy CloudFormation à Retain pour les types de ressources que nous considérons comme critiques :
const PROTECTED_TYPES = new Set([
'AWS::AppRunner::Service',
'AWS::Cognito::UserPool',
'AWS::DynamoDB::Table',
'AWS::RDS::DBCluster',
'AWS::SQS::Queue',
'AWS::S3::Bucket',
'AWS::SecretsManager::Secret',
]);
export class ProtectCriticalResources implements IAspect {
visit(node: IConstruct): void {
if (!CfnResource.isCfnResource(node)) return;
if (!PROTECTED_TYPES.has(node.cfnResourceType)) return;
const current = node.cfnOptions.deletionPolicy;
if (current && current !== CfnDeletionPolicy.RETAIN) return; // respect explicit choices
node.applyRemovalPolicy(RemovalPolicy.RETAIN);
}
}
Aspects.of(stack).add(new ProtectCriticalResources());
Avec Retain, lorsqu'un template cesse de déclarer une ressource, CloudFormation la retire de la comptabilité de la pile mais laisse la ressource AWS réelle continuer à fonctionner. Dans le scénario d'avril, les services auraient continué à servir le trafic, et le déploiement correct suivant aurait nécessité un import plutôt qu'une création. C'est une contrariété, pas une panne.
Deux choix de conception méritent d'être signalés. L'aspect respecte les décisions explicites : si un construct a défini DESTROY intentionnellement (un bucket de dev avec autoDeleteObjects, par exemple), l'aspect n'y touche pas, car passer outre casserait la synthèse ou annulerait silencieusement un choix délibéré. Et l'aspect s'applique à chaque étape, y compris en dev, car le risque d'incident est le même partout où vivent de vrais utilisateurs ou de vraies données de test.
Le compromis : un cdk destroy délibéré laisse désormais des ressources orphelines derrière lui. Le nettoyage consiste en une suppression manuelle par ressource. Nous l'acceptons ; c'est le prix explicite de la sécurité.
4. Les déploiements se font depuis la CI, et la CLI vous le rappelle
La dernière couche est procédurale. Les trois environnements se déploient depuis GitHub Actions via des rôles assumés par OIDC, chacun déclenché par sa propre branche. Un cdk synth ou cdk diff local est acceptable et encouragé. Un cdk deploy local ne l'est pas, et comme CDK ne peut pas le bloquer, l'application affiche une bannière chaque fois qu'elle s'exécute en dehors de la CI :
⚠️ CDK is running outside of CI.
`cdk synth` and `cdk diff` are safe to run locally.
`cdk deploy` from a laptop is the cause of the 2026-04-30 hosting
incident — push to the dev branch and let GitHub Actions deploy.
La bannière est supprimée par CI=true ou par une variable de confirmation explicite. Ce n'est pas un contrôle technique, et nous ne prétendons pas que ça l'est. C'est un rappel au moment précis où un rappel est utile, et il fait référence à l'incident par sa date pour que personne n'ait à demander pourquoi.
Ce que nous vous conseillerions de vérifier dès aujourd'hui
Vous n'avez pas besoin d'avoir vécu cet incident pour en tirer profit. Trois questions à vous poser cette semaine à propos de vos propres piles CDK :
- Lequel de vos indicateurs de contexte ou de vos variables d'environnement supprime une ressource s'il est oublié ? Inversez leurs valeurs par défaut.
- Quelle est la
DeletionPolicyde vos bases de données, user pools et files d'attente ? Exécutezcdk synthet faites un grep sur le template. Si la réponse estDeleteou absente, un aspect d'une seule ligne suffit à corriger cela. - Un ordinateur portable peut-il déployer en production ? Si oui, qu'est-ce qui empêche un mauvais
AWS_PROFILE? L'épinglage du compte dansenvet un chemin de déploiement réservé à la CI coûtent tous les deux peu.
Une leçon supplémentaire tirée d'un second incident, plus modeste, en août : des alias CloudFront et un certificat attachés manuellement ont été effacés par le déploiement CDK suivant, parce que CloudFormation remplace toute la configuration de la distribution. Même cause profonde sous un autre déguisement : tout ce qui n'est pas dans le template n'existe pas. Les politiques Retain protègent les ressources ; elles ne protègent pas les propriétés. La seule défense pour les propriétés consiste à les inscrire dans le code.
Si vous souhaitez un second regard sur votre configuration CDK avant qu'elle ne vous enseigne elle-même cette leçon, contactez-nous.