Skip to content
Trois comptes AWS, une seule base de code CDK : comment nous empêchons dev, staging et prod de se contaminer mutuellement
← ← Retour aux Réflexions Cloud

Trois comptes AWS, une seule base de code CDK : comment nous empêchons dev, staging et prod de se contaminer mutuellement

La plupart des articles AWS de ce blog ont jusqu'ici été des guides : ce que fait un service, comment le mettre en place, ce qu'il coûte. Celui-ci est différent. Il documente un dispositif réel que nous avons construit et que nous exploitons pour un client en 2026 : une plateforme multi-applications (trois apps Next.js, quatre fonctions Lambda, une API GraphQL, DynamoDB, Aurora Serverless v2, S3, Cognito) qui tourne dans trois comptes AWS distincts, un par environnement, tous déployés depuis une seule base de code CDK.

Nous ne nommons volontairement ni le client, ni le produit, ni aucun identifiant de compte. Ce que nous partageons, c'est la structure, les décisions, et les erreurs qui les ont façonnées. Si vous êtes une petite équipe qui a dépassé le stade « un seul compte avec un suffixe -dev sur tout », voici le dispositif que nous recommanderions.

Pourquoi trois comptes et non un seul compte avec trois stacks

La plateforme a démarré en février 2026 dans un seul compte géré à la main. En avril, un audit de sécurité interne a produit une liste de constats qui remontaient tous à la même cause profonde : les ressources dev, staging et prod partageaient des rôles IAM, des clés KMS partagées, des autorisations wildcard partagées (politiques de table du style platform-*), et il n'y avait aucun moyen de dire « ce credential ne peut toucher que dev ».

Un compte AWS est la seule frontière de sécurité dure que fournit AWS. Tout le reste — tags, conventions de nommage, conditions IAM — est une frontière souple qu'un seul wildcard mal placé annule. La décision a donc été la suivante :

  • dev — un compte greenfield, entièrement géré par CDK, démantèlement bon marché autorisé.
  • staging — le compte historique, reprovisionné à partir du même code CDK après une migration contrôlée (mai 2026).
  • prod — un troisième compte dédié, monté à partir de zéro en juin et lancé fin juillet.

Même région dans les trois. Des CIDR VPC non chevauchants (10.50, 10.51, 10.52) pour que le peering reste possible plus tard. Chaque compte a ses propres clés KMS, son propre pool Cognito, ses propres secrets, son propre CloudTrail.

infra/ (CDK, TypeScript) config.ts = source unique de vérité -c stage=dev -c stage=staging -c stage=prod Compte : dev branche dev · DESTROY · MFA off · PITR off Compte : staging branche staging · RETAIN · sandbox IDV Compte : prod branche master · RETAIN · Object Lock 7 ans · Config on Par compte, jamais partagé • Clés KMS • Pool d'utilisateurs Cognito • Entrées Secrets Manager • VPC (10.50 / 10.51 / 10.52) • CloudTrail + GuardDuty • Rôle de déploiement GitHub OIDC • Bootstrap CDK • Balises d'allocation des coûts

Une seule base de code, un seul config.ts

Tout l'intérêt d'avoir trois comptes est que le code est identique et que seule la configuration diffère. Nous l'imposons avec un seul fichier, infra/lib/config.ts, qui est le seul endroit où les environnements sont autorisés à différer. Chaque construct reçoit un objet StageConfig et ne se demande jamais directement « suis-je en prod ? ».

Le point d'entrée ressemble à peu près à ceci :

const stage = app.node.tryGetContext('stage') ?? 'dev';
const config = getStageConfig(stage);  // throws on unknown stage

new PlatformStack(app, `PlatformStack-${config.stage}`, {
  config,
  env: { account: config.account, region: config.region },
  terminationProtection: true,
  tags: { Project: 'platform', Stage: config.stage, ManagedBy: 'cdk' },
});

Deux détails comptent ici. D'abord, le nom du stack porte le stage, donc PlatformStack-dev et PlatformStack-prod ne peuvent jamais être confondus. Ensuite, le compte est fixé dans env : CDK refuse de déployer si les identifiants que vous détenez se résolvent vers un compte différent de celui attendu par le stage. Vous ne pouvez pas déployer prod avec des identifiants dev par accident, et vous ne pouvez pas déployer dev dans le compte prod par accident.

