Skip to content
Drei AWS-Konten, eine CDK-Codebasis: wie wir verhindern, dass sich Dev, Staging und Prod gegenseitig kontaminieren
← ← Zurück zu Gedanken Cloud

Drei AWS-Konten, eine CDK-Codebasis: wie wir verhindern, dass sich Dev, Staging und Prod gegenseitig kontaminieren

Die meisten AWS-Artikel in diesem Blog waren bisher Anleitungen: was ein Dienst macht, wie man ihn einrichtet, was er kostet. Dieser Artikel ist anders. Er dokumentiert ein reales Setup, das wir 2026 für einen Kunden gebaut haben und betreiben: eine Multi-App-Plattform (drei Next.js-Apps, vier Lambda-Funktionen, eine GraphQL-API, DynamoDB, Aurora Serverless v2, S3, Cognito), die in drei getrennten AWS-Konten läuft, eines pro Umgebung, alle bereitgestellt aus einer einzigen CDK-Codebasis.

Wir nennen bewusst weder den Kunden noch das Produkt noch irgendwelche Konto-IDs. Was wir teilen, sind die Struktur, die Entscheidungen und die Fehler, die sie geprägt haben. Wenn Sie ein kleines Team sind, das „ein Konto mit einem -dev-Suffix an allem" hinter sich gelassen hat, ist dies das Setup, das wir empfehlen würden.

Warum drei Konten und nicht ein Konto mit drei Stacks

Die Plattform startete im Februar 2026 in einem Konto, das von Hand verwaltet wurde. Bis April ergab ein internes Sicherheitsaudit eine Liste von Befunden, die alle auf dieselbe Grundursache zurückgingen: Dev-, Staging- und Prod-Ressourcen teilten sich IAM-Rollen, gemeinsame KMS-Schlüssel, gemeinsame Wildcard-Berechtigungen (Tabellenrichtlinien im Stil von platform-*), und es gab keine Möglichkeit zu sagen: „Dieses Credential darf nur Dev anfassen."

Ein AWS-Konto ist die einzige harte Sicherheitsgrenze, die AWS bietet. Alles andere — Tags, Namenskonventionen, IAM-Bedingungen — ist eine weiche Grenze, die ein einziger falscher Wildcard-Eintrag aufhebt. Die Entscheidung fiel also so aus:

  • dev — ein Greenfield-Konto, vollständig CDK-verwaltet, günstiger Abbau erlaubt.
  • staging — das Legacy-Konto, nach einer kontrollierten Migration (Mai 2026) aus demselben CDK-Code neu bereitgestellt.
  • prod — ein drittes, dediziertes Konto, im Juni von null aufgebaut und Ende Juli live geschaltet.

Dieselbe Region in allen dreien. Nicht überlappende VPC-CIDRs (10.50, 10.51, 10.52), damit Peering später möglich bleibt. Jedes Konto hat seine eigenen KMS-Schlüssel, seinen eigenen Cognito-Pool, seine eigenen Secrets, sein eigenes CloudTrail.

infra/ (CDK, TypeScript) config.ts = einzige Quelle der Wahrheit -c stage=dev -c stage=staging -c stage=prod Konto: dev Branch dev · DESTROY · MFA aus · PITR aus Konto: staging Branch staging · RETAIN · IDV-Sandbox Konto: prod Branch master · RETAIN · Object Lock 7 J. · Config ein Pro Konto, nie geteilt • KMS-Schlüssel • Cognito-User-Pool • Secrets-Manager-Einträge • VPC (10.50 / 10.51 / 10.52) • CloudTrail + GuardDuty • GitHub-OIDC-Deploy-Rolle • CDK-Bootstrap • Kostenzuordnungs-Tags

Eine Codebasis, eine config.ts

Der ganze Sinn von drei Konten besteht darin, dass der Code identisch ist und sich nur die Konfiguration unterscheidet. Wir erzwingen das mit einer einzigen Datei, infra/lib/config.ts, die der einzige Ort ist, an dem sich Umgebungen unterscheiden dürfen. Jedes Construct erhält ein StageConfig-Objekt und fragt nie direkt „Bin ich in Prod?".

Der Einstiegspunkt sieht ungefähr so aus:

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' },
});

Zwei Details sind hier wichtig. Erstens trägt der Stack-Name die Stage, sodass PlatformStack-dev und PlatformStack-prod nie verwechselt werden können. Zweitens ist das Konto in env fest verankert: CDK verweigert das Deployment, wenn die Credentials, die Sie gerade verwenden, zu einem anderen Konto aufgelöst werden als dem, das die Stage erwartet. Sie können Prod nicht versehentlich mit Dev-Credentials deployen, und Sie können Dev nicht versehentlich in das Prod-Konto deployen.

Die Hardening-Matrix

