Cloud Development Kit
A Cloud Development Kit (CDK) is a framework for defining infrastructure in a general-purpose programming language — TypeScript, Python, Java, C#, Go — that synthesizes (compiles down) to the declarative template format of an existing provisioning engine, which then does the actual work of creating resources. The archetype is the AWS Cloud Development Kit (AWS CDK), “an open-source software development framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation” (AWS CDK guide). The same pattern was generalised into the Construct Programming Model (CPM) and reused by CDK for Terraform (CDKTF), which emits Terraform JSON, and CDK for Kubernetes (CDK8s), which emits Kubernetes YAML manifests. The defining move of the whole family is imperative front-end, declarative back-end: you write loops, conditionals, types, and functions, but what you ship is a plain declarative artifact that a mature, unchanged engine (AWS CloudFormation, Terraform, or Kubernetes) executes. This is the crucial contrast with Pulumi, which also lets you write infrastructure in a real language but runs its own deployment engine rather than generating another tool’s template.
The tension a CDK resolves is old. Declarative infrastructure formats — CloudFormation JSON/YAML, HCL, Kubernetes YAML — are excellent at describing a desired end state and letting an engine reconcile the world to it (the model taught in The Desired-State Provisioning Model), but they are miserable to program: no real loops, weak abstraction, copy-paste for repetition, and no type checking until you deploy. A CDK keeps the declarative engine — with all its plan/apply/rollback machinery intact — and puts a programming language in front of it as a template generator. You get IDE autocomplete, refactoring, unit tests, and package management on the authoring side; the engine on the execution side never changes.
Mental Model — Imperative Front-End, Declarative Back-End
flowchart LR subgraph AUTHOR["Author side — imperative, general-purpose language"] CODE["Your program<br/>(TypeScript / Python / Java / Go)<br/>loops · types · functions · tests"] CONSTRUCTS["Constructs<br/>reusable objects modelling<br/>one or more resources"] CODE --> CONSTRUCTS end CONSTRUCTS -->|"cdk synth<br/>(run program, traverse tree)"| ARTIFACT subgraph ARTIFACT["Synthesized declarative artifact"] CFN["CloudFormation template<br/>(AWS CDK)"] TFJSON["Terraform JSON<br/>(CDKTF)"] K8S["Kubernetes manifests<br/>(CDK8s)"] end CFN -->|"cdk deploy"| CFENGINE["CloudFormation service<br/>creates AWS resources"] TFJSON -->|"terraform apply"| TFENGINE["Terraform engine<br/>creates resources via providers"] K8S -->|"kubectl apply"| KAPI["Kubernetes API server<br/>reconciles objects"]
What it shows and the insight to take: the CDK is a template compiler, not a provisioning engine. All the value the author sees — a real language, constructs, tests — lives on the left. All the value the operator relies on — diffing, dependency ordering, rollback, drift detection — lives on the right and is provided by an unchanged, battle-tested engine. The cdk synth arrow in the middle is where the two worlds meet: it runs your program and captures the resources it declared as a static file. Nothing your program does at runtime survives past that arrow — a point with sharp debugging consequences (see Failure Modes).
The Construct Programming Model
The unit of composition in every CDK is the construct. A construct is “a component within your application that represents one or more AWS CloudFormation resources and their configuration” (AWS CDK constructs) — but the idea is engine-agnostic, which is exactly why the same base class powers CDKTF and CDK8s. Constructs are ordinary classes that extend a Construct base class (published in the standalone constructs package), and they compose into a tree.
Every construct takes three initializer arguments, and understanding them is understanding the model:
scope— the construct’s parent or owner, which fixes its place in the construct tree. You almost always passthis(selfin Python).id— a string identifier that need only be unique within its scope. It is used to generate deterministic names and, in AWS CDK, CloudFormation logical IDs.props— a bag of configuration properties. Higher-level constructs supply more defaults, sopropsis often optional.
The two roots of any AWS CDK program are themselves special constructs that create no resources of their own but provide context: an App (the whole program) contains one or more Stack constructs (each maps to one deployment unit — a CloudFormation stack), and every resource-bearing construct must live inside a Stack (AWS CDK constructs).
The three abstraction levels (L1 / L2 / L3)
AWS CDK constructs come in three levels of abstraction — the single most-quoted piece of CDK vocabulary — and each trades power for convenience (AWS CDK constructs):
flowchart TD L3["L3 — Patterns<br/>whole architectures, opinionated defaults<br/>e.g. ApplicationLoadBalancedFargateService<br/>(ALB + ECS cluster + service + task def)"] L2["L2 — Curated constructs<br/>one resource, intent-based API, sensible/secure defaults<br/>e.g. s3.Bucket — generates boilerplate + glue"] L1["L1 — CFN resources (Cfn* prefix)<br/>1:1 with a CloudFormation resource, zero abstraction<br/>e.g. s3.CfnBucket — you set every property"] RAW["Raw CloudFormation resource"] L3 -->|"composed from"| L2 L2 -->|"wraps"| L1 L1 -->|"maps 1:1 to"| RAW style L3 fill:#1a4d2e,color:#fff style L2 fill:#2d6a4f,color:#fff style L1 fill:#40916c,color:#fff
What it shows and the insight to take: abstraction is a stack, and every level ultimately bottoms out in raw CloudFormation resources. Higher = less code, more opinion, less control; lower = more code, no opinion, total control.
- Level 1 (L1) constructs, also called CFN resources, “offer no abstraction” and each “maps directly to a single AWS CloudFormation resource” (AWS CDK constructs). They are named with a
Cfnprefix (s3.CfnBucket) and are auto-generated from the CloudFormation resource specification — so any resource CloudFormation supports is available as an L1 construct, typically within about a week of release. You set every property yourself. L1 is the escape hatch: whatever the higher levels can’t express, you can always drop to L1. - Level 2 (L2) constructs, the curated constructs, are hand-written by the CDK team and are the most widely used.
s3.Bucketis the canonical example. An L2 still maps to (largely) one resource but exposes an intent-based API with sensible defaults, best-practice security policies, and generated boilerplate/glue logic. L2s also carry helper methods — for instance grant-style methods that wire up the correct IAM policy for one resource to access another, so you never hand-author the JSON policy document. - Level 3 (L3) constructs, the patterns, are the highest abstraction: each bundles “a collection of resources that are configured to work together to accomplish a specific task.” The guide’s example,
ApplicationLoadBalancedFargateService, stands up an entire load-balanced containerised service — the ECS cluster, the Fargate service, the task definition, and the Application Load Balancer — from a handful of properties. L3s are opinionated; you accept their architecture in exchange for near-zero code.
Uncertain
Verify: the exact stable public surface of L2 helper/grant methods and the newer Mixins (
.with()) andgrantsnamespace shown in the current AWS CDK guide. Reason: the fetched constructs guide showed API forms (rawData.grants.read(...),s3.mixins.BucketVersioning()via.with()) that differ from the long-standard grant methods (e.g.bucket.grantRead(principal)) and may be a recent or in-preview API revision as of mid-2026. To resolve: check theaws-cdk-libAPI reference for the installed version. This note deliberately describes grant/helper behaviour generically rather than pinning exact method names.#uncertain
Composition — not inheritance — is the intended way to build higher-level constructs: a custom construct usually contains an s3.Bucket and an sns.Topic rather than subclassing them. That is the whole reason the model scales: teams package company best-practice constructs (a hardened DynamoDB table, a compliant VPC) and share them as ordinary library packages via npm, PyPI, or Maven, discoverable through the Construct Hub (constructs.dev).
The Synth-then-Deploy Two-Step
The defining runtime behaviour of a CDK is that execution happens in two distinct phases, and conflating them is the source of most CDK confusion.
sequenceDiagram participant Dev as Developer participant CLI as CDK CLI participant App as Your program participant CFN as CloudFormation participant AWS as AWS APIs Dev->>CLI: cdk synth CLI->>App: run program (construct → prepare → validate → synth) App->>App: app.synth() traverses construct tree App-->>CLI: cloud assembly in cdk.out/<br/>(CFN template + assets) Note over App: program has now EXITED Dev->>CLI: cdk deploy CLI->>App: re-run synth (implicitly) CLI->>CFN: submit template + upload assets to S3/ECR CFN->>AWS: create / update / delete resources CFN-->>Dev: stack events, rollback on error
What it shows and the insight to take: cdk synth runs your code and freezes the result into a static cloud assembly; cdk deploy hands that frozen artifact to the engine. By the time deployment starts, “your CDK app has already finished running and exited” (AWS CDK deploy). Your program cannot observe or react to deployment.
The AWS CDK formalises the first phase as the app lifecycle with four stages (AWS CDK deploy):
- Construction (Initialization) — your constructors run and instantiate the whole construct tree. This is where most of your code executes.
- Preparation — constructs that implement a
preparehook do final mutations. Rarely needed. - Validation — constructs that implement
validateself-check and can throw before anything is written. - Synthesis — triggered by
app.synth(), the CDK traverses the tree, invokes each construct’ssynthesizemethod, and writes deployment artifacts — CloudFormation templates, Lambda bundles, file and Docker image assets — into a cloud assembly (thecdk.outdirectory).
cdk deploy then takes that cloud assembly and submits it: assets are uploaded to the bootstrapped Amazon S3 bucket and ECR repository, and the CloudFormation template is submitted to CloudFormation (AWS CDK deploy). Note the prerequisite: the target AWS environment must be bootstrapped first (cdk bootstrap), which provisions the S3 bucket, ECR repo, and IAM roles the CDK uses to stage assets and run deployments.
A subtle consequence is the Token. Because synthesis happens before deployment, values that only exist after a resource is created — an auto-generated bucket name, an ARN — are not real strings at synth time. The CDK represents them as Token placeholders that resolve to CloudFormation intrinsic functions (Ref, Fn::GetAtt) in the emitted template. If you console.log(bucket.bucketName) you will see an unresolved token, not a name — because the name does not exist yet.
The Two Steps Applied — a minimal AWS CDK program
import { App, Stack, StackProps } from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
class HelloCdkStack extends Stack { // a Stack = one deploy unit
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props); // wire this stack into the tree
new s3.Bucket(this, 'MyFirstBucket', { // an L2 construct; scope=this, id='MyFirstBucket'
versioned: true // intent-based prop; L2 fills the rest
});
}
}
const app = new App(); // the root construct
new HelloCdkStack(app, 'HelloCdkStack'); // instantiate the stack under the app
app.synth(); // (usually implicit) — produce the cloud assemblyLine by line: App is the root construct; Stack is its child and the deployment boundary; s3.Bucket is an L2 construct whose id ('MyFirstBucket') is not the physical bucket name — it is a logical identifier within the app that the CDK uses to compute the CloudFormation logical ID (AWS CDK constructs). Running cdk synth on this expands the single Bucket into a full AWS::S3::Bucket resource with versioning configuration; the guide notes a comparable ECS example whose ~30 lines of CDK synthesise to a 500+ line CloudFormation template producing 50+ resources. That expansion ratio is the entire point.
The Family — Same Model, Different Back-Ends
The Construct Programming Model (CPM) is what makes the pattern portable. The AWS CDK team factored the construct machinery out of the AWS-specific parts, so the identical Construct base class drives three different engines (AWS CDK guide):
| Tool | Language front-end | Synthesizes to | Executed by | Status (as of mid-2026) |
|---|---|---|---|---|
| AWS CDK | TS, JS, Python, Java, C#, Go | CloudFormation template | AWS CloudFormation | Actively developed, v2 line (aws-cdk-lib on the v2.x series) |
| CDK for Terraform (CDKTF) | TS, Python, Java, C#, Go | Terraform JSON | Terraform engine + providers | Deprecated 10 Dec 2025 — HashiCorp no longer maintains it; last line v0.21.x |
| CDK8s | TS, Python, Java, Go | Kubernetes YAML manifests | Kubernetes API (via kubectl apply / GitOps) | Active, v2.x line |
| Projen | TS et al. | project config files | (n/a — scaffolds repos) | Active |
CDK for Terraform (CDKTF) applies the model to Terraform: you write constructs in a general-purpose language, and cdktf synth “converts your procedural code into JSON Terraform configurations, which are then processed by the standard Terraform engine” (CDKTF docs). Crucially it reuses the entire Terraform ecosystem — providers, modules, the Registry, state, HCP Terraform, Sentinel — because it only replaces the authoring language, not the engine. Its resource classes are auto-generated from Terraform provider schemas, the direct analogue of AWS CDK’s L1 generation from the CloudFormation spec.
Uncertain
Verify: CDKTF’s deprecation date and maintenance status. Reason: the CDKTF documentation page (fetched 2026-07-24) states it was deprecated as of 10 December 2025 and that “HashiCorp no longer supports or maintains the Cloud Development Kit for Terraform,” with the last version line at v0.21.x — but this is a fast-moving, post-IBM-acquisition governance fact. To resolve: re-check the CDKTF docs / HashiCorp changelog. Point-in-time: as of 2026-07-24, CDKTF is deprecated.
#uncertain
CDK8s (CDK for Kubernetes) applies the model to Kubernetes: it is “a software development framework for defining Kubernetes applications and reusable abstractions using familiar programming languages” that generates pure Kubernetes YAML manifests (CDK8s docs). Its tree has an App root and Chart constructs (each Chart synthesizes to a separate manifest file), with Kubernetes objects (Pod, Service, Deployment) as leaves. Critically, CDK8s does not talk to a cluster — it synthesizes charts into a dist/ directory, which you then apply with kubectl apply -f or feed to a GitOps tool like Flux or Argo CD. That clean separation makes CDK8s a natural fit for GitOps: the synthesized YAML is the artifact Git tracks and the cluster reconciles.
Where the CDK Family Sits vs Pulumi
The sharpest way to understand a CDK is by contrast with Pulumi, which superficially looks identical (write infrastructure in TypeScript/Python/Go) but is architecturally the opposite choice.
flowchart TD subgraph CDKPATH["CDK approach — generate a template"] C1["Program"] --> C2["synth"] --> C3["CloudFormation / TF JSON / K8s YAML"] --> C4["Existing engine executes"] end subgraph PULUMIPATH["Pulumi approach — own engine"] P1["Program"] --> P2["Language host detects<br/>resource registrations"] --> P3["Pulumi deployment engine<br/>(embedded in the CLI)"] --> P4["Resource providers create resources"] end
What it shows and the insight to take: a CDK emits an intermediate declarative template and hands it off; Pulumi has no intermediate template — “the deployment engine is embedded in the pulumi CLI itself,” and “the engine does not talk directly to AWS, instead it just asks the AWS Resource Plugin to create a Bucket” (Pulumi docs). Pulumi’s program registers resources directly with its own engine, which diffs against its own state and drives providers. So:
- With AWS CDK you can always drop to the CloudFormation template, deploy it without the CDK, and lean on CloudFormation’s drift detection and rollback. The engine is a well-known, separately-supported product.
- With Pulumi there is no template to inspect; the “plan” is computed by Pulumi’s engine and the state lives in Pulumi’s backend. You gain a single tool with no synthesis seam; you lose the ability to hand the artifact to a foreign engine.
Neither is strictly better — it is a genuine trade-off between reusing a mature declarative engine (CDK) and owning the whole pipeline (Pulumi).
Failure Modes and Common Misunderstandings
- “My code should run during deployment.” It does not. Everything imperative happens at synth time; by deploy time the program has exited. To run logic during a deployment you must inject a custom resource (a Lambda CloudFormation invokes) — you cannot just write a function and expect it to fire mid-apply (AWS CDK deploy).
- Printing a token and getting gibberish. Reading
bucket.bucketNameat synth time yields an unresolvedToken, not a name, because the resource does not exist until CloudFormation creates it. Use it in construct props (where it becomes aRef/Fn::GetAtt), not in your own runtime logic. - Debugging the wrong layer. A CDK failure can live in three places: your program (a synth error, caught early — which is why
cdk synthbeforecdk deployis good hygiene), the synthesized template, or the engine’s deployment (a CloudFormation rollback). The two-step means you debug by first inspecting the emitted template (cdk synthoutput /cdk.out), then the engine’s stack events. Skipping straight to “my code is wrong” wastes time when the template is fine and CloudFormation rejected it. - Forgetting to bootstrap.
cdk deployfails if the environment was nevercdk bootstrap-ed, because there is no S3 bucket/ECR repo to stage assets and no IAM roles to assume (AWS CDK deploy). - Over-reaching L3s. Patterns hide an entire architecture behind a few props; when their opinions don’t match your requirement, fighting an L3 is worse than composing L2s yourself. The escape hatch (drop to L2, then L1, then raw
CfnResourceoverrides) exists precisely for this. - Turing-complete infrastructure. Because you now have loops and conditionals, you can generate wildly different templates from tiny code changes — powerful, but it makes review harder. Reviewing a CDK PR often means reviewing the synthesized template diff (
cdk diff), not just the code, for the same reason Infrastructure Plan Review reviews the plan and not the HCL.
Alternatives and When to Choose a CDK
Choose a CDK when your team already lives in a general-purpose language and wants types, tests, and abstraction without abandoning a specific declarative engine you’re committed to (CloudFormation, Terraform, or Kubernetes). Choose raw HCL / CloudFormation YAML / Kubernetes YAML when you value the transparency and reviewability of a flat declarative file and don’t need programmatic abstraction — the artifact is exactly what runs, with no synthesis step to reason about. Choose Pulumi when you want a real language and are happy to adopt its own engine and state model rather than generating another tool’s template. Choose Terraform/OpenTofu plain HCL when the ecosystem breadth of Terraform providers matters more than language ergonomics. The CDK family’s unique niche is precisely the hyphen in “imperative-front, declarative-back” — you keep the engine, you upgrade the pen.
See Also
- Pulumi — the general-purpose-language IaC tool with its own engine; the load-bearing contrast to the CDK’s synthesize-to-a-foreign-engine model
- Terraform — the engine CDKTF synthesizes to
- AWS CloudFormation — the engine AWS CDK synthesizes to; the declarative service underneath every
cdk deploy - Azure Bicep — a sibling idea one level lower: a DSL (not a full language) that transpiles to ARM templates
- Provisioning versus Configuration Management — the category the CDK lives in (provisioning), distinct from configuring inside machines
- The Desired-State Provisioning Model — the reconcile loop the synthesized template feeds
- Declarative vs Imperative Configuration — the axis a CDK straddles: imperative authoring, declarative execution
- Infrastructure Plan Review — why you review the synthesized diff, not just the code
- GitOps — CDK8s’s natural downstream: synthesized YAML tracked in Git, reconciled into the cluster
- Infrastructure as Code MOC — parent map (§5 Tools Landscape)