Pulumi vs AWS CDK — Real-Code IaC Compared

If you've ever hit the wall of HCL loops or YAML anchors and thought "I just want a for-loop and a type checker," both Pulumi and AWS CDK are the answer. They let you write infrastructure in TypeScript, Python, Go, or C#, with real functions, real classes, and an IDE that autocompletes resource properties. That shared premise hides a deep architectural split, and picking the wrong side means committing to the wrong cloud strategy or the wrong deployment engine for years.

The core split #

AWS CDK is a code layer that generates CloudFormation. Your TypeScript or Python program runs during a synth step and emits a CloudFormation template, which AWS then deploys. CDK is AWS-only by design. Everything it produces is a CloudFormation stack.

Pulumi is a full engine. Your program talks to Pulumi's own deployment engine, which calls cloud provider APIs directly through Terraform-derived providers. There is no CloudFormation, no intermediate template you have to reason about. Pulumi supports AWS, Azure, GCP, Kubernetes, Cloudflare, Datadog, and a hundred-plus other providers with the same programming model.

That single difference cascades into everything else: state, speed, rollback behavior, and how far your knowledge transfers when you add a second cloud.

What the code looks like #

Here's an S3 bucket with versioning in CDK:

typescript.typescript

import { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
import { Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export class StorageStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

new Bucket(this, 'AssetBucket', {
versioned: true,
encryption: BucketEncryption.S3_MANAGED,
removalPolicy: RemovalPolicy.RETAIN,
});
}
}

The equivalent in Pulumi, in Python:

python.python

import pulumi
import pulumi_aws as aws

bucket = aws.s3.BucketV2("asset-bucket")

aws.s3.BucketVersioningV2(
"asset-bucket-versioning",
bucket=bucket.id,
versioning_configuration={"status": "Enabled"},
)

aws.s3.BucketServerSideEncryptionConfigurationV2(
"asset-bucket-encryption",
bucket=bucket.id,
rules=[{"apply_server_side_encryption_by_default": {"sse_algorithm": "AES256"}}],
)

pulumi.export("bucket_name", bucket.id)

Notice the shape difference. CDK's constructs bundle related settings into one high-level object with sensible defaults. Pulumi mirrors the provider's resource model more closely, so you compose the primitives yourself. CDK feels more opinionated; Pulumi feels closer to the metal. Both are far more pleasant than raw templates once the infrastructure gets nontrivial.

State management #

This is where the two philosophies diverge most.

CDK has no state of its own. CloudFormation is the state store. AWS tracks what exists, computes changesets, and holds the record of every resource in the stack. You never manage a state file, you never worry about locking, and drift detection is a native CloudFormation feature. The tradeoff is that you inherit CloudFormation's quirks: slow operations, resources it doesn't support yet, and stacks that can get stuck in UPDATE_ROLLBACK_FAILED.

Pulumi manages its own state. By default it lives in Pulumi Cloud, but you can point the backend at S3, Azure Blob, GCS, or a local file. You own the state, which means you own locking, backups, and the occasional pulumi refresh to reconcile drift. It's the same operational model Terraform users already know, with the same benefits and the same footguns.

If you want zero state files to babysit, CDK wins. If you want portable state that isn't tied to one cloud's control plane, Pulumi wins.

Deployment speed and rollback #

CDK inherits CloudFormation's deployment behavior wholesale, and this is the part teams underestimate. CloudFormation deployments can be slow, and its automatic rollback is a double-edged sword. When an update fails midway, CloudFormation rolls the entire stack back to its last good state. That's safe, but a failed rollback can leave a stack wedged and require manual intervention or stack recreation. Large stacks also hit resource limits and long dependency-resolution times.

Pulumi's engine does a direct diff-and-apply against provider APIs, which is generally faster to reach a plan and start executing. There is no automatic full-stack rollback. If a deployment fails partway, Pulumi stops and leaves already-created resources in place, and you fix forward or destroy explicitly. That's more control and less magic. Neither model is strictly better; CloudFormation's rollback saves you from half-applied states, while Pulumi's fail-in-place avoids the wedged-stack problem.

Language support and testing #

CDK supports TypeScript, JavaScript, Python, Java, Go, and C#, generated from a single core via jsii. TypeScript is the first-class citizen and gets features first. Pulumi supports TypeScript, JavaScript, Python, Go, C#, Java, and YAML, with each SDK maintained as a native binding.

For testing, both are genuinely good because it's real code. CDK offers assertion libraries that let you snapshot-test the synthesized template and assert specific resources exist with specific properties. Pulumi supports unit tests with mocked provider calls and property-based checks, plus policy-as-code through CrossGuard. If enforcing "no public S3 buckets" as a test gate matters to you, both deliver, though CDK's template-assertion approach is more mature and widely documented.

Ecosystem and CDKTF #

CDK's ecosystem is CloudFormation constructs, including the higher-level L2 and L3 constructs that encode AWS best practices, plus Construct Hub for shared libraries. It's deep on AWS and shallow everywhere else.

Pulumi's ecosystem is its provider registry, which reuses the enormous Terraform provider corpus plus native providers generated directly from cloud API schemas. Broader coverage, less AWS-specific hand-holding.

Worth knowing: CDKTF (CDK for Terraform) is a related option that gives you CDK's programming model but synthesizes Terraform instead of CloudFormation. It's multi-cloud like Pulumi but deploys through Terraform's engine and state. If you love CDK's construct style but need multi-cloud, CDKTF sits between the two. It's less mature than either mainline tool, so weigh that before betting a platform on it.

Making the call #

Choose AWS CDK if you are all-in on AWS and expect to stay there, want CloudFormation to own state so you never touch a state file, and value the deep library of AWS best-practice constructs. It's the natural choice for AWS-native teams who want typed code without adopting a new engine.

Choose Pulumi if you run more than one cloud, or expect to, want one programming model across AWS, Kubernetes, and SaaS providers, and prefer owning your state backend and deployment engine over inheriting CloudFormation's behavior.

Consider CDKTF only if you specifically want CDK-style constructs with Terraform underneath and accept its lower maturity.

The call we'd make #

For a single-cloud AWS shop, we'd reach for CDK. The lack of state to manage and the quality of the AWS constructs pay off every day, and CloudFormation's rollback catches real mistakes.

For anything genuinely multi-cloud, or any team that already runs heavy Kubernetes plus a couple of SaaS providers, we'd pick Pulumi. One language, one engine, one mental model across every provider beats maintaining CDK for AWS and something else for the rest. The state-management responsibility is a fair price for that portability.

For the wider landscape and where each of these fits, see our guide to the best Infrastructure-as-Code tools. And if your real decision is Pulumi against HCL rather than against CDK, our Terraform vs Pulumi comparison digs into that matchup directly.