Skip to content
La limite de 500 ressources de CloudFormation, et comment nous avons découpé un stack sans perdre de données
← ← Retour aux Réflexions Cloud

La limite de 500 ressources de CloudFormation, et comment nous avons découpé un stack sans perdre de données

cdk deploy a échoué avec un message que nous n'avions jamais vu : Template format error: Number of resources, 503, is greater than maximum allowed, 500. Rien dans le diff n'était gros. Nous avions ajouté une fonction Lambda avec un groupe de logs et un rôle, et ça a suffi pour pousser un stack que nous construisions depuis huit mois au-dessus d'un plafond dont nous ignorions l'existence.

La limite est réelle, elle est par stack, et elle n'est pas négociable via une augmentation de quota. Cet article raconte comment nous avons découvert ce qu'il y avait dans le stack, où nous avons coupé, et comment nous avons déplacé une base de données, trois tables DynamoDB et une clé KMS vers un nouveau stack sans en supprimer aucune.

Comment on arrive à 500 sans s'en apercevoir

Notre code CDK déclarait peut-être 60 choses : un VPC, un cluster Aurora, trois services App Runner, une douzaine de Lambdas, quelques tables DynamoDB, une distribution CloudFront, un WAF, quelques secrets. CloudFormation en voyait 503. La différence, c'est tout ce que CDK génère pour vous :

npx cdk synth platform-prod --quiet
grep -h '"Type": "AWS::' cdk.out/platform-prod.template.json | sort | uniq -c | sort -rn | head
Type de ressource Nombre D'où ça vient
AWS::IAM::Policy 71 chaque appel grant*(), une politique par rôle et par groupe de permissions
AWS::IAM::Role 48 un par Lambda, par service App Runner, par custom resource
AWS::Lambda::Function 31 nos 12, plus les providers de custom resources de CDK pour la rétention des logs, le déploiement de buckets, la rotation des secrets du cluster
AWS::Logs::LogGroup 29 un par fonction, explicitement, parce que nous fixons la rétention
AWS::EC2::* 58 le VPC : sous-réseaux, tables de routage, routes, associations, NAT, endpoints, security groups, règles d'entrée
AWS::Lambda::Permission 24 chaque source d'événements et chaque route API Gateway
Custom::* 19 rétention des logs, déploiements S3, rotation du mot de passe du cluster
Tout le reste 223

Un VPC sur trois zones de disponibilité, c'est à lui seul plus de cinquante ressources. Chaque Lambda est une fonction, un rôle, une à trois politiques, un groupe de logs, une custom resource de rétention et une permission : sept ressources par fonction que vous pensiez être une. IAM seul représentait un quart du stack. Rien de tout cela n'est du gaspillage ; c'est la bonne quantité d'infrastructure. C'est juste bien plus que le décompte mental.

503 ressources dans un stack, par type Politiques IAM71 Réseau EC258 Rôles IAM48 Fonctions Lambda31 · seulement 12 sont à nous Groupes de logs29 Permissions Lambda24 Custom resources19 · aides CDK Tout le reste223

Où couper

La contrainte qui décide du découpage n'est pas le nombre, c'est le rayon d'explosion et la fréquence de changement. Les choses qui changent à chaque déploiement (Lambdas, tags d'image App Runner) ne devraient pas partager un stack avec les choses qu'on ne doit jamais toucher par accident (la base de données). Nous l'avions écrit comme principe en mettant en place trois comptes, puis nous avons quand même tout mis dans un seul stack, parce qu'un seul stack est plus simple jusqu'au jour où il ne l'est plus.

La frontière que nous avons tracée, dans l'ordre de déploiement :

Stack Contenu Ressources Changements
network VPC, sous-réseaux, NAT, endpoints, security groups de base ~70 presque jamais
data cluster Aurora, tables DynamoDB, clés KMS, secrets, coffre de sauvegarde ~60 rarement, et avec précaution
app Lambdas, services App Runner, files, règles d'événements, rôles ~300 à chaque déploiement
edge CloudFront, WAF, certificats, enregistrements Route 53 ~50 mensuel

Quatre stacks au lieu d'un, le plus gros à 300 avec de la marge. Le stack data est celui qui reçoit la termination protection et le rôle de déploiement le plus strict ; le stack app est celui que la CI touche chaque jour.

Nous avons envisagé les nested stacks et les avons rejetés. Un NestedStack CDK lève la limite de 500 (chaque nested stack a la sienne), mais un nested stack est déployé comme partie de son parent, donc le rayon d'explosion ne rétrécit pas du tout : un mauvais changement sur une Lambda exécute toujours un change set qui inclut la base de données. Des stacks séparés, c'était tout l'intérêt.

Déplacer les ressources sans état : il suffit de les déplacer

Lambdas, rôles, règles d'événements, services App Runner sans état : couper le construct d'un fichier, le coller dans l'autre, déployer les deux. CloudFormation supprime la ressource de l'ancien stack et la crée dans le nouveau. Deux choses à vérifier avant :

  • Les noms physiques. Une Lambda avec un functionName explicite ne peut pas exister deux fois, donc la suppression doit précéder la création, ce qui signifie déployer l'ancien stack d'abord. Les ressources sans nom explicite reçoivent un nouveau nom généré et peuvent coexister brièvement. Nous avons retiré les noms explicites sur tout ce qui n'en avait pas besoin, c'est-à-dire presque tout.
  • Les choses qui pointent vers la ressource par ARN depuis l'extérieur. Le service App Runner avait un domaine personnalisé, donc le recréer aurait signifié un nouveau nom d'hôte *.awsapprunner.com, un changement DNS et une validation de certificat. Nous avons laissé les trois services App Runner dans le stack app, en place, là où ils devaient être de toute façon.

