Alle Artikel
24. September 2026
11 mins

Terraform oder AWS CDK? Wie ich Infrastructure as Code auswähle

Von Tim Rutte, Cloud & Software ArchitectThemaAWS & Cloud

Die Einzelteile einer Tischlampe liegen sauber ausgelegt neben einer fertig montierten weißen Lampe, deren einziges Bedienelement ein blauer Kippschalter ist.

Wenn ich heute eine AWS-Infrastruktur neu aufbaue und sonst nichts über das Projekt weiß, fange ich mit Terraform an.

Nicht weil AWS CDK schlechter wäre. Ich habe mit beiden gearbeitet, und beide stellen dieselbe produktive Infrastruktur zuverlässig bereit. Aber sie machen Infrastruktur auf unterschiedliche Art kompliziert. Terraform zwingt mich, Infrastruktur als Infrastruktur zu beschreiben. CDK erlaubt mir, sie wie Software zu modellieren. Genau darin liegt die Stärke von CDK, und genau darin liegt für mich auch sein größtes Risiko.

Meine Voreinstellung lautet deshalb: Für normale AWS-Infrastruktur starte ich mit Terraform. CDK nehme ich, wenn echte Abstraktion und Wiederverwendung den zusätzlichen Programmierlayer rechtfertigen. Nicht vorher.

Was ich hier über Terraform sage, gilt genauso für OpenTofu, den offenen Fork. Die Unterschiede zwischen beiden sind für diese Entscheidung nicht ausschlaggebend.

Zwei Modelle für dasselbe Ziel

Beides ist Infrastructure as Code, aber mit verschiedenen Modellen. Terraform beschreibt Ressourcen in HCL, einer eigenen deklarativen Sprache. Ein vereinfachter ECS-Service beginnt so:

resource "aws_ecs_cluster" "app" {
  name = "app"
}

resource "aws_ecs_service" "api" {
  name            = "api"
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = 2
}

CDK drückt dieselbe Infrastruktur in einer allgemeinen Programmiersprache aus, hier TypeScript:

const cluster = new ecs.Cluster(this, "Cluster", { vpc });

new ecs.FargateService(this, "Api", {
  cluster,
  taskDefinition,
  desiredCount: 2,
});

Auf den ersten Blick ist der Unterschied kosmetisch. Er ist es nicht. CDK ersetzt CloudFormation nicht, es ist eine Abstraktionsschicht darüber: Das Programm läuft, erzeugt ein CloudFormation-Template, und erst CloudFormation legt die Ressourcen an. Terraform spricht über seinen Provider direkt mit den AWS-APIs.

Diese Unterscheidung prägt fast jede weitere Entscheidung. Die erste ist Sichtbarkeit. Öffne ich eine Terraform-Datei und lese

resource "aws_sqs_queue" "orders" {
  name                       = "orders"
  visibility_timeout_seconds = 60
}

dann sehe ich ziemlich direkt, was existieren soll. Natürlich lässt sich auch Terraform verschachteln: Module, Variablen, for_each, dynamische Blöcke und lokale Werte machen eine Konfiguration schnell indirekt. Aber das Grundmodell bleibt: Diese Ressource soll existieren, mit diesen Eigenschaften.

Das gefällt mir bei Infrastruktur, weil sie ohnehin schon indirekt ist. Drei geänderte Zeilen können Netzwerk, IAM, Datenbanken und Produktionsverkehr verändern. Eine zusätzliche Abstraktionsschicht brauche ich dort nicht automatisch.

Warum das Beispiel ein ECS-Service ist und kein Kubernetes-Deployment, steht in ECS Fargate oder Kubernetes.

Wo CDK seine Stärke hat: Constructs

CDK arbeitet mit Constructs, und die gibt es in drei Höhen. Ganz unten bildet ein Construct genau eine CloudFormation-Ressource ab. Darüber liegen die kuratierten Constructs mit sinnvollen Voreinstellungen, etwa ein FargateService, der Rollen und Security Groups mitbringt. Ganz oben kapselt ein Construct eine komplette Plattformkomponente:

new PublicApiService(this, "Orders", {
  domainName: "orders.example.com",
  cpu: 1024,
  memory: 2048,
  minCapacity: 2,
  maxCapacity: 20,
});