La matrice de durcissement

C'est la partie qu'on afficherait au mur. Plutôt que de disséminer if (stage === 'prod') dans toute la base de code, chaque stage renvoie un bloc de configuration typé, et les différences se lisent comme un tableau :

Paramètre dev staging prod
Politique de suppression DESTROY RETAIN RETAIN
Point-in-time recovery DynamoDB désactivé activé activé
KMS géré par le client (DynamoDB) activé activé activé
DynamoDB Streams sur les tables sensibles désactivé activé activé
Cycle de vie du coffre de documents (S3) 30 jours 30 jours 7 ans + Object Lock
Cognito Advanced Security audit audit audit (enforce est une décision liée au lancement)
Journalisation complète des requêtes AppSync activé activé désactivé
Traçage X-Ray activé activé activé
WAF sur CloudFront + AppSync activé activé activé
AWS Config désactivé activé activé
CloudTrail + GuardDuty activé activé activé
Capacité Aurora Serverless v2 0.5–4 ACU 0.5–4 ACU 2–16 ACU
Rétention des sauvegardes Aurora 7 jours 7 jours 30 jours
Concurrence réservée de la Lambda de redirection 10 50 200
Vérification d'identité tierce désactivé (mock) sandbox production

En lisant ce tableau, on comprend immédiatement ce que « dev » signifie pour nous : bon marché, jetable, mais avec la même forme que prod. Le chiffrement KMS est activé partout, car l'activer plus tard signifierait recréer les tables. Le WAF est activé partout, car une règle WAF qui n'existe qu'en prod est une règle que personne n'a testée.

Le bloc dev porte aussi une ligne dont nous sommes fiers : une allowlist d'e-mails fixée à une seule adresse puits qui ne correspond jamais. Dev contient des copies de véritables enregistrements utilisateurs, donc le transport d'e-mails abandonne chaque message sortant. C'est une garantie produit exprimée dans la configuration d'infrastructure, pas un commentaire dans un README.

Une branche vers un compte, pas un ordinateur portable vers un compte

Chaque compte est alimenté par exactement une branche Git : dev → compte dev, staging → compte staging, master → compte prod. La promotion vers prod est un merge fast-forward, jamais un rebase, de sorte que le SHA de commit testé sur dev est le même SHA qui est livré en prod.

Côté CI/CD, nous utilisons le fournisseur OIDC de GitHub au lieu de clés d'accès à longue durée de vie. Chaque compte dispose de deux rôles IAM :

  • github-actions-cdk-diff — lecture seule, assumable uniquement depuis les exécutions de pull request. Il exécute cdk diff afin que les relecteurs voient les changements d'infrastructure avant la fusion.
  • github-actions-cdk-deploy — assumable uniquement depuis les push sur la branche de ce compte. Il ne détient pas lui-même de larges pouvoirs IAM ; il délègue aux rôles de bootstrap CDK. Élargir ce que CDK peut déployer passe par cdk bootstrap, pas par la modification d'une policy.

La trust policy fixe le dépôt et la ref exacte, de sorte qu'un fork ou une branche de fonctionnalité ne peut pas assumer le rôle de déploiement.

Nous avons aussi appris, à nos dépens, que cdk deploy depuis un ordinateur portable est un bug, pas une fonctionnalité. Un déploiement local sans les bons flags de contexte a un jour synthétisé un template auquel manquaient les services App Runner, et CloudFormation les a consciencieusement supprimés. Cet incident a eu son propre article ; en résumé, la protection contre la terminaison, un aspect RETAIN sur les types de ressources critiques, et une bannière bien visible quand CDK s'exécute en dehors de la CI font désormais partie de la base de code.

Pull request GitHub OIDC github-actions-cdk-diff lecture seule · cdk diff push dev GitHub OIDC github-actions-cdk-deploy push uniquement · délègue au bootstrap CDK cdk deploy Compte: dev push staging GitHub OIDC github-actions-cdk-deploy push uniquement · délègue au bootstrap CDK cdk deploy Compte: staging push master GitHub OIDC github-actions-cdk-deploy push uniquement · délègue au bootstrap CDK cdk deploy Compte: prod Trust policy = ce dépôt + cette ref exacte. Zéro clé d'accès de longue durée.

