Terraform vs OpenTofu vs CDKTF: IaC for TypeScript Teams 2026
Terraform vs OpenTofu vs CDKTF: IaC for TypeScript Teams 2026
TL;DR
HashiCorp changed Terraform's license from MPL-2.0 to BSL 1.1 in August 2023 — restricting commercial use by competitors. This triggered a fork (OpenTofu) maintained by the Linux Foundation. Meanwhile, CDKTF lets TypeScript developers write Terraform configs in code instead of HCL. Terraform (BSL 1.1) is the most mature IaC tool with 3,000+ providers and the largest community, but the license change creates uncertainty for some organizations. OpenTofu (MPL-2.0) is a drop-in replacement for Terraform — same HCL, same providers, fully open-source, already at feature parity. CDKTF is the TypeScript abstraction layer on top of either Terraform or OpenTofu — write infrastructure in TypeScript, generate HCL, leverage the full Terraform provider ecosystem. For TypeScript teams who want to own their infra code: CDKTF. For HCL users who need open-source guarantees: OpenTofu. For teams already on Terraform without licensing concerns: Terraform.
Key Takeaways
- OpenTofu 1.8+ is feature-compatible with Terraform 1.8 — migration is change one binary
- CDKTF generates Terraform-compatible HCL — use all 3,000+ Terraform providers with TypeScript
- Terraform BSL restricts hosting Terraform-as-a-service — doesn't affect internal use
- OpenTofu has its own registry — providers work from both registry.opentofu.org and registry.terraform.io
- CDKTF uses constructs — the same CDK model as AWS CDK (familiar if you've used CDK)
- State management is identical — Terraform state files work between Terraform and OpenTofu
- Pulumi vs CDKTF: Pulumi is a full rewrite; CDKTF generates HCL and uses Terraform providers
The Terraform Licensing Split
August 2023:
Terraform (MPL-2.0) ──license change──> Terraform (BSL 1.1)
|
┌──────────────────────┘
▼
OpenTofu (MPL-2.0) — Linux Foundation
"The truly open-source Terraform"
What BSL 1.1 means:
- ✅ Internal use is fine — DevOps teams can use Terraform normally
- ✅ Consulting and implementation work is fine
- ❌ You cannot offer Terraform as a hosted service to others
- ❌ Products that compete with HashiCorp (Terraform Cloud) cannot use Terraform
For most teams: BSL 1.1 has zero practical impact. But enterprises with strict open-source compliance requirements increasingly choose OpenTofu.
Terraform: The Established Standard
Terraform defines infrastructure via HCL (HashiCorp Configuration Language) — a declarative DSL that describes desired state, and Terraform plans + applies the diff.
Installation
# macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Check version
terraform version
Basic AWS Provider Setup
# main.tf — Terraform configuration
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Remote state — store state in S3 with DynamoDB locking
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
}
Variables and Outputs
# variables.tf
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "environment" {
type = string
default = "production"
}
variable "app_name" {
type = string
}
# outputs.tf
output "load_balancer_dns" {
value = aws_lb.app.dns_name
description = "The DNS name of the load balancer"
}
output "rds_endpoint" {
value = aws_db_instance.main.endpoint
sensitive = true
}
EC2 + RDS + S3 Stack
# infrastructure.tf
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "${var.app_name}-vpc"
Environment = var.environment
}
}
# RDS PostgreSQL
resource "aws_db_instance" "main" {
identifier = "${var.app_name}-db"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t3.micro"
allocated_storage = 20
storage_encrypted = true
db_name = "appdb"
username = "postgres"
password = var.db_password # From var, not hardcoded
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = 7
skip_final_snapshot = false
final_snapshot_identifier = "${var.app_name}-final-snapshot"
tags = {
Environment = var.environment
}
}
# S3 bucket
resource "aws_s3_bucket" "assets" {
bucket = "${var.app_name}-assets-${var.environment}"
tags = {
Environment = var.environment
}
}
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
Terraform Workflow
# Initialize (download providers)
terraform init
# Review planned changes
terraform plan -var-file="production.tfvars"
# Apply changes
terraform apply -var-file="production.tfvars"
# Destroy infrastructure
terraform destroy -var-file="production.tfvars"
# Format code
terraform fmt -recursive
# Validate syntax
terraform validate
# Import existing resource into state
terraform import aws_s3_bucket.assets my-existing-bucket
# Workspace management (for multi-env)
terraform workspace new staging
terraform workspace select production
OpenTofu: The Open-Source Drop-In
OpenTofu is a fork of Terraform 1.5.x maintained by the Linux Foundation. The CLI, HCL syntax, provider ecosystem, and state format are all compatible.
Installation
# macOS
brew install opentofu
# Or via the official installer
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh | sh
tofu version
# OpenTofu v1.8.3
Identical HCL — Just Change the Binary
# Migrate from Terraform to OpenTofu:
# 1. Install tofu binary
# 2. Replace "terraform" with "tofu" in your commands
# That's it.
tofu init
tofu plan
tofu apply
# State files are 100% compatible — no migration needed
OpenTofu-Specific Features (Ahead of Terraform)
# OpenTofu 1.7+: Native state encryption
terraform {
encryption {
key_provider "pbkdf2" "my_key" {
passphrase = var.state_encryption_passphrase
}
method "aes_gcm" "state_encryption" {
keys = key_provider.pbkdf2.my_key
}
state {
method = method.aes_gcm.state_encryption
enforced = true
}
}
}
# OpenTofu 1.8+: Provider-defined functions
# Call functions defined in providers directly in HCL
provider::aws::arn_parse("arn:aws:s3:::my-bucket")
Registry Compatibility
# OpenTofu uses its own registry by default
# registry.opentofu.org mirrors all providers from registry.terraform.io
# Explicit registry source (usually not needed — OpenTofu finds providers automatically)
terraform {
required_providers {
aws = {
source = "registry.opentofu.org/hashicorp/aws"
version = "~> 5.0"
}
}
}
CDKTF: Terraform with TypeScript
CDKTF (Cloud Development Kit for Terraform) lets you write infrastructure in TypeScript, Python, Java, C#, or Go — generating Terraform HCL under the hood.
Installation
npm install -g cdktf-cli
cdktf init --template=typescript --providers=aws
# This creates:
# cdktf.json — project config
# main.ts — your infrastructure code
# package.json — TypeScript dependencies
Basic Stack in TypeScript
// main.ts
import { App, TerraformStack, TerraformOutput } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { Instance } from "@cdktf/provider-aws/lib/instance";
import { S3Bucket } from "@cdktf/provider-aws/lib/s3-bucket";
import { S3BucketVersioningA } from "@cdktf/provider-aws/lib/s3-bucket-versioning";
import { DbInstance } from "@cdktf/provider-aws/lib/db-instance";
class MyAppStack extends TerraformStack {
constructor(scope: App, id: string) {
super(scope, id);
new AwsProvider(this, "AWS", {
region: "us-east-1",
});
// S3 bucket — TypeScript autocomplete for all properties
const assetsBucket = new S3Bucket(this, "assets", {
bucket: "my-app-assets-production",
tags: { Environment: "production" },
});
new S3BucketVersioningA(this, "assets-versioning", {
bucket: assetsBucket.id,
versioningConfiguration: { status: "Enabled" },
});
// RDS instance
const db = new DbInstance(this, "database", {
identifier: "my-app-db",
engine: "postgres",
engineVersion: "16.1",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
dbName: "appdb",
username: "postgres",
password: process.env.DB_PASSWORD!,
skipFinalSnapshot: false,
finalSnapshotIdentifier: "my-app-final-snapshot",
storageEncrypted: true,
});
// Outputs
new TerraformOutput(this, "db-endpoint", {
value: db.endpoint,
sensitive: true,
});
new TerraformOutput(this, "assets-bucket", {
value: assetsBucket.bucket,
});
}
}
const app = new App();
new MyAppStack(app, "my-app-production");
app.synth();
Reusable Constructs
// constructs/web-app.ts — reusable infrastructure pattern
import { Construct } from "constructs";
import { TerraformOutput } from "cdktf";
import { Lb } from "@cdktf/provider-aws/lib/lb";
import { LbListener } from "@cdktf/provider-aws/lib/lb-listener";
import { EcsService } from "@cdktf/provider-aws/lib/ecs-service";
import { EcsCluster } from "@cdktf/provider-aws/lib/ecs-cluster";
interface WebAppConfig {
name: string;
imageUri: string;
cpu: number;
memory: number;
port: number;
vpcId: string;
subnetIds: string[];
desiredCount?: number;
}
export class WebApp extends Construct {
public readonly loadBalancerDns: string;
constructor(scope: Construct, id: string, config: WebAppConfig) {
super(scope, id);
const cluster = new EcsCluster(this, "cluster", {
name: `${config.name}-cluster`,
});
const lb = new Lb(this, "lb", {
name: `${config.name}-lb`,
internal: false,
loadBalancerType: "application",
subnets: config.subnetIds,
});
const service = new EcsService(this, "service", {
name: config.name,
cluster: cluster.id,
desiredCount: config.desiredCount ?? 2,
launchType: "FARGATE",
// ... task definition, network config
});
this.loadBalancerDns = lb.dnsName;
new TerraformOutput(this, "lb-dns", {
value: lb.dnsName,
});
}
}
// In your main stack — reuse the construct
import { WebApp } from "./constructs/web-app";
class ProductionStack extends TerraformStack {
constructor(scope: App, id: string) {
super(scope, id);
// ...provider setup...
const webApp = new WebApp(this, "web-app", {
name: "my-api",
imageUri: "123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:latest",
cpu: 256,
memory: 512,
port: 3000,
vpcId: "vpc-12345",
subnetIds: ["subnet-1", "subnet-2"],
});
console.log("Load balancer:", webApp.loadBalancerDns);
}
}
CDKTF Workflow
# Synthesize HCL (generate Terraform config from TypeScript)
cdktf synth
# Outputs to cdktf.out/stacks/my-app-production/cdk.tf.json
# Plan (runs terraform plan under the hood)
cdktf plan my-app-production
# Deploy
cdktf deploy my-app-production
# Destroy
cdktf destroy my-app-production
# Use OpenTofu instead of Terraform
CDKTF_HOME=$HOME/.cdktf cdktf deploy # Configure TERRAFORM_BINARY_NAME=tofu
Feature Comparison
| Feature | Terraform | OpenTofu | CDKTF |
|---|---|---|---|
| Language | HCL | HCL | TypeScript / Python / Go |
| License | BSL 1.1 | ✅ MPL-2.0 | Apache 2.0 |
| Provider count | ✅ 3,000+ | ✅ 3,000+ (same) | ✅ Uses TF providers |
| State management | ✅ Full | ✅ Full + encryption | Delegates to TF/OTF |
| TypeScript support | HCL only | HCL only | ✅ Native |
| Type safety | Limited | Limited | ✅ Full TypeScript |
| Reusable constructs | Modules (HCL) | Modules (HCL) | ✅ Class-based |
| Testing | Terraform test | OpenTofu test | ✅ Jest/Vitest |
| Native state encryption | ❌ | ✅ Built-in | Delegates to backend |
| Terraform Cloud | ✅ | HCP Terraform | ✅ |
| Community | ✅ Largest | Growing | Medium |
| GitHub stars | 43k | 23k | 4.7k |
When to Use Each
Choose Terraform if:
- Your team is already on Terraform and BSL doesn't affect your use case
- Terraform Cloud or HCP Terraform is part of your workflow
- You need access to the most mature provider versions first
- Enterprise support from HashiCorp matters to your organization
Choose OpenTofu if:
- Open-source licensing is a compliance requirement
- You want to avoid BSL uncertainty in your infrastructure toolchain
- You're starting a new project and want the most future-proof option
- Native state encryption without third-party tools is needed
Choose CDKTF if:
- Your team prefers TypeScript over HCL (strong typing, IDE completion)
- You want reusable infrastructure patterns as npm packages
- Testing infrastructure code with Jest/Vitest is a priority
- You're already using AWS CDK and want similar patterns for multi-cloud
Methodology
Data sourced from official Terraform and OpenTofu documentation, HashiCorp BSL license FAQ (hashicorp.com), OpenTofu changelog (github.com/opentofu/opentofu), CDKTF documentation (developer.hashicorp.com/terraform/cdktf), and community discussions from the OpenTofu Discord, r/devops, and HashiCorp forums. GitHub star counts as of February 2026.
Related: Pulumi vs SST vs CDKTF for JavaScript-first IaC alternatives, or SST v3 vs Serverless Framework vs AWS CDK for serverless-focused infrastructure tools.