Les Lambdas ont reçu de nouveaux ARN. Rien en dehors du stack ne les référençait par ARN sauf une règle EventBridge dans le même stack, donc rien ne l'a remarqué.

Déplacer les ressources avec état : l'import en quatre étapes

La base de données, les tables et la clé KMS ne peuvent pas être recréées. Elles devaient changer de stack tout en restant exactement où elles étaient. CloudFormation le permet via l'import de ressources, et CDK l'enveloppe en cdk import. La séquence, par ressource :

1 · RETAINancien stack : removalPolicy= RETAIN, déployerrien ne change encore 2 · orphelinesupprimer le construct,déployer l'ancien stackla ressource reste vivante, non gérée 3 · cdk importmême construct, mêmenom physique, nouveau stackCloudFormation l'adopte 4 · vérifiercdk diff doit êtrevide, puis déployerde nouveau gérée Entre les étapes 2 et 3, la ressource existe mais aucun stack ne la possède. Enchaînez les deux étapes, en une seule séance, avec un snapshot pris avant. Si l'étape 1 est sautée, l'étape 2 supprime la base de données. C'est toute la raison d'être de l'étape 1.

L'étape un est celle que les gens sautent. Sans RemovalPolicy.RETAIN déployé d'abord, retirer le construct à l'étape deux supprime la ressource, et pour un cluster Aurora, c'est au mieux un snapshot final. Nous avions déjà vécu un incident de suppression cette année-là et n'avions aucune envie d'un second, donc nous avons fait l'étape un, déployé, puis vérifié dans la console que DeletionPolicy: Retain était sur la ressource dans le template avant de faire quoi que ce soit d'autre.

L'étape trois exige que le construct du nouveau stack produise exactement les propriétés de la ressource existante. Pour DynamoDB, c'est le nom de la table, le schéma de clés et le mode de facturation ; pour Aurora, l'identifiant du cluster, le moteur et quelques autres ; pour KMS, l'identifiant de la clé. cdk import demande les identifiants qu'il ne peut pas déduire, puis exécute un change set d'import. Si une propriété ne correspond pas, l'import échoue proprement et rien n'est modifié, ce qui est le bon type d'échec.

L'étape quatre est la preuve. cdk diff sur le nouveau stack après l'import doit être vide. Le nôtre ne l'était pas, la première fois, pour le cluster Aurora : nous avions déclaré deletionProtection: true dans le nouveau stack, et le vrai cluster l'avait désactivée, parce que l'ancien stack ne l'avait jamais définie. Le diff l'a montré, nous l'avons déployé, et le cluster a fini mieux protégé qu'avant.

Les références entre stacks, et le piège qu'elles contiennent

Une fois les ressources dans des stacks différents, le stack app a besoin du VPC de network et des noms de tables de data. Le comportement par défaut de CDK est de passer l'objet et de générer une paire export/import CloudFormation. Ça marche, puis ça vous enferme : une valeur exportée ne peut pas changer tant qu'un autre stack l'importe, donc un changement de VPC qui modifie un identifiant de sous-réseau exporté échoue tant que vous n'avez pas retiré chaque consommateur. Nous l'avons subi le deuxième jour.

Nous sommes passés aux paramètres SSM pour tout ce qui traverse une frontière de stack :

// stack data
new ssm.StringParameter(this, 'OrdersTableName', {
  parameterName: `/platform/${env}/orders-table-name`,
  stringValue: ordersTable.tableName,
});

// stack app
const ordersTableName = ssm.StringParameter.valueForStringParameter(this, `/platform/${env}/orders-table-name`);
const ordersTable = dynamodb.Table.fromTableName(this, 'OrdersTable', ordersTableName);

Le consommateur résout le paramètre au déploiement ; il n'y a pas d'export, donc rien n'est verrouillé. Le coût, c'est que CDK ne connaît plus la dépendance, donc vous déployez les stacks dans l'ordre vous-même. Notre script de déploiement les liste : network data app edge. Pour le VPC, Vpc.fromLookup par tag fait le même travail, avec la recherche mise en cache dans cdk.context.json.

Tout le déplacement, chronométré

Étape Temps Interruption
Inventaire et tracé de la frontière 2 heures aucune
Code découpé en quatre stacks 3 heures aucune
Ressources sans état déplacées (Lambdas, règles, rôles) 20 minutes de déploiements ~1 minute pour les fonctions événementielles
RETAIN déployé sur 5 ressources avec état 5 minutes aucune
Orphelinage et import, 5 ressources 40 minutes aucune
Références entre stacks passées sur SSM 1 heure aucune
Vérification des diffs vides sur les quatre stacks 15 minutes aucune

Un après-midi et une matinée, d'abord en staging puis en production le lendemain avec un runbook. La minute d'interruption concernait les Lambdas qui consomment des files : entre suppression et création, les messages ont attendu. Ils ont été traités quand les nouvelles fonctions sont arrivées.

Ce que nous dirions à nos anciens nous

Découpez avant 300, pas à 500. Comptez les ressources avec grep après chaque changement significatif, et mettez le compte en CI comme avertissement à 350. Gardez les choses avec état dans un stack qui change aussi rarement que possible, dès le premier déploiement. Et ne retirez jamais un construct avec état d'un stack sans avoir vu Retain dans le template déployé d'abord.

Si vous regardez l'erreur des 500 en ce moment et que le stack contient une base de données, parlons-en avant de lancer le prochain déploiement.