# Comment nous construisons le cloud et le DevOps : trois comptes, une base CDK, et aucun cluster

Nous exploitons AWS pour nos propres produits et pour une plateforme cliente avec de vrais utilisateurs et de l'argent réel qui y circule. Entre mars et septembre de cette année, nous avons commis la plupart des erreurs qu'une petite équipe peut faire sur AWS et nous les avons consignées dans une série de 29 articles. Voici la version condensée : la configuration avec laquelle nous démarrons désormais chaque projet, à quoi sert chaque élément, et où sont enterrés les cadavres.

En bref : trois comptes sous une seule organisation, chaque ressource en CDK, des conteneurs sans cluster, des déploiements via GitHub sans clé longue durée, une observabilité qui n'appartient à aucun fournisseur, et une revue mensuelle des coûts. Rien de tout cela n'est brillant. Tout est écrit noir sur blanc.

## Les comptes d'abord : dev, staging, production, dès le premier jour

La première chose que nous créons n'est pas un serveur. C'est une AWS Organization avec trois comptes membres : dev, staging et production. Le même code CDK, des paramètres différents, des frontières IAM strictes entre eux. Une erreur en dev ne peut pas toucher les données de production, parce qu'elle ne peut même pas les voir.

Nous avons hérité de trop de plateformes où le « staging » était une seconde base de données sur le serveur de production, et où le dev était l'ordinateur portable de quelqu'un. Corriger cela après le lancement est une migration ; le faire avant le lancement est une affaire d'un après-midi. Nous avons exposé nos arguments dans [dev, staging, prod dès le premier jour](/fr/blog/dev-staging-prod-from-day-one) et montré la structure du code dans [trois comptes AWS, une seule base CDK](/fr/blog/aws-three-accounts-one-cdk-codebase).

Les déploiements atteignent ces comptes depuis GitHub Actions via une fédération OIDC : un rôle par compte, autorisé uniquement pour le bon dépôt et la bonne branche, sans aucune clé d'accès stockée nulle part. Quand un ingénieur part, rien n'a besoin d'être renouvelé. La configuration tient en une centaine de lignes de CDK et nous l'avons publiée dans [rôles de déploiement GitHub OIDC par compte AWS](/fr/blog/github-oidc-deploy-roles-per-aws-account).

<div class="article-figure">
<svg viewBox="0 0 900 290" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Flux de déploiement : une pull request ne construit que ce qui a changé, GitHub Actions endosse un rôle OIDC par compte, et le même code CDK déploie vers les comptes dev, staging et production">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="40" y="34" fill="#f1f3ff" font-size="15" font-weight="700">Une base de code, trois comptes, zéro clé stockée</text>
<rect x="40" y="110" width="140" height="50" rx="8" fill="#151b2e" stroke="#9aa3c7"/>
<text x="110" y="131" text-anchor="middle" fill="#f1f3ff">Pull request</text>
<text x="110" y="148" text-anchor="middle" fill="#9aa3c7">build incrémental</text>
<rect x="220" y="110" width="150" height="50" rx="8" fill="#151b2e" stroke="#7b8cff"/>
<text x="295" y="131" text-anchor="middle" fill="#f1f3ff">GitHub Actions</text>
<text x="295" y="148" text-anchor="middle" fill="#9aa3c7">OIDC → rôle par compte</text>
<rect x="410" y="110" width="130" height="50" rx="8" fill="#151b2e" stroke="#4fffb0"/>
<text x="475" y="131" text-anchor="middle" fill="#f1f3ff">cdk deploy</text>
<text x="475" y="148" text-anchor="middle" fill="#9aa3c7">mêmes stacks</text>
<rect x="600" y="50" width="120" height="44" rx="8" fill="#151b2e" stroke="#ffd166"/>
<text x="660" y="77" text-anchor="middle" fill="#f1f3ff">compte dev</text>
<rect x="600" y="113" width="120" height="44" rx="8" fill="#151b2e" stroke="#ffd166"/>
<text x="660" y="140" text-anchor="middle" fill="#f1f3ff">compte staging</text>
<rect x="600" y="176" width="120" height="44" rx="8" fill="#151b2e" stroke="#ff6b8a"/>
<text x="660" y="203" text-anchor="middle" fill="#f1f3ff">compte prod</text>
<g stroke="#2a3150" stroke-width="2" fill="none">
<line x1="180" y1="135" x2="220" y2="135"/><line x1="370" y1="135" x2="410" y2="135"/>
<line x1="540" y1="135" x2="600" y2="72"/><line x1="540" y1="135" x2="600" y2="135"/><line x1="540" y1="135" x2="600" y2="198"/>
</g>
<text x="780" y="77" fill="#9aa3c7">sur merge</text>
<text x="780" y="133" fill="#9aa3c7">sur merge,</text><text x="780" y="149" fill="#9aa3c7">smoke tests</text>
<text x="780" y="196" fill="#9aa3c7" textLength="110" lengthAdjust="spacingAndGlyphs">validation manuelle,</text><text x="780" y="212" fill="#9aa3c7">même image</text>
<text x="450" y="260" text-anchor="middle" fill="#9aa3c7">Le tag d'image est le SHA git. La production déploie l'image exacte validée par staging, jamais une reconstruction.</text>
</g>
</svg>
</div>