Das ist der Teil, den wir an die Wand hängen würden. Statt if (stage === 'prod') über die gesamte Codebasis zu verstreuen, liefert jede Stage einen typisierten Konfigurationsblock, und die Unterschiede lesen sich wie eine Tabelle:

Einstellung dev staging prod
Removal Policy DESTROY RETAIN RETAIN
DynamoDB Point-in-Time Recovery aus ein ein
DynamoDB kundenverwaltetes KMS ein ein ein
DynamoDB Streams auf sensiblen Tabellen aus ein ein
Dokumententresor (S3) Lifecycle 30 Tage 30 Tage 7 Jahre + Object Lock
Cognito Advanced Security audit audit audit (enforce ist eine Launch-Entscheidung)
AppSync vollständiges Request-Logging ein ein aus
X-Ray Tracing ein ein ein
WAF auf CloudFront + AppSync ein ein ein
AWS Config aus ein ein
CloudTrail + GuardDuty ein ein ein
Aurora Serverless v2 Kapazität 0.5–4 ACU 0.5–4 ACU 2–16 ACU
Aurora Backup-Aufbewahrung 7 Tage 7 Tage 30 Tage
Redirect-Lambda Reserved Concurrency 10 50 200
Identitätsprüfung durch Dritte aus (mock) sandbox production

Beim Lesen dieser Tabelle sieht man sofort, was „dev" für uns bedeutet: billig, wegwerfbar, aber mit derselben Form wie Prod. KMS-Verschlüsselung ist überall an, weil sie später einzuschalten bedeuten würde, Tabellen neu anzulegen. WAF ist überall an, weil eine WAF-Regel, die nur in Prod existiert, eine Regel ist, die niemand getestet hat.

Der Dev-Block enthält außerdem eine Zeile, auf die wir stolz sind: eine E-Mail-Allowlist, die auf eine einzige, niemals passende Sink-Adresse gesetzt ist. Dev enthält Kopien echter Benutzerdatensätze, daher verwirft der E-Mail-Transport jede ausgehende Nachricht. Das ist eine Produktgarantie, die in der Infrastrukturkonfiguration ausgedrückt wird, nicht ein Kommentar in einer README.

Branch zu Konto, nicht Laptop zu Konto

Jedes Konto wird von genau einem Git-Branch gespeist: dev → Dev-Konto, staging → Staging-Konto, master → Prod-Konto. Die Beförderung nach Prod ist ein Fast-Forward-Merge, nie ein Rebase, sodass der Commit-SHA, der auf Dev getestet wurde, derselbe SHA ist, der nach Prod ausgeliefert wird.

Auf der CI/CD-Seite wird der OIDC-Provider von GitHub anstelle von langlebigen Access Keys verwendet. Jedes Konto hat zwei IAM-Rollen:

  • github-actions-cdk-diff — nur lesend, nur aus Pull-Request-Läufen annehmbar. Es führt cdk diff aus, damit Reviewer Infrastrukturänderungen vor dem Merge sehen.
  • github-actions-cdk-deploy — nur aus Pushes auf den Branch des jeweiligen Kontos annehmbar. Sie besitzt selbst keine breiten IAM-Rechte; sie delegiert an die CDK-Bootstrap-Rollen. Eine Erweiterung dessen, was CDK deployen darf, läuft über cdk bootstrap, nicht über das Bearbeiten einer Policy.

Die Trust Policy verankert das Repository und die exakte Ref, sodass ein Fork oder ein Feature-Branch die Deploy-Rolle nicht annehmen kann.

Wir haben außerdem teuer gelernt, dass cdk deploy von einem Laptop aus ein Bug ist, kein Feature. Ein lokales Deployment ohne die richtigen Context-Flags synthetisierte einmal ein Template, dem die App-Runner-Services fehlten, und CloudFormation löschte sie pflichtbewusst. Dieser Vorfall bekam einen eigenen Artikel; die Kurzfassung ist, dass Termination Protection, ein RETAIN-Aspect auf kritischen Ressourcentypen und ein lautes Banner, wenn CDK außerhalb von CI läuft, jetzt Teil der Codebasis sind.

Pull Request GitHub OIDC github-actions-cdk-diff nur lesen · cdk diff push dev GitHub OIDC github-actions-cdk-deploy nur push · delegiert an CDK-Bootstrap cdk deploy Konto: dev push staging GitHub OIDC github-actions-cdk-deploy nur push · delegiert an CDK-Bootstrap cdk deploy Konto: staging push master GitHub OIDC github-actions-cdk-deploy nur push · delegiert an CDK-Bootstrap cdk deploy Konto: prod Trust Policy = dieses Repo + genau dieser Ref. Nirgends langlebige Zugangsschlüssel.

Tags zeigen, was es kostet

