Pulumi vs CDKTF for AWS: A Side-by-Side Comparison
Both Pulumi and CDKTF let you define AWS infrastructure in typed Python, but they differ on execution model, state ownership, and provider coverage in ways that decide which fits your team — this comparison puts them head to head with a decision table and the same S3 bucket plus VPC written both ways, as part of Python vs Terraform vs Ansible under Python IaC Fundamentals & Strategy.
The short version: CDKTF synthesizes to Terraform and reuses the entire Terraform provider and state ecosystem, while Pulumi runs your Python program directly against its own engine. The deeper trade-offs follow.
Context
Teams evaluating Python IaC for AWS almost always shortlist these two because they avoid HCL — a tension explored in Why Python is Replacing HCL for Modern IaC. The choice usually comes down to whether you want to stay inside the Terraform ecosystem (CDKTF) or adopt a self-contained engine with first-class language support (Pulumi).
Prerequisites
- Python 3.9+ and AWS credentials with permission to create VPCs and S3 buckets.
- For Pulumi:
pulumiCLI andpip install pulumi pulumi-aws. - For CDKTF:
cdktfCLI, the Terraform binary, and the generated AWS provider bindings (cdktf get). - A chosen state backend for each tool — see Managing IaC State for Python Projects.
Decision Table
| Criterion | Pulumi (Python) | CDKTF (Python) |
|---|---|---|
| Execution model | Runs your Python program against the Pulumi engine | Synthesizes Python to Terraform JSON, then Terraform applies |
| State | Pulumi-format checkpoint (Pulumi Cloud, S3, GCS, local) | Standard Terraform .tfstate in any Terraform backend |
| Typing | Native typed SDK, Output[T] for deferred values |
Native typed constructs over generated provider bindings |
| Testing | pulumi.runtime.set_mocks unit tests |
Snapshot test synthesized JSON via Testing.synth |
| Provider coverage | Native + Terraform-bridged providers | Every Terraform provider (largest ecosystem) |
| Learning curve | One tool, async Output model to learn |
Two layers (CDKTF + Terraform), but reuses TF knowledge |
| Best when | You want a self-contained, language-first engine | You already invest in Terraform providers/state/policy |
Implementation
1. The same AWS resources with Pulumi
Pulumi expresses the VPC and bucket as Python objects; the engine diffs them against its own state.
# CLI Context: pulumi up
# Provider note: pulumi_aws is the native AWS provider; region comes from `aws:region` config.
import pulumi
import pulumi_aws as aws
from dataclasses import dataclass
@dataclass(frozen=True)
class NetConfig:
cidr: str = "10.0.0.0/16"
bucket_name: str = "example-iac-data"
def build(cfg: NetConfig) -> None:
vpc = aws.ec2.Vpc(
"main-vpc",
cidr_block=cfg.cidr,
enable_dns_hostnames=True,
tags={"Name": "main-vpc"},
)
bucket = aws.s3.BucketV2("data", bucket=cfg.bucket_name)
# State implication: resource IDs are recorded in the Pulumi checkpoint on `up`.
pulumi.export("vpc_id", vpc.id)
pulumi.export("bucket", bucket.bucket)
build(NetConfig())
2. The same AWS resources with CDKTF
CDKTF builds the same two resources as constructs, synthesizes Terraform JSON, and Terraform applies it.
# CLI Context: cdktf get && cdktf synth && cdktf deploy
# Provider note: AwsProvider configures the Terraform AWS provider; state is plain .tfstate.
from constructs import Construct
from cdktf import App, TerraformStack, TerraformOutput
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.vpc import Vpc
from cdktf_cdktf_provider_aws.s3_bucket import S3Bucket
class NetStack(TerraformStack):
def __init__(self, scope: Construct, ns: str) -> None:
super().__init__(scope, ns)
AwsProvider(self, "aws", region="us-east-1")
Vpc(self, "main-vpc", cidr_block="10.0.0.0/16",
enable_dns_hostnames=True, tags={"Name": "main-vpc"})
bucket = S3Bucket(self, "data", bucket="example-iac-data")
# State implication: IDs land in Terraform state after `cdktf deploy`.
TerraformOutput(self, "bucket", value=bucket.bucket)
app = App()
NetStack(app, "net")
app.synth()
3. Decide based on ecosystem fit
If your organization already standardizes on Terraform providers, modules, and policy tooling, CDKTF lets you keep all of it — the synthesis mechanics are detailed in CDKTF Workflows & Terraform Synthesis. If you want a single tool with a native engine and rich component model, Pulumi is the better fit — its patterns live in Pulumi Patterns & Provider Management.
Verification
# Pulumi: a dry run shows planned resources without applying.
pulumi preview --diff
# CDKTF: synthesize, then validate the generated Terraform.
cdktf synth
terraform -chdir=cdktf.out/stacks/net validate
# Both should report exactly one VPC and one bucket to create on first run.
Gotchas & Edge Cases
Pulumi Output[T] values are not plain strings.
You cannot use vpc.id directly in an f-string; you must use .apply() or Output.concat. Treating an Output as a resolved value is the most common Pulumi-on-AWS mistake.
CDKTF needs cdktf get before the first synth.
The typed AWS bindings are generated from the provider; skipping cdktf get yields import errors. Cache the generated .gen directory in CI to avoid regenerating on every run.
State formats are not interchangeable. You cannot copy a Pulumi checkpoint into a Terraform backend or vice versa. Switching tools means re-importing resources, not migrating state — see How to Migrate IaC State Between Backends for the within-ecosystem case.
Frequently Asked Questions
Which has broader AWS provider coverage? CDKTF, because it can use any Terraform provider, which collectively cover the most services. Pulumi covers AWS natively and can also bridge Terraform providers, so the practical gap on AWS is small.
Is Pulumi or CDKTF easier to test?
Both test well. Pulumi unit tests inject mocks via pulumi.runtime.set_mocks; CDKTF favors snapshot testing the synthesized JSON. Pick the style your team prefers — neither requires live AWS calls.
Can I use boto3 inside either tool? Yes, for read-only lookups not covered by the provider. Be careful about side effects that bypass state. The pattern is the same in both engines.
Do they both support remote state with locking on AWS? Yes. CDKTF uses S3+DynamoDB through Terraform; Pulumi can use an S3 backend or Pulumi Cloud. Both support locking and encryption.
Related
- Why Python is Replacing HCL for Modern IaC — the case for typed, testable infrastructure.
- Python vs Terraform vs Ansible — the broader tool-selection landscape.
- Pulumi Patterns & Provider Management — deep dives on Pulumi for AWS and beyond.
- CDKTF Workflows & Terraform Synthesis — how CDKTF synthesizes and applies Terraform.