Hinter diesen sieben Zeilen können ein ECS-Service, die Fargate Task Definition, ein Application Load Balancer mit Target Group, CloudWatch-Alarme, IAM-Rollen, Auto Scaling, eine Log Group, der DNS-Eintrag und das TLS-Zertifikat liegen. Ein Entwickler konfiguriert keine zwölf AWS-Ressourcen mehr. Er sagt nur noch: Ich brauche einen öffentlichen API-Service.

Dazu kommt etwas, das ich in Terraform wirklich vermisse:

bucket.grantRead(importFunction);

Diese eine Zeile erzeugt eine IAM-Policy, die genau das erlaubt, was Lesen auf diesem Bucket bedeutet, und hängt sie an die Rolle der Funktion. In Terraform schreibe ich dieselbe Policy von Hand, mit allen Aktionen und ARNs, und kann mich dabei vertun.

In einer Organisation mit vielen ähnlichen Workloads ist das extrem wertvoll. Aber ich stelle mir vorher eine Frage: Haben wir wirklich ein wiederkehrendes Plattformmuster, oder bauen wir eine Abstraktion für etwas, das zweimal vorkommt?

Abstraktion verschiebt Komplexität, sie beseitigt sie nicht

In Anwendungscode ist Abstraktion oft ein Qualitätsmerkmal: Wiederholte Logik wird gekapselt, Details verschwinden hinter Interfaces, Komplexität bekommt einen Namen. Bei Infrastruktur bin ich vorsichtiger, denn eine Abstraktion kann auch wichtige Unterschiede verstecken.

new Database(this, "Orders");

Das sieht hervorragend aus. Aber was ist diese Database? Aurora oder RDS PostgreSQL? Multi-AZ oder nicht? Welche Backup-Aufbewahrung, welche Verschlüsselung, welche Security Groups, welche Instanzgröße, welche Parameter Group? Ist Deletion Protection an? Wer wird alarmiert?

Je mehr davon der Construct entscheidet, desto weniger sieht der Entwickler beim Aufruf. Das kann genau das Ziel sein. Es kann aber auch bedeuten, dass eine geschäftskritische Datenbank mit zehn impliziten Architekturentscheidungen aus einer einzigen Codezeile entsteht. Abstraktion reduziert sichtbare Komplexität, nicht reale. Bei Infrastruktur will ich deshalb bewusst entscheiden, welche Details verschwinden dürfen. Das gilt übrigens auch für das grantRead von oben: bequem, aber die Policy, die dabei entsteht, steht nirgends im Quelltext.

Terraform kennt Wiederverwendung natürlich auch:

module "orders_api" {
  source = "../modules/ecs-service"

  name          = "orders"
  cpu           = 1024
  memory        = 2048
  desired_count = 2
}

Auch hier verschwinden darunter mehrere Ressourcen. Der Unterschied liegt also nicht darin, ob beide abstrahieren können, sondern wie weit man damit kommt und wie leicht die Abstraktion anfängt, echte Programmlogik zu enthalten. Mit CDK habe ich Schleifen, Klassen, Vererbung, Funktionen, Dependency Injection, externe Bibliotheken und das ganze Werkzeug einer Programmiersprache. Das ist großartig, wenn ich es brauche, und unnötige Macht, wenn nicht.

„Wir können TypeScript“ reicht mir nicht

Das Argument höre ich oft: Das Team schreibt ohnehin TypeScript oder Python, also soll es auch die Infrastruktur in dieser Sprache definieren. Zunächst stimmt das. Diese Zeilen versteht ein Entwickler schneller als das Gegenstück in HCL:

if (environment === "production") {
  replicas = 4;
}

Aber eine bekannte Sprache macht die entstehende Infrastruktur nicht automatisch verständlicher. Ich mag bei Infrastruktur Einschränkungen. Ich will nicht, dass beim Erzeugen meiner Umgebung beliebige Logik läuft, und ich will vier Fragen schnell beantworten können: Welche Ressourcen entstehen? Warum entstehen sie? Welche Abhängigkeiten haben sie? Was ändert dieses Deployment?

