Both tools solve the same problem: describe cloud infrastructure once, then create, update, and destroy it predictably. The choice between them comes down to one question you answer before writing a line of config. Do you want to describe infrastructure in a general-purpose programming language you already know, or in HCL, the declarative configuration language that Terraform popularized and that OpenTofu now carries forward under an open-source license?
Pulumi takes the first path. You write TypeScript, Python, Go, or C#, and Pulumi's engine turns your program's output into a resource graph. OpenTofu takes the second. It is the Linux Foundation fork of Terraform, created after HashiCorp relicensed Terraform under the Business Source License in 2023, and it stays deliberately close to Terraform's HCL, state format, and provider protocol.
This guide compares the two on the axes that decide the pick: language model, state handling, provider ecosystem, and licensing. It shows the same resource in both, and it ends with guidance on when each is the right call.
| Pulumi | OpenTofu | |
|---|---|---|
| Language | TypeScript, Python, Go, C#, Java, YAML | HCL |
| Model | Imperative program that builds a declarative graph | Declarative config |
| License | Apache 2.0 (CLI + SDKs) | Mozilla Public License 2.0 |
| Stewardship | Pulumi Corporation | Linux Foundation |
| Default state backend | Pulumi Cloud (SaaS, free individual tier) | Configured backend (S3, GCS, Azure Blob, etc.) |
| Self-managed state | Yes (S3, GCS, Azure Blob, local) | Yes (native) |
| Provider ecosystem | Native Pulumi providers + bridged Terraform providers | Terraform provider protocol (near-full compatibility) |
| Terraform compatibility | Convert via pulumi convert --from terraform, adopt via pulumi import | Drop-in for most Terraform 1.5 workflows |
| Testing | Native unit/integration tests in the host language | terraform test / third-party (Terratest, etc.) |
| State encryption | Secrets encrypted by default | Native state encryption since the fork |
Here is an AWS S3 bucket with versioning and a tag, written in Pulumi (TypeScript) and in OpenTofu (HCL).
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("assets", {
tags: { Environment: "production" },
});
new aws.s3.BucketVersioning("assets-versioning", {
bucket: bucket.id,
versioningConfiguration: { status: "Enabled" },
});
export const bucketName = bucket.id;
resource "aws_s3_bucket" "assets" {
tags = {
Environment = "production"
}
}
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
output "bucket_name" {
value = aws_s3_bucket.assets.id
}
For a single bucket the two are close in length and clarity. HCL is arguably tighter here, and that is HCL's strength: a flat resource declaration with no surrounding program.
The difference shows when logic enters. Suppose you need a bucket per team, with a policy that varies by team tier. In Pulumi you write a normal loop and a normal conditional:
import * as aws from "@pulumi/aws";
const teams = [
{ name: "payments", tier: "premium" },
{ name: "marketing", tier: "standard" },
];
for (const team of teams) {
const bucket = new aws.s3.Bucket(`${team.name}-assets`, {
tags: { Team: team.name, Tier: team.tier },
});
if (team.tier === "premium") {
new aws.s3.BucketLifecycleConfiguration(`${team.name}-lifecycle`, {
bucket: bucket.id,
rules: [{ id: "archive", status: "Enabled", /* ... */ }],
});
}
}
In HCL you reach for for_each, count, and conditional expressions:
variable "teams" {
default = {
payments = "premium"
marketing = "standard"
}
}
resource "aws_s3_bucket" "assets" {
for_each = var.teams
tags = {
Team = each.key
Tier = each.value
}
}
resource "aws_s3_bucket_lifecycle_configuration" "assets" {
for_each = { for k, v in var.teams : k => v if v == "premium" }
bucket = aws_s3_bucket.assets[each.key].id
# rule { ... }
}
Both work. The HCL version is a common source of friction because for_each, count, and ternary expressions are a constrained sub-language, and once the logic gets involved you hit the limits of what HCL will express cleanly. The Pulumi version is ordinary code, testable with the language's own tools, but it also means an infrastructure change now inherits everything that comes with a full programming language: runtime bugs, dependency upgrades, and behavior that varies between runs, all of which HCL's constraints were designed to keep out.
Neither tool escapes state. Both keep a state file that records the mapping between the resources you declared and the real resources in your cloud account, and both diff that state against reality on every plan.
OpenTofu stores state in a configured backend. In practice that is an S3 bucket with DynamoDB locking, a GCS bucket, Azure Blob, or a third-party runner like Spacelift or Scalr. You choose it, you own it, and you handle backup and access control. OpenTofu added native state encryption after the fork, so state at rest can be encrypted without a separate KMS-wrapped backend workflow.
Pulumi defaults to Pulumi Cloud, a hosted backend that stores state and provides history, RBAC, and a web console. It is free for individuals and paid for teams. If you would rather not depend on Pulumi's SaaS, you point the CLI at an S3, GCS, Azure Blob, or local backend with pulumi login s3://my-bucket, and you run entirely self-managed. Pulumi encrypts secret values inside state by default, using either a Pulumi Cloud key or a provider KMS you configure.
The practical difference: OpenTofu assumes you bring your own backend from day one, and Pulumi assumes you use its cloud unless you opt out. Both let you self-host. If your compliance posture forbids state leaving your accounts, both can satisfy it, but with Pulumi you have to actively choose the self-managed path.
This is where OpenTofu's compatibility bet pays off. OpenTofu speaks the same provider protocol as Terraform, so the thousands of existing Terraform providers run on it with no changes, and OpenTofu maintains its own registry alongside the Terraform Registry. If a provider exists for your service, OpenTofu almost certainly runs it.
Pulumi has native providers for the major clouds (AWS, Azure, GCP, Kubernetes) that are generated from the cloud APIs, plus a bridge that wraps Terraform providers so Pulumi can use them too. Coverage is broad. The bridged providers occasionally lag the native Terraform version by a release, and the ergonomics of a bridged provider are slightly less polished than a first-class Pulumi one, but for common infrastructure you are unlikely to hit a wall.
For teams with an existing Terraform or OpenTofu estate, the migration story matters:
tofu init against existing state and confirm a clean plan.pulumi convert --from terraform automates most of it (Pulumi reports 90 to 95 percent of typical programs converting without manual TODOs), and pulumi import can adopt existing resources into Pulumi state. It is a genuine conversion rather than a rename, and complex modules need manual review afterward.Both are more open than post-2023 Terraform, but for different reasons.
If your organization has a hard policy requiring OSI-approved licenses, both pass. If you specifically want foundation governance rather than a single commercial steward, OpenTofu is the closer fit. If you want a permissive engine license and are comfortable with a vendor whose business model is the hosted backend, Pulumi fits.
Choose OpenTofu if:
Choose Pulumi if:
HCL's constraints help when your infrastructure is mostly static declarations and get in the way when it is not. Pulumi's general-purpose languages help when you need real logic and cost you when a stray abstraction turns a one-line change into a debugging session. The deciding factor is how dynamic your infrastructure is, well ahead of which language you find more pleasant to read.
Both Pulumi and OpenTofu share a shape: infrastructure lives in a separate codebase from the application, tracked in a separate state file, applied by a separate tool. Your application code has no awareness of the database or queue it depends on, and the infrastructure definition has no awareness of how the application uses them. You keep the two in sync by hand.
For teams that would rather not manage state or a parallel infrastructure repository at all, a different model is worth knowing about. Encore is an open-source backend framework (around 12,000 GitHub stars, used in production by teams including Groupon) where you declare infrastructure directly in your application code, and Encore Cloud provisions the real resources in your own AWS or GCP account. There is no HCL file, no Pulumi program, and no state file to store or lock.
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { Topic } from "encore.dev/pubsub";
// Provisions managed Postgres (RDS on AWS, Cloud SQL on GCP, Docker locally).
const db = new SQLDatabase("orders", { migrations: "./migrations" });
// Provisions Pub/Sub (SNS+SQS on AWS, Pub/Sub on GCP).
export const orderPlaced = new Topic<{ orderID: string }>("order-placed", {
deliveryGuarantee: "at-least-once",
});
export const create = api(
{ method: "POST", path: "/orders", expose: true },
async (req: { customerID: string }) => {
const row = await db.queryRow`
INSERT INTO orders (customer_id) VALUES (${req.customerID}) RETURNING id
`;
await orderPlaced.publish({ orderID: row.id });
return row;
},
);
This does not replace Pulumi or OpenTofu for every job. Teams managing large VM fleets, complex networking, or IAM at scale will keep using an IaC tool, and Encore itself has an encore build docker path that produces a standalone image for teams that want to leave. What it replaces is the configuration sprawl and state management that grow around an application-level backend. If your infrastructure is mostly the databases, queues, cron jobs, and storage your services reach for, defining it in the application code removes the second codebase entirely.
Skip the separate IaC codebase and state file. Deploy a TypeScript backend with Postgres and Pub/Sub to your own AWS or GCP account in minutes.
Yes. OpenTofu uses the same provider protocol as Terraform, so the vast majority of Terraform providers run unchanged. OpenTofu also runs its own registry alongside the Terraform Registry.
Yes. Pulumi bridges many Terraform providers, and it can convert existing HCL with `pulumi convert --from terraform` and adopt live resources into Pulumi state via `pulumi import`. Coverage is broad but not identical to the native Terraform ecosystem.
The Pulumi CLI and SDKs are Apache 2.0. Pulumi Cloud, the default state and secrets backend, is a paid SaaS with a free individual tier. You can self-manage state in S3, GCS, or Azure Blob instead.
HashiCorp relicensed Terraform under the Business Source License in August 2023. A group of vendors and community members forked the last MPL-licensed release into OpenTofu, now hosted under the Linux Foundation.
Neither removes state. Both track a state file that maps declared resources to real cloud resources. Pulumi defaults to Pulumi Cloud for storage; OpenTofu uses a configured backend such as S3. You still manage locking, backups, and access.
Pulumi, if you already write TypeScript, Python, Go, or C#, because you reuse the language, its tooling, and its testing frameworks. OpenTofu's HCL is a smaller surface but a new language to learn.