Les tags permettent de savoir ce que ça coûte

Chaque ressource, dans chaque compte, porte trois tags : Project, Stage, ManagedBy. Avec les tags d'allocation des coûts activés dans Billing, la facture mensuelle se répartit proprement par environnement. Les chiffres que nous pouvons partager sans rompre la confidentialité :

  • Maintenir Aurora Serverless v2 à un plancher de 0.5 ACU plutôt que l'auto-pause coûte environ 30 dollars par mois et par environnement. Nous le payons aussi sur dev, car l'auto-pause imposait un cold start de 15–30 secondes sur le parcours d'inscription.
  • Le compte dev est de loin le moins cher des trois, principalement grâce aux politiques DESTROY, à l'absence d'AWS Config, et aux instances App Runner de taille minimale.
  • Le plancher fixe de prod est dominé par deux instances App Runner chaudes pour l'application publique, le plancher Aurora, et le WAF.

Sans les tags, « combien nous coûte staging » est une question dont la réponse prend un après-midi. Avec eux, c'est un filtre dans Cost Explorer.

Ce qui a mal tourné en chemin

Un compte-rendu honnête doit inclure les bosses.

La limite de 500 ressources de CloudFormation. Un stack par compte était la conception d'origine. En août, le stack prod se synthétisait à exactement 500 ressources et dev à 499. Ajouter une seule Lambda cassait cdk synth. Nous avons scindé l'observabilité (politiques de protection des données de logs, metric filters, alarms, SNS) dans son propre stack, puis déplacé plus tard une application entière dans un autre. La leçon : concevoir dès le premier jour pour plusieurs stacks par compte, avec des exports explicites, et ne pas laisser un seul stack dépasser environ 350 ressources.

Le drift meurt au prochain déploiement. Des alias CloudFront et un certificat ACM avaient été attachés à la main à une distribution en juillet. Le déploiement CDK d'août a remplacé l'intégralité du DistributionConfig et les a supprimés silencieusement. Tout ce qui n'est pas dans le code n'existe pas. Nous conservons désormais les alias et les ARN de certificat dans la config du stage.

L'environnement au moment du build n'est pas l'environnement au moment de l'exécution. Next.js intègre les variables NEXT_PUBLIC_* au moment du build. Les variables d'environnement à l'exécution sur le conteneur ne peuvent plus les modifier. Notre script de déploiement lit désormais la configuration de build à partir du template synthétisé et la transmet à CodeBuild à chaque build, de sorte qu'une variable publique modifiée atteint le navigateur dès le premier déploiement plutôt qu'au second.

La limite de débit qui a bloqué toute l'équipe. La règle de débit par IP du WAF était fixée à 10 000 requêtes par 5 minutes. L'équipe QA du client se trouve derrière une seule adresse NAT de bureau. Une session de test intensive a déclenché la règle, et chaque requête depuis le bureau a reçu un 403 pendant cinq minutes. Elle est désormais à 30 000, et c'est dans config.ts, pas dans la console.

Le referions-nous ?

Oui, et plus tôt. Le coût de trois comptes est réel mais faible : trois bootstraps CDK, trois rôles OIDC, trois jeux de secrets à renseigner, et un seul fichier de config qui doit rester honnête. Le coût de ne pas les avoir est la liste de constats d'audit par laquelle nous avons commencé, plus l'inquiétude permanente qu'un script dev avec une autorisation wildcard puisse toucher des données de production.

Si vous mettez cela en place pour la première fois, l'ordre que nous recommandons est : écrire d'abord config.ts avec la matrice de durcissement, même si la moitié des paramètres ne sont pas encore implémentés ; fixer les comptes dans env et activer la protection contre la terminaison avant le premier déploiement ; câbler OIDC avant de donner à quiconque des identifiants de déploiement ; et scinder en plusieurs stacks avant d'en avoir besoin.

Besoin d'aide pour structurer vos propres comptes AWS et votre code CDK de cette manière ? Parlez-nous — nous l'avons fait une fois à la dure pour que vous n'ayez pas à le faire.