Skip to content
Das 500-Ressourcen-Limit von CloudFormation, und wie wir einen Stack ohne Datenverlust aufgeteilt haben
← ← Zurück zu Gedanken Cloud

Das 500-Ressourcen-Limit von CloudFormation, und wie wir einen Stack ohne Datenverlust aufgeteilt haben

cdk deploy scheiterte mit einer Meldung, die wir noch nie gesehen hatten: Template format error: Number of resources, 503, is greater than maximum allowed, 500. Nichts im Diff war groß. Wir hatten eine Lambda-Funktion mit Log-Gruppe und Rolle hinzugefügt, und das reichte, um einen Stack, den wir seit acht Monaten aufbauten, über eine Decke zu schieben, von der wir nichts wussten.

Das Limit ist real, gilt pro Stack und lässt sich nicht per Quota-Erhöhung verhandeln. Dieser Artikel beschreibt, wie wir herausfanden, was im Stack steckte, wo wir geschnitten haben und wie wir eine Datenbank, drei DynamoDB-Tabellen und einen KMS-Schlüssel in einen neuen Stack verschoben haben, ohne eines davon zu löschen.

Wie man unbemerkt auf 500 kommt

Unser CDK-Code deklarierte vielleicht 60 Dinge: ein VPC, einen Aurora-Cluster, drei App-Runner-Services, ein Dutzend Lambdas, ein paar DynamoDB-Tabellen, eine CloudFront-Distribution, eine WAF, einige Secrets. CloudFormation sah 503. Die Differenz ist alles, was CDK für Sie generiert:

npx cdk synth platform-prod --quiet
grep -h '"Type": "AWS::' cdk.out/platform-prod.template.json | sort | uniq -c | sort -rn | head
Ressourcentyp Anzahl Woher er kommt
AWS::IAM::Policy 71 jeder grant*()-Aufruf, eine Policy pro Rolle pro Berechtigungsgruppe
AWS::IAM::Role 48 eine pro Lambda, pro App-Runner-Service, pro Custom Resource
AWS::Lambda::Function 31 unsere 12, plus CDKs Custom-Resource-Provider für Log-Aufbewahrung, Bucket-Deployment, Secret-Rotation des Clusters
AWS::Logs::LogGroup 29 eine pro Funktion, explizit, weil wir die Aufbewahrung setzen
AWS::EC2::* 58 das VPC: Subnetze, Routing-Tabellen, Routen, Zuordnungen, NAT, Endpunkte, Security Groups, Ingress-Regeln
AWS::Lambda::Permission 24 jede Event-Quelle und jede API-Gateway-Route
Custom::* 19 Log-Aufbewahrung, S3-Deployments, Passwort-Rotation des Clusters
Alles andere 223

Ein VPC mit drei Availability Zones sind allein über fünfzig Ressourcen. Jede Lambda ist eine Funktion, eine Rolle, ein bis drei Policies, eine Log-Gruppe, eine Custom Resource für die Aufbewahrung und eine Permission: sieben Ressourcen pro Funktion, die Sie für eine gehalten haben. IAM allein war ein Viertel des Stacks. Nichts davon ist Verschwendung; es ist die richtige Menge Infrastruktur. Nur weit mehr als die Zählung im Kopf.

503 Ressourcen in einem Stack, nach Typ IAM-Policies71 EC2-Netzwerk58 IAM-Rollen48 Lambda-Funktionen31 · nur 12 sind unsere Log-Gruppen29 Lambda-Permissions24 Custom Resources19 · CDK-Helfer Alles andere223

Wo man schneidet

Die Bedingung, die die Aufteilung entscheidet, ist nicht die Anzahl, sondern der Explosionsradius und die Änderungsrate. Dinge, die sich bei jedem Deploy ändern (Lambdas, App-Runner-Image-Tags), sollten keinen Stack mit Dingen teilen, die niemals versehentlich berührt werden dürfen (die Datenbank). Wir hatten das beim Aufbau der drei Konten als Prinzip aufgeschrieben und dann trotzdem alles in einen Stack gepackt, weil ein Stack einfacher ist, bis er es nicht mehr ist.