## Tout en CDK, et ce que ça coûte

Chaque ressource est du TypeScript dans une app CDK : comptes, réseau, bases de données, services, DNS, alarmes, IAM. Le même langage que l'application, relu dans les mêmes pull requests, testé avec le même runner. Personne ne clique dans la console ; si quelque chose a été cliqué, c'est du drift, et c'est importé ou supprimé. Nous sommes détendus sur l'outil et stricts sur le principe, comme nous l'avons expliqué dans [l'infrastructure as code ne veut pas dire Terraform](/fr/blog/infrastructure-as-code-does-not-mean-terraform).

CDK a des angles coupants et nous nous sommes coupés sur la plupart d'entre eux. Les stacks CloudFormation ont une limite de cinq cents ressources, et une plateforme avec trois applications, des queues, des fonctions et des alarmes l'atteint plus vite qu'on ne le pense ; nous avons atteint 493 et avons dû scinder les stacks sans rien recréer, ce qui est toute une procédure ([limite de 500 ressources CloudFormation : scinder une stack](/fr/blog/cloudformation-500-resource-limit-split-a-stack)). Un refactoring qui a changé un identifiant logique a un jour supprimé et recréé deux services App Runner en production ; le post-mortem est dans [CloudFormation a supprimé nos services App Runner](/fr/blog/cloudformation-deleted-our-app-runner-services), et depuis, la règle est que tout diff touchant une ressource avec état ou exposée publiquement est relu par deux personnes avant le déploiement.

Les secrets ne vivent jamais dans le template. Mots de passe de base de données, clés d'API et secrets de webhook résident dans Secrets Manager ou SSM et sont injectés au démarrage du conteneur ; le rôle du conteneur ne peut lire que ces ARN précis et rien d'autre. Les compromis sont détaillés dans [secrets en CDK : Secrets Manager, SSM ou rien dans le template](/fr/blog/secrets-in-cdk-secrets-manager-ssm-or-nothing-in-the-template).

## Des conteneurs sans la taxe du cluster