Wenn CDK-Code diese Fragen leichter beantwortet, ist alles gut. Wenn ich dafür durch Klassenhierarchien, Factory-Funktionen und Hilfsbibliotheken springen muss, hat die vertraute Sprache das Problem nicht gelöst. Sie hat nur einen neuen Ort geschaffen, es zu verstecken.

State ist kein Makel, sondern ein Betriebsmodell

Terraform arbeitet mit State. Er verbindet die Ressourcen in der Konfiguration mit den realen Objekten beim Provider, und er muss behandelt werden wie produktive Infrastruktur: nicht lokal auf dem Laptop, sondern in einem Remote Backend mit Zugriffskontrolle, Verschlüsselung und Locking, auf AWS typischerweise ein S3-Bucket. Weil im State auch sensible Werte landen können, gehört der Zugriff darauf so eng begrenzt wie der auf die Produktion selbst.

Das ist zusätzlicher Betriebsaufwand, aber kein Argument gegen Terraform. Irgendein System muss wissen, welche deklarierte Ressource zu welchem realen Objekt gehört. Bei CDK übernimmt das CloudFormation, und das hat eine echte Stärke: Scheitert ein Stack-Update, rollt CloudFormation den Stack auf den letzten funktionierenden Stand zurück. Ein abgebrochenes terraform apply lässt dagegen einen Teil der Änderungen stehen, und der nächste Lauf muss den Rest nachholen. Dafür bringt CloudFormation eigene Grenzen mit, etwa 500 Ressourcen je Stack, und einen Bootstrap-Stack je Konto und Region, bevor CDK überhaupt deployen kann.

Die Frage ist also nicht, ob ich State will oder keinen, sondern welches Zustands- und Deployment-Modell ich betreiben möchte. Bei Terraform liegt dieses Modell offen vor mir. Das empfinde ich eher als Vorteil.

Wenn AWS nicht die ganze Welt ist

Das ist wahrscheinlich der stärkste strukturelle Unterschied. CDK ist auf AWS ausgerichtet. Terraform arbeitet über Provider und verwaltet mit derselben Sprache auch Cloudflare, GitHub, Datadog, Grafana, Kubernetes, SaaS-Dienste und andere Clouds.

Das heißt nicht, dass alles in einen einzigen State gehört, im Gegenteil. Aber ich verwende dieselbe Sprache, dieselben Workflows und dasselbe Tooling über verschiedene Infrastrukturgrenzen hinweg. Und in realen Projekten ist AWS selten die ganze Infrastruktur: DNS liegt bei Cloudflare, die Repositories bei GitHub, das Monitoring bei Datadog, die Statusseite woanders. Wer das auch als Code verwalten will, findet im Provider-Modell von Terraform einen echten Vorteil.

Portabilität ist dagegen ein schwächeres Argument, als es oft klingt. Nur weil ich AWS und Azure mit Terraform beschreiben kann, wird eine AWS-Anwendung nicht portabel. Wenn sie auf DynamoDB, SQS, EventBridge, Lambda, IAM, S3 und CloudFront aufbaut, macht HCL daraus keine Cloud-unabhängige Plattform. Terraform macht mein Werkzeug providerübergreifend, nicht meine Architektur providerunabhängig. Wegen einer hypothetischen Multi-Cloud-Zukunft würde ich es nicht auswählen.

Wo CDK gewinnt: die interne Plattform und das reine AWS-Produkt

Jetzt drehe ich das Szenario um. Ein Unternehmen betreibt 80 Services, mehrere Teams sollen dieselbe Infrastruktur bekommen, und jeder Service braucht ECS oder Lambda, IAM, Logging, Tracing, Alarme, Dashboards, DNS, TLS, Deployment, Tags und Sicherheitsvorgaben. Die Plattformabteilung will nicht, dass jedes Team diese Bausteine selbst zusammensetzt.

Dann wird CDK sehr stark:

new CompanyService(this, "Billing", {
  type: ServiceType.Api,
  exposure: Exposure.Internal,
  scaling: { min: 2, max: 10 },
});

Dieser Construct setzt die Unternehmensstandards automatisch um: Security, Tags, Observability, Netzwerk, Standardalarme, Deployment-Konventionen. Ich abstrahiere hier nicht mehr nur AWS-Ressourcen, ich kodiere einen internen Plattformstandard. An diesem Punkt ist die Programmiersprache kein Gimmick mehr, sondern das Werkzeug für ein Plattformprodukt. Das ist für mich der beste Anwendungsfall für CDK.

