Am 30. April 2026 löschte ein einziger cdk deploy-Lauf von einem Laptop aus sämtliche App-Runner-Services in unserer Dev-Umgebung. Es gingen keine Daten verloren, niemand außerhalb des Teams bemerkte etwas, und die Services liefen innerhalb einer Stunde wieder. Trotzdem war es der lehrreichste Vorfall des Jahres, denn das Tool tat genau das, was ihm aufgetragen wurde, und die Person, die es ausführte, machte nichts, was falsch aussah.
Dies ist das Post-Mortem, geschrieben für Entwickler, die AWS CDK und CloudFormation einsetzen und dieses Fehlerbild noch nie erlebt haben. Die Plattform ist ein Kundensystem, das wir bauen und betreiben: drei Next.js-Apps auf App Runner, Lambda-gestütztes GraphQL, DynamoDB, Aurora, alles in einem CDK-Stack pro Umgebung. Wir nennen den Kunden nicht namentlich; entscheidend ist die Mechanik.
Was im Detail geschah
Unser CDK-Stack hatte ein Context-Flag namens hostingReady, das während der ursprünglichen Inbetriebnahme eingeführt wurde. Beim ersten Deploy einer neuen Umgebung müssen die ECR-Repositories erstellt werden, bevor ein App-Runner-Service ein Image darin referenzieren kann. Der Stack war also so geschrieben: Wenn hostingReady false ist, wird alles außer den App-Runner-Services synthetisiert; wenn es true ist, werden sie einbezogen.
Der GitHub-Actions-Workflow übergab immer -c hostingReady=true. Der Standardwert des Flags war false – der „sichere" Wert für den ersten Deploy und der falsche Wert für jeden Deploy danach.
Ein Entwickler führte lokal cdk deploy PlatformStack-dev aus, um eine kleine Änderung auszurollen. Kein Context-Flag. CDK synthetisierte ein Template ohne die drei App-Runner-Services. CloudFormation verglich das neue Template mit dem bereitgestellten Stack, sah drei Ressourcen, die nicht mehr deklariert waren, und tat, was ein deklaratives Tool tun soll: Es löschte sie.
Die Wiederherstellung war unkompliziert: den Deploy mit gesetztem Flag erneut ausführen, warten, bis App Runner die Images zieht und die Health-Checks bestehen. Die Container-Images lagen weiterhin in ECR, die Datenbanken waren unangetastet, Cognito war unangetastet. Der Schaden beschränkte sich darauf, dass die Apps in Dev etwa eine Stunde lang nicht erreichbar waren.
Warum das ein Design-Fehler ist, kein menschlicher Fehler
Die naheliegende Schlussfolgerung lautet: „Der Entwickler hätte das Flag setzen sollen." Wir haben das aus drei Gründen verworfen.
Erstens teilt CDK Ihrer App nicht mit, welcher Befehl gerade läuft. Wenn Ihr TypeScript ausgeführt wird, sehen cdk synth, cdk diff und cdk deploy identisch aus. Man kann nicht schreiben: „Wenn dies ein Deploy ist, verweigern". Jede Absicherung muss auf Template-Ebene oder auf CloudFormation-Ebene ansetzen.
Zweitens ist der Standardwert eines Flags eine Design-Entscheidung. Ein Flag, dessen Standard der destruktive Wert ist, ist eine geladene Waffe auf dem Tisch. Der einmalige Inbetriebnahme-Fall hätte das Opt-in sein sollen, nicht der Alltagsfall.
Drittens ist das Löschverhalten von CloudFormation korrekt und wird sich nicht ändern. Deklarative Infrastruktur bedeutet: „Das Template ist die Wahrheit". Fehlt eine Ressource im Template, verschwindet sie. Die einzige Frage ist, ob Sie CloudFormation mitgeteilt haben, dass bestimmte Ressourcen zu wichtig sind, um sie ohne Gegenwehr zu löschen.
Die vier Sicherheitsnetze
Wir haben alle vier innerhalb einer Woche ausgeliefert. Es sind unabhängige Schichten; jede einzelne davon hätte den Vorfall verhindert, und zusammen decken sie Fälle ab, die die anderen übersehen.
1. Standardmäßig sichere Flags
hostingReady hat jetzt standardmäßig den Wert true. Der Inbetriebnahme-Fall, bei dem ECR-Repositories vor den Services erstellt werden müssen, ist -c skipHosting=true – das tippt niemand aus Versehen. Jedes Context-Flag im Stack wurde mit derselben Frage überprüft: „Wenn jemand das vergisst, in welche Richtung geht der Fehler?" Ein vergessenes Flag darf niemals eine Ressource entfernen.
2. Terminationsschutz für den Stack
new PlatformStack(app, `PlatformStack-${config.stage}`, {
config,
env: { account: config.account, region: config.region },
terminationProtection: true,
});
Das blockiert cdk destroy und den „Delete stack"-Button in der Konsole, bis ein Operator es explizit deaktiviert. Allein hätte es den April-Vorfall nicht verhindert, weil der Stack aktualisiert und nicht gelöscht wurde, aber es schließt die benachbarte Tür. Es kostet nichts.
3. Ein RETAIN-Aspect für kritische Ressourcentypen
Das ist derjenige, der den Vorfall direkt adressiert. Ein CDK-Aspect durchläuft nach der Synthese jedes Construct im Baum und fixiert die CloudFormation-DeletionPolicy für die von uns als kritisch eingestuften Ressourcentypen auf Retain:
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());
Mit Retain entfernt CloudFormation eine Ressource, sobald ein Template sie nicht mehr deklariert, aus der Buchführung des Stacks, lässt die eigentliche AWS-Ressource aber weiterlaufen. Im April-Szenario hätten die Services weiterhin Traffic bedient, und der nächste korrekte Deploy hätte einen Import statt eines Create benötigt. Das ist ein Ärgernis, kein Ausfall.
Zwei Design-Entscheidungen sind erwähnenswert. Der Aspect respektiert explizite Entscheidungen: Wenn ein Construct absichtlich DESTROY gesetzt hat (zum Beispiel ein Dev-Bucket mit autoDeleteObjects), lässt der Aspect es unangetastet, weil ein Überschreiben entweder den Synth-Vorgang zerstören oder eine bewusste Entscheidung stillschweigend rückgängig machen würde. Und der Aspect gilt in jeder Stage, auch in Dev, weil das Vorfallsrisiko überall dort gleich ist, wo echte Nutzer oder echte Testdaten leben.
Der Kompromiss: Ein bewusster cdk destroy hinterlässt jetzt verwaiste Ressourcen. Die Bereinigung ist ein manuelles Löschen pro Ressource. Das nehmen wir in Kauf; es ist der explizite Preis der Sicherheit.
4. Deploys laufen über CI, und die CLI sagt Ihnen das
Die letzte Schicht ist prozedural. Alle drei Umgebungen deployen aus GitHub Actions über per OIDC übernommene Rollen, jeweils ausgelöst durch ihren eigenen Branch. Ein lokaler cdk synth oder cdk diff ist in Ordnung und erwünscht. Ein lokaler cdk deploy ist es nicht, und da CDK ihn nicht blockieren kann, gibt die App ein Banner aus, sobald sie außerhalb von CI läuft:
⚠️ 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.
Das Banner wird durch CI=true oder eine explizite Bestätigungsvariable unterdrückt. Es ist keine technische Kontrolle, und das behaupten wir auch nicht. Es ist eine Erinnerung genau in dem Moment, in dem eine Erinnerung nützlich ist, und es verweist per Datum auf den Vorfall, sodass niemand fragen muss, warum.
Was wir Ihnen empfehlen würden, heute zu prüfen
Sie müssen diesen Vorfall nicht selbst erlebt haben, um davon zu profitieren. Drei Fragen, die Sie sich diese Woche zu Ihren eigenen CDK-Stacks stellen sollten:
- Welche Ihrer Context-Flags oder Umgebungsvariablen entfernt eine Ressource, wenn sie vergessen wird? Kehren Sie deren Standardwerte um.
- Welche
DeletionPolicyhaben Ihre Datenbanken, User Pools und Queues? Führen Siecdk synthaus und greppen Sie das Template. Lautet die AntwortDeleteoder fehlt sie, ist das mit einem einzeiligen Aspect behoben. - Kann ein Laptop nach Produktion deployen? Falls ja, was verhindert ein falsches
AWS_PROFILE? Account-Pinning inenvund ein reiner CI-Deploy-Pfad sind beide günstig zu haben.
Eine weitere Lektion aus einem zweiten, kleineren Vorfall im August: CloudFront-Aliase und ein von Hand angehängtes Zertifikat wurden vom nächsten CDK-Deploy weggewischt, weil CloudFormation die gesamte Distribution-Konfiguration ersetzt. Dieselbe Grundursache in anderem Gewand: Alles, was nicht im Template steht, existiert nicht. Retain-Policies schützen Ressourcen; sie schützen keine Eigenschaften. Der einzige Schutz für Eigenschaften besteht darin, sie im Code festzuschreiben.
Wenn Sie sich einen zweiten, unabhängigen Blick auf Ihr CDK-Setup wünschen, bevor es Ihnen diese Lektion selbst erteilt, kontaktieren Sie uns.