Vous n'avez pas besoin de Kubernetes. Pour des produits en dessous de quelques centaines de requêtes par seconde, ce qui est le cas de la plupart des produits, ECS sur Fargate ou App Runner offrent la même fiabilité avec une fraction de la surface d'exposition. Nous avons expliqué ce raisonnement dans [vous n'avez pas besoin de Kubernetes](/fr/blog/you-do-not-need-kubernetes) et ne l'avons jamais regretté.

App Runner est notre choix par défaut pour les applications web : on pousse une image, on obtient HTTPS et l'auto-scaling, et on le gère depuis CDK comme n'importe quelle autre ressource. Il a de vraies limites, un délai d'expiration de passerelle de 120 secondes et une minute de démarrage à froid parmi elles, et nous savons exactement où elles sont parce que nous les avons toutes rencontrées. La revue honnête est dans [App Runner après six mois en production](/fr/blog/aws-app-runner-review-six-months). ECS sur Fargate est l'étape supérieure quand un service a besoin d'un chemin interne au VPC, de sidecars ou d'un scaling fin.

Les images sont distroless et multi-étapes, ce qui a divisé par deux la taille des images et supprimé toute une catégorie de bruit de CVE dans le scanner ([images Docker pour Node en 2026](/fr/blog/docker-images-for-node-in-2026-distroless-multi-stage)). Les health checks vérifient la connexion à la base de données et la seule dépendance en aval qui compte, pas seulement que le port est ouvert, parce que [les health checks qui mentent](/fr/blog/health-checks-that-lie) sont pires que pas de health check du tout.

## Les données : managées, sauvegardées, et réellement restaurées

PostgreSQL sur RDS ou Aurora, chiffré, dans des sous-réseaux privés, avec des sauvegardes automatisées et une restauration qui a été répétée. Ce dernier point est celui que la plupart des équipes sautent. Nous restaurons un snapshot de production dans le compte staging chaque mois, faisons tourner les smoke tests contre lui et chronométrons l'ensemble, parce que [des sauvegardes jamais restaurées ne sont pas des sauvegardes](/fr/blog/backups-you-never-restored-are-not-backups).

Pour les charges légères avec un trafic en pics, nous apprécions Aurora Serverless v2 ; nous savons aussi que son plancher d'un demi-ACU signifie qu'il n'est jamais gratuit, et nous l'avons évalué honnêtement dans [la revue d'Aurora Serverless v2](/fr/blog/aurora-serverless-v2-review). Les changements de schéma se font en expand-and-contract, jamais une table verrouillée pendant un déploiement ([zéro interruption est une discipline de migration de schéma](/fr/blog/zero-downtime-is-a-schema-migration-discipline)), et le mot de passe de la base de données tourne selon un planning sans que personne ne le remarque ([faire tourner le mot de passe de la base de données sans interruption](/fr/blog/rotating-the-database-password-without-downtime)).

Les queues sont SQS et EventBridge, pas Kafka, jusqu'à ce qu'il y ait une raison ([des queues sans Kafka](/fr/blog/queues-without-kafka-sqs-eventbridge-streaming)). Les workers sont des conteneurs ou des fonctions Lambda selon la durée, et chaque tâche asynchrone est idempotente, parce qu'elle s'exécutera deux fois.

<div class="article-figure">
<svg viewBox="0 0 900 300" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Topologie de production : CloudFront et WAF en frontal, services App Runner ou ECS, RDS PostgreSQL en sous-réseaux privés, workers SQS et Lambda, et un pipeline d'observabilité avec OpenTelemetry exportant logs, traces et alertes">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="40" y="34" fill="#f1f3ff" font-size="15" font-weight="700">Le compte de production, sur une diapositive</text>
<rect x="40" y="100" width="130" height="50" rx="8" fill="#151b2e" stroke="#7b8cff"/>
<text x="105" y="121" text-anchor="middle" fill="#f1f3ff">CloudFront + WAF</text>
<text x="105" y="138" text-anchor="middle" fill="#9aa3c7">règles managées</text>
<rect x="210" y="70" width="140" height="44" rx="8" fill="#151b2e" stroke="#4fffb0"/>
<text x="280" y="97" text-anchor="middle" fill="#f1f3ff">App Runner : web</text>
<rect x="210" y="130" width="140" height="44" rx="8" fill="#151b2e" stroke="#4fffb0"/>
<text x="280" y="157" text-anchor="middle" fill="#f1f3ff">ECS Fargate : API</text>
<rect x="390" y="70" width="130" height="44" rx="8" fill="#151b2e" stroke="#ffd166"/>
<text x="455" y="90" text-anchor="middle" fill="#f1f3ff">RDS PostgreSQL</text>
<text x="455" y="105" text-anchor="middle" fill="#9aa3c7">sous-réseaux privés</text>
<rect x="390" y="130" width="130" height="44" rx="8" fill="#151b2e" stroke="#ffd166"/>
<text x="455" y="150" text-anchor="middle" fill="#f1f3ff">SQS → Lambda</text>
<text x="455" y="165" text-anchor="middle" fill="#9aa3c7">workers idempotents</text>
<rect x="560" y="70" width="140" height="104" rx="8" fill="#151b2e" stroke="#ff6b8a"/>
<text x="630" y="95" text-anchor="middle" fill="#f1f3ff">OpenTelemetry</text>
<text x="630" y="115" text-anchor="middle" fill="#9aa3c7" textLength="130" lengthAdjust="spacingAndGlyphs">logs · traces · métriques</text>
<text x="630" y="135" text-anchor="middle" fill="#9aa3c7">exporté, pas enfermé</text>
<text x="630" y="155" text-anchor="middle" fill="#9aa3c7">alertes → le runbook</text>
<rect x="740" y="70" width="120" height="104" rx="8" fill="#151b2e" stroke="#9aa3c7"/>
<text x="800" y="95" text-anchor="middle" fill="#f1f3ff">Secrets</text>
<text x="800" y="115" text-anchor="middle" fill="#9aa3c7">Secrets Manager</text>
<text x="800" y="135" text-anchor="middle" fill="#9aa3c7" textLength="110" lengthAdjust="spacingAndGlyphs">injectés au démarrage</text>
<text x="800" y="155" text-anchor="middle" fill="#9aa3c7">rotation planifiée</text>
<g stroke="#2a3150" stroke-width="2" fill="none">
<line x1="170" y1="125" x2="210" y2="92"/><line x1="170" y1="125" x2="210" y2="152"/>
<line x1="350" y1="92" x2="390" y2="92"/><line x1="350" y1="152" x2="390" y2="152"/>
<line x1="520" y1="92" x2="560" y2="110"/><line x1="520" y1="152" x2="560" y2="140"/>
</g>
<rect x="40" y="210" width="820" height="40" rx="8" fill="#151b2e" stroke="#2a3150"/>
<text x="450" y="234" text-anchor="middle" fill="#9aa3c7">Pas de NAT gateway sauf si un service privé doit appeler l'extérieur. Pas de Kubernetes. Chaque flèche ci-dessus est une ligne de CDK.</text>
<text x="450" y="280" text-anchor="middle" fill="#9aa3c7">Chaque mois : restaurer un snapshot prod dans staging, relire la facture ligne par ligne, tourner ce qui doit l'être.</text>
</g>
</svg>
</div>

## L'observabilité qui reste la vôtre

Logs, traces et métriques passent par OpenTelemetry vers le backend que le client paie déjà, ou vers une pile auto-hébergée quand il ne veut pas en payer une. L'application ne sait pas laquelle ; elle émet des signaux standards et c'est l'exportateur qui décide. Quand le client a changé de fournisseur d'observabilité cette année, le changement côté application a été de zéro ligne. L'argumentaire est dans [vos logs ne devraient pas savoir quel cloud](/fr/blog/your-logs-should-not-know-which-cloud) et [vos traces ne devraient pas savoir quel cloud](/fr/blog/your-traces-should-not-know-which-cloud), et la réalité tarifaire dans [le coût réel de l'observabilité managée à 100 Go par jour](/fr/blog/real-cost-of-managed-observability-100gb-a-day).

Les alertes sont peu nombreuses et chacune renvoie vers une page du runbook. Le runbook est écrit pour une équipe qui n'a pas d'astreinte, parce que la plupart de nos clients n'en ont pas : quoi vérifier, où cliquer, qui appeler, dans cet ordre ([runbook d'incident pour une équipe sans astreinte](/fr/blog/incident-runbook-for-a-team-without-on-call)). Les alertes sur lesquelles personne n'agit sont supprimées ; [les alertes qui ne devraient pas savoir quel cloud](/fr/blog/alerts-that-do-not-know-which-cloud) explique le routage.