Den zweiten sehe ich am anderen Ende der Größenskala: ein kleines Team, ein neues AWS-natives Produkt, alles liegt bei AWS, die Entwickler schreiben ohnehin täglich TypeScript, und das System besteht aus vielen Lambda-Funktionen, EventBridge-Regeln und Step Functions. Wenn Constructs dann fachliche Bausteine ausdrücken, passen Sprache und Infrastrukturmodell gut zusammen:

new InvoiceProcessingPipeline(this, "Invoices", { ... });
new CustomerEventConsumer(this, "CustomerEvents", { ... });
new ScheduledImport(this, "NightlyImport", { ... });

Eine Grenze würde ich auch dort ziehen: Die Constructs dürfen nicht zur zweiten Anwendungsschicht werden. Die Geschäftslogik gehört in die Anwendung, CDK beschreibt die Infrastruktur, auf der sie läuft.

Jede Abstraktion ist ein Produkt

Der interne Construct klingt nach einer einmaligen Investition. Er ist Software, mit Versionen, Abhängigkeiten, Tests, Breaking Changes, Dokumentation, Migrationen, einem Owner und einem Release-Prozess. Verwenden ihn 40 Teams, besitzt die Plattformabteilung ein Produkt. Das ist in Ordnung, solange man es auch so behandelt.

Ein schlecht gepflegtes internes CDK-Framework kann schlimmer werden als duplizierte Infrastruktur. Dann hängt jede Anwendung auf einer alten Construct-Version, weil niemand das Upgrade riskieren will, und die Abstraktion, die Geschwindigkeit bringen sollte, wird selbst zum Legacy-System. Umfangreiche interne Constructs würde ich deshalb erst bauen, wenn die Organisation bereit ist, sie langfristig zu besitzen.

Das Problem ist nicht CDK-spezifisch. Auch Terraform eskaliert in eine interne Modularchitektur, und irgendwann gibt es company-ecs-service-v17, company-rds-v9, company-vpc-v12 und company-observability-v6, ohne dass jemand weiß, welche Kombination zusammenpasst. Meine Regel ist bei beiden Werkzeugen dieselbe: Ich abstrahiere erst, wenn ich das wiederkehrende Muster wirklich kenne. Die ersten beiden Umsetzungen dürfen sich ähneln. Beim dritten Mal weiß ich meistens, was tatsächlich gleich ist und was nur zufällig ähnlich aussah. Dann lohnt sich die Abstraktion.

Ich reviewe die Wirkung, nicht die Zeilen

Ein CDK-Construct macht aus hundert Zeilen CloudFormation zehn Zeilen TypeScript, ein Terraform-Modul aus dreihundert Zeilen Ressourcen zwanzig Zeilen Konfiguration. Das ist nicht automatisch ein Gewinn. Meine wichtigere Kennzahl ist: Wie schnell versteht ein anderer Engineer, was diese Infrastruktur tatsächlich tut? Muss er dafür durch fünf Repositories springen, ist die Abstraktion zu teuer geworden. Infrastruktur wird lange betrieben, oft länger, als ihr ursprünglicher Autor am Projekt arbeitet. Ich optimiere sie deshalb nicht auf wenige Zeilen, sondern auf Verständlichkeit bei Änderungen und im Incident.

Dazu gehört, jede Änderung vor dem Deployment zu sehen. Bei Terraform gehört terraform plan selbstverständlich in die Pipeline, bei CDK cdk diff, das zeigt, welche CloudFormation-Änderungen tatsächlich entstehen. Reviewt wird nicht der Quelltext, sondern seine Auswirkung. Denn diese Zeile

enableFeature();

kann intern zehn Ressourcen ersetzen, und diese

engine_version = "17"

kann die Datenbank betreffen, an der das Geschäft hängt. Infrastructure as Code macht Infrastruktur nicht ungefährlich. Es macht ihre Änderungen versionierbar und reproduzierbar, und erst damit überhaupt prüfbar.

Bestehende Umgebungen: erst sichtbar machen