Jede Ressource in jedem Konto trägt drei Tags: Project, Stage, ManagedBy. Mit aktivierten Cost Allocation Tags in Billing teilt sich die monatliche Rechnung sauber nach Umgebung auf. Die Zahlen, die wir ohne Vertraulichkeitsbruch teilen können:

  • Aurora Serverless v2 auf einem Sockel von 0.5 ACU zu halten statt Auto-Pause zu nutzen, kostet etwa 30 Dollar im Monat pro Umgebung. Wir zahlen das auch bei Dev, weil Auto-Pause einen Cold Start von 15–30 Sekunden auf dem Signup-Pfad verursachte.
  • Das Dev-Konto ist von den dreien mit deutlichem Abstand das günstigste, hauptsächlich wegen DESTROY-Policies, fehlendem AWS Config und App-Runner-Instanzen in Mindestgröße.
  • Der feste Sockel von Prod wird von zwei warmen App-Runner-Instanzen für die öffentlich zugängliche App, dem Aurora-Sockel und WAF dominiert.

Ohne die Tags ist „Wie viel kostet uns Staging" eine Frage, deren Beantwortung einen Nachmittag dauert. Mit ihnen ist es ein Filter in Cost Explorer.

Was auf dem Weg schiefgelaufen ist

Ein ehrlicher Bericht muss auch die blauen Flecken enthalten.

Das 500-Ressourcen-Limit von CloudFormation. Ein Stack pro Konto war das ursprüngliche Design. Bis August synthetisierte der Prod-Stack auf genau 500 Ressourcen und Dev auf 499. Das Hinzufügen einer einzigen Lambda-Funktion brach cdk synth. Wir haben Observability (Log Data Protection Policies, Metric Filters, Alarms, SNS) in einen eigenen Stack ausgelagert und später eine ganze App in einen weiteren verschoben. Die Lehre: von Tag eins an für mehrere Stacks pro Konto planen, mit expliziten Exports, und keinen einzelnen Stack über etwa 350 Ressourcen hinaus wachsen lassen.

Drift stirbt beim nächsten Deploy. CloudFront-Aliase und ein ACM-Zertifikat wurden im Juli von Hand an eine Distribution angehängt. Das CDK-Deployment im August ersetzte die gesamte DistributionConfig und entfernte sie stillschweigend. Alles, was nicht im Code steht, existiert nicht. Wir halten Aliase und Zertifikat-ARNs jetzt in der Stage-Konfiguration.

Build-Time-Environment ist nicht Runtime-Environment. Next.js bindet NEXT_PUBLIC_*-Variablen zur Build-Zeit ein. Runtime-Umgebungsvariablen im Container können sie nicht mehr ändern. Unser Deploy-Skript liest die Build-Time-Konfiguration jetzt aus dem synthetisierten Template und übergibt sie pro Build an CodeBuild, sodass eine geänderte öffentliche Variable schon beim ersten Deployment statt erst beim zweiten im Browser ankommt.

Das Rate Limit, das das ganze Team aussperrte. Die IP-basierte Rate-Regel der WAF war auf 10.000 Anfragen pro 5 Minuten gesetzt. Das QA-Team des Kunden sitzt hinter einer einzigen Büro-NAT-Adresse. Eine intensive Testsitzung löste die Regel aus, und jede Anfrage aus dem Büro erhielt fünf Minuten lang einen 403. Sie liegt jetzt bei 30.000 und steht in config.ts, nicht in der Konsole.

Würden wir es wieder so machen?

Ja, und zwar früher. Die Kosten von drei Konten sind real, aber gering: drei CDK-Bootstraps, drei OIDC-Rollen, drei Sätze von Secrets, die befüllt werden müssen, und eine Konfigurationsdatei, die ehrlich bleiben muss. Die Kosten, sie nicht zu haben, sind die Liste der Audit-Befunde, mit der wir angefangen haben, plus die permanente Sorge, dass ein Dev-Skript mit einer Wildcard-Berechtigung Produktionsdaten berühren könnte.

Wenn Sie das zum ersten Mal aufsetzen, empfehlen wir folgende Reihenfolge: Schreiben Sie zuerst config.ts mit der Hardening-Matrix, auch wenn die Hälfte der Einstellungen noch nicht implementiert ist; verankern Sie Konten in env und aktivieren Sie Termination Protection vor dem ersten Deployment; verdrahten Sie OIDC, bevor Sie irgendjemandem Deploy-Credentials geben; und splitten Sie in mehrere Stacks, bevor Sie es müssen.

Möchten Sie Hilfe dabei, Ihre eigenen AWS-Konten und Ihren CDK-Code auf diese Weise zu strukturieren? Sprechen Sie mit uns — wir haben es einmal auf die harte Tour gemacht, damit Sie es nicht müssen.