Die Grenze, die wir gezogen haben, in Deploy-Reihenfolge:

Stack Inhalt Ressourcen Änderungen
network VPC, Subnetze, NAT, Endpunkte, Basis-Security-Groups ~70 fast nie
data Aurora-Cluster, DynamoDB-Tabellen, KMS-Schlüssel, Secrets, Backup-Vault ~60 selten, und vorsichtig
app Lambdas, App-Runner-Services, Queues, Event-Regeln, Rollen ~300 bei jedem Deploy
edge CloudFront, WAF, Zertifikate, Route-53-Einträge ~50 monatlich

Vier Stacks statt einem, der größte bei 300 mit Luft nach oben. Der data-Stack bekommt termination protection und die strengere Deploy-Rolle; der app-Stack ist der, den CI täglich anfasst.

Nested Stacks haben wir erwogen und verworfen. Ein CDK-NestedStack hebt das 500er-Limit auf (jeder Nested Stack hat sein eigenes), aber ein Nested Stack wird als Teil seines Eltern-Stacks ausgerollt, sodass der Explosionsradius überhaupt nicht schrumpft: eine schlechte Änderung an einer Lambda führt trotzdem ein Change Set aus, das die Datenbank enthält. Getrennte Stacks waren der Punkt.

Zustandslose Ressourcen verschieben: einfach verschieben

Lambdas, Rollen, Event-Regeln, App-Runner-Services ohne Zustand: das Construct aus einer Datei ausschneiden, in die andere einfügen, beide deployen. CloudFormation löscht die Ressource aus dem alten Stack und erstellt sie im neuen. Zwei Dinge vorher prüfen:

  • Physische Namen. Eine Lambda mit explizitem functionName kann nicht zweimal existieren, also muss das Löschen vor dem Erstellen passieren, was bedeutet, zuerst den alten Stack zu deployen. Ressourcen ohne explizite Namen bekommen einen neuen generierten Namen und können kurz koexistieren. Wir haben explizite Namen bei allem entfernt, was keine brauchte, und das war das meiste.
  • Dinge, die von außen per ARN auf die Ressource zeigen. Der App-Runner-Service hatte eine Custom Domain, also hätte eine Neuerstellung einen neuen *.awsapprunner.com-Hostnamen, eine DNS-Änderung und eine Zertifikatsvalidierung bedeutet. Wir haben alle drei App-Runner-Services im app-Stack an Ort und Stelle gelassen, wo sie ohnehin hingehörten.

Die Lambdas bekamen neue ARNs. Nichts außerhalb des Stacks referenzierte sie per ARN außer einer EventBridge-Regel im selben Stack, also merkte niemand etwas.

Zustandsbehaftete Ressourcen verschieben: der Import in vier Schritten

Die Datenbank, die Tabellen und der KMS-Schlüssel lassen sich nicht neu erstellen. Sie mussten den Stack wechseln und dabei genau dort bleiben, wo sie waren. CloudFormation unterstützt das über Resource Import, und CDK verpackt es als cdk import. Die Abfolge, pro Ressource:

1 · RETAINalter Stack: removalPolicy= RETAIN, deployennoch ändert sich nichts 2 · verwaisenConstruct löschen,alten Stack deployenRessource lebt weiter, unverwaltet 3 · cdk importgleiches Construct, gleicherphysischer Name, neuer StackCloudFormation adoptiert sie 4 · prüfencdk diff muss leersein, dann deployenwieder verwaltet Zwischen Schritt 2 und 3 existiert die Ressource, aber kein Stack besitzt sie. Beide Schritte direkt nacheinander machen, in einer Sitzung, mit vorher gezogenem Snapshot. Wird Schritt 1 übersprungen, löscht Schritt 2 die Datenbank. Genau deshalb gibt es Schritt 1.

Schritt eins ist der, den Leute überspringen. Ohne zuerst deploytes RemovalPolicy.RETAIN löscht das Entfernen des Constructs in Schritt zwei die Ressource, und für einen Aurora-Cluster ist das bestenfalls ein finaler Snapshot. Wir hatten in dem Jahr bereits einen Löschvorfall hinter uns und kein Interesse an einem zweiten, also machten wir Schritt eins, deployten und prüften in der Konsole, dass DeletionPolicy: Retain auf der Ressource im Template stand, bevor wir irgendetwas anderes taten.

