Skip to main content

Terraform vs OpenTofu vs CDKTF: IaC for TypeScript Teams 2026

·PkgPulse Team

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

FeatureTerraformOpenTofuCDKTF
LanguageHCLHCLTypeScript / Python / Go
LicenseBSL 1.1✅ MPL-2.0Apache 2.0
Provider count✅ 3,000+✅ 3,000+ (same)✅ Uses TF providers
State management✅ Full✅ Full + encryptionDelegates to TF/OTF
TypeScript supportHCL onlyHCL only✅ Native
Type safetyLimitedLimited✅ Full TypeScript
Reusable constructsModules (HCL)Modules (HCL)✅ Class-based
TestingTerraform testOpenTofu test✅ Jest/Vitest
Native state encryption✅ Built-inDelegates to backend
Terraform CloudHCP Terraform
Community✅ LargestGrowingMedium
GitHub stars43k23k4.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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.