Übernehme ich eine Infrastruktur, die seit Jahren existiert, interessiert mich zuerst Transparenz. Was existiert? Was gehört zusammen? Was wurde von Hand angelegt, was darf verändert werden, und wo gibt es Drift?

Hier tendiere ich noch stärker zu Terraform, weil ich den Bestand Stück für Stück in ein explizites Ressourcenmodell überführen kann: Import-Blöcke holen eine vorhandene Ressource unter Verwaltung, und terraform plan zeigt danach, ob Code und Wirklichkeit übereinstimmen. CDK kann das mit cdk import und cdk migrate ebenfalls, aber der Weg führt über CloudFormation und fühlt sich für mich weniger direkt an.

Ich versuche dabei nicht, sofort ein perfektes Plattformmodell darüberzulegen. Erst sichtbar machen, dann reproduzierbar machen, dann vereinfachen, erst danach abstrahieren. Das ist dieselbe Reihenfolge, die ich auch bei Legacy-Code bevorzuge.

Wie ich Terraform in einer gewachsenen Umgebung einführe, vom ersten Import bis zum leeren Plan, steht auf einer eigenen Seite.

Was man tut, wenn Code und Konsole danach wieder auseinanderlaufen, steht in Terraform-Drift: wenn Konsole und Code auseinanderlaufen.

Die Entscheidung als Tabelle

FrageTerraformAWS CDK
Normale AWS-Infrastrukturmeist mein Defaultebenfalls gut geeignet
Explizite Ressourcendefinitionsehr starkje nach Construct
Allgemeine Programmiersprachebewusst neinja
Abdeckung von AWSsehr gutsehr gut, direkt aus CloudFormation
Andere Provider und SaaSgroße Stärkenicht das Kernmodell
Eigene PlattformbausteineModuleConstructs besonders mächtig
IAM aus Beziehungen ableitenvon Handgrant-Methoden
Gefahr von Overengineeringvorhandendurch Programmlogik höher
ZustandTerraform State, selbst betriebenCloudFormation-Stack mit Rollback
Bestehende Infrastruktur übernehmenfür mich sehr angenehmmöglich, nicht mein Default
Große interne AWS-Plattformguthier wird CDK interessant
Team kennt TypeScript oder Pythonweniger relevanterleichtert den Einstieg

Eine Entscheidung würde ich trotzdem nicht aus dieser Tabelle allein treffen.

Die Frage, mit der ich anfange

Sagt jemand „Wir sollten CDK nehmen, weil wir dann programmieren können“, frage ich zurück: Welche Infrastrukturkomplexität wollen wir mit dieser Programmiersprache kapseln?

Kommt darauf eine konkrete Antwort, etwa „Wir haben 60 Services und wollen jedem Team denselben sicheren, beobachtbaren ECS-Service als Plattformbaustein geben“, kann CDK hervorragend passen. Lautet die Antwort „TypeScript können wir schon“, reicht mir das nicht. Umgekehrt ist „Terraform ist Industriestandard“ für mich ebenfalls kein Architekturargument. Das Werkzeug muss zur Organisation und zum Problem passen.

Für ein normales AWS-Projekt fange ich deshalb weiter mit Terraform an. Ich bekomme ein explizites Infrastrukturmodell, verwalte AWS und die übrigen Plattformen mit demselben Werkzeug, kann jede Änderung gut reviewen, und Module geben mir genug Wiederverwendung, ohne dass ich sofort ein internes Framework baue.

CDK ziehe ich vor, wenn die Organisation einen Schritt weiter ist: viele ähnliche AWS-Workloads, eine echte Plattformverantwortung, wiederkehrende Standards, Constructs, die sie sinnvoll kapseln, und ein Team, das diese Constructs anschließend wie ein langfristiges Softwareprodukt betreibt. Dann leistet CDK etwas, das Terraform-Module nur annähern: eine programmierbare interne Cloud-Plattform. Vorher brauche ich diese Macht meistens nicht.

Ich wähle Infrastructure as Code nicht danach aus, womit ich die wenigsten Zeilen schreibe. Ich wähle das Werkzeug, mit dem die nächste Person am schnellsten versteht, was in Produktion tatsächlich existiert.