Schritt drei verlangt, dass das Construct im neuen Stack genau die Eigenschaften der bestehenden Ressource erzeugt. Für DynamoDB sind das Tabellenname, Key-Schema und Billing Mode; für Aurora Cluster-Identifier, Engine und ein paar mehr; für KMS die Key-ID. cdk import fragt nach den Identifikatoren, die es nicht ableiten kann, und führt dann ein Import-Change-Set aus. Passt eine Eigenschaft nicht, scheitert der Import sauber und nichts wird geändert, was die gute Art von Scheitern ist.

Schritt vier ist der Beweis. cdk diff auf dem neuen Stack nach dem Import sollte leer sein. Unserer war es beim ersten Mal nicht, beim Aurora-Cluster: wir hatten im neuen Stack deletionProtection: true deklariert, und der echte Cluster hatte es aus, weil der alte Stack es nie gesetzt hatte. Das Diff zeigte es, wir deployten es, und der Cluster war am Ende besser geschützt als zuvor.

Cross-Stack-Referenzen und die Falle darin

Sobald Ressourcen in verschiedenen Stacks leben, braucht der app-Stack das VPC aus network und die Tabellennamen aus data. CDKs Standard ist, das Objekt hinüberzureichen und ein CloudFormation-Export/Import-Paar zu generieren. Es funktioniert, und dann sperrt es Sie ein: ein exportierter Wert kann sich nicht ändern, solange ein anderer Stack ihn importiert, sodass eine VPC-Änderung, die eine exportierte Subnetz-ID verändert, scheitert, bis Sie jeden Konsumenten entfernt haben. Das traf uns an Tag zwei.

Wir sind für alles, was eine Stack-Grenze überquert, auf SSM-Parameter umgestiegen:

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

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

Der Konsument löst den Parameter zur Deploy-Zeit auf; es gibt keinen Export, also ist nichts gesperrt. Der Preis ist, dass CDK die Abhängigkeit nicht mehr kennt, sodass Sie die Stacks selbst in Reihenfolge deployen. Unser Deploy-Skript listet sie: network data app edge. Für das VPC erledigt Vpc.fromLookup per Tag dieselbe Aufgabe, mit dem Lookup in cdk.context.json gecacht.

Der ganze Umzug, gestoppt

Schritt Zeit Ausfall
Inventar und Ziehen der Grenze 2 Stunden keiner
Code in vier Stacks aufgeteilt 3 Stunden keiner
Zustandslose Ressourcen verschoben (Lambdas, Regeln, Rollen) 20 Minuten Deploys ~1 Minute für eventgetriebene Funktionen
RETAIN auf 5 zustandsbehafteten Ressourcen deployt 5 Minuten keiner
Verwaisen und importieren, 5 Ressourcen 40 Minuten keiner
Cross-Stack-Referenzen auf SSM umgestellt 1 Stunde keiner
Leere Diffs auf allen vier Stacks verifiziert 15 Minuten keiner

Ein Nachmittag und ein Vormittag, zuerst in Staging und am nächsten Tag mit Runbook in Produktion. Die eine Minute Ausfall betraf die Lambdas, die Queues konsumieren: zwischen Löschen und Erstellen warteten die Nachrichten. Sie wurden verarbeitet, als die neuen Funktionen hochkamen.

Was wir unserem früheren Ich sagen würden

Vor 300 aufteilen, nicht bei 500. Ressourcen nach jeder nennenswerten Änderung per grep zählen und die Zahl in CI als Warnung bei 350 hinterlegen. Zustandsbehaftete Dinge vom ersten Deploy an in einem Stack halten, der sich so selten wie möglich ändert. Und niemals ein zustandsbehaftetes Construct aus einem Stack entfernen, ohne vorher Retain im deployten Template gesehen zu haben.

Wenn Sie gerade auf den 500er-Fehler starren und der Stack eine Datenbank enthält, sprechen Sie mit uns, bevor Sie den nächsten Deploy starten.