## La facture, revue chaque mois

Le coût est un paramètre de conception, pas une surprise en fin de mois. La ligne la plus coûteuse d'une petite facture AWS est généralement celle dont personne ne se souvient avoir créé : une NAT gateway au service d'un sous-réseau privé qui aurait pu utiliser un VPC endpoint ([NAT gateway : la ligne la plus coûteuse que vous ne voyez pas](/fr/blog/nat-gateway-the-most-expensive-line-you-do-not-see)). Ensuite viennent les bases de données surdimensionnées, les logs conservés indéfiniment, et le calcul que personne n'a engagé sur un savings plan.

Chaque mois, nous passons la facture en revue ligne par ligne avec le client. Pour notre propre société de trois personnes, l'exercice est public, dans [la facture AWS d'une startup de trois personnes](/fr/blog/aws-bill-of-a-three-person-startup), et le raisonnement sur les engagements dans [reserved, savings plans, ou rien à petite échelle](/fr/blog/reserved-savings-plans-or-nothing-at-small-scale). Les politiques de protection des données dans CloudWatch tiennent les données personnelles hors des logs avant que cela ne devienne un problème de conformité ([politiques de protection des données CloudWatch pour les PII](/fr/blog/cloudwatch-data-protection-policies-pii)).

## À quoi ressemble une mission

D'abord une semaine d'évaluation : accès en lecture seule aux comptes, aux pipelines et aux trois dernières factures, et un rapport écrit avec les priorités. Puis la fondation : comptes, réseau, squelette CDK, déploiements OIDC, le premier service qui tourne à la nouvelle façon. Puis la migration, environnement par environnement, en gardant l'ancien chemin actif jusqu'à ce que le nouveau soit vérifié, jamais un grand soir en un seul week-end. Puis soit une passation propre avec formation, soit nous restons pour la revue mensuelle et le support à la demande.

Si votre infrastructure a été construite à la main, ou si vos déploiements sont faits par la seule personne qui ne peut pas partir en vacances, [parlez-nous-en](/contact). La semaine d'évaluation est à périmètre fixe et le rapport est à vous, où que vous décidiez d'aller ensuite.
