Originally published on DevToolHub.

HCL and Python programs each pass through a state file to the same cloud providers

Choose Terraform when your team wants one declarative language that every cloud engineer already reads. Choose Pulumi when you want loops, tests and secrets handled in a real programming language. The terraform vs pulumi decision matters less for what each tool can build, because both deploy to AWS, Azure and GCP. It matters more for how they store state and survive refactors.

I ran the same resources through both tools to see where they differ. I used OpenTofu 1.13.9 and Pulumi 3.274.1 with Python. Nothing touched a cloud account. I used the random provider, so every resource lives only in state.

What Is the Difference Between Terraform vs Pulumi?

Terraform describes infrastructure in HCL, a configuration language built for this job. Pulumi lets you describe it in Python, TypeScript, JavaScript, Go, .NET or Java. Pulumi's own comparison page lists those languages, plus YAML and HCL. It says Terraform "uses HCL exclusively."

Terraform / OpenTofu Pulumi
Language HCL Python, TypeScript, Go, .NET, Java, YAML
Default state Local file, or a remote backend you configure Pulumi Cloud, with self-managed backends available
Secrets in state Plain text unless you add encryption Encrypted by default
Rename a resource safely moved block aliases option
Licence Terraform CLI is BSL 1.1, OpenTofu is MPL 2.0 Apache 2.0
Tests terraform test, Terratest Your normal test framework

The state and licence rows come from Pulumi's page, so they reflect Pulumi's view of Terraform. I checked the secrets row myself in the test below. For the Terraform fork question, see OpenTofu vs Terraform.

How Does the Same Config Look in Terraform vs Pulumi?

Both tools express a keyed loop, a generated secret and an output in about 15 lines. I built three random_pet names, one for each of dev, staging and prod. I also created one random_password. Here is the HCL:

variable "envs" { type = list(string) default = ["dev", "staging", "prod"] } resource "random_pet" "name" { for_each = toset(var.envs) length = 2 prefix = each.key } resource "random_password" "db" { length = 16 } output "names" { value = { for k, v in random_pet.name : k => v.id } } 

And the Pulumi program in Python:

import pulumi import pulumi_random as random envs = ["dev", "staging", "prod"] names = {e: random.RandomPet(f"name-{e}", length=2, prefix=e) for e in envs} db = random.RandomPassword("db", length=16) pulumi.export("names", {e: p.id for e, p in names.items()}) 

The preview output is where you feel the difference first. tofu plan reported Plan: 4 to add, 0 to change, 0 to destroy. and showed every attribute. pulumi preview printed one line per resource and reported 5 to create. The fifth is the stack itself.

Changing the list to ["dev", "qa", "prod"] gave the same result in both tools. One resource was created and one destroyed, and dev and prod stayed untouched. So keyed loops behave the same either way.

Where Does Each Tool Keep Secrets in State?

OpenTofu wrote the generated password into terraform.tfstate as plain text. Pulumi stored the same value as ciphertext. This was the biggest practical difference in my test. After tofu apply, I opened the state file and found the random_password entry:

"result": "cVq-B3MvW+QCGYhA", "length": 16, 

The file also listed result under sensitive_attributes. That flag only hides the value in CLI output. It does not encrypt anything. Pulumi's docs say the same about Terraform: "Sensitive values are not encrypted in the state file."

The Pulumi state for the same resource looked like this:

"result": { "4dabf18193072939515e22adb298388d": "1b47061264138c4ac30d75fd1eb44270", "ciphertext": "v1:m+xLJWdYPNumRzyw:O6sab+Z3GWlcV4j8llyJu2PIfm8YPduK2psiVe6xVmC/YA==" } 

⚠️ Note: I ran Pulumi against a local file backend with an empty passphrase. That proves the field gets encrypted, not that the key is well protected. In real use, pick Pulumi Cloud or a KMS-backed secrets provider.

You can close the gap on the OpenTofu side. OpenTofu's state encryption docs show a terraform { encryption { ... } } block with key providers such as PBKDF2, AWS KMS, GCP KMS and Azure Vault. I did not test it. The point is that encryption is opt-in there and automatic in Pulumi.

For more on what lives in a state file, read Terraform state explained.

What Happens When You Rename a Resource?

A rename destroys and recreates the resource in both tools unless you tell them the old identity. I renamed db to database in each project. Without any help, OpenTofu planned random_password.database will be created and random_password.db will be destroyed. Pulumi showed a create and a delete for the same pair.

For a database or a bucket, that plan is how people lose data. Each tool has a fix, and both worked in my test. In OpenTofu you add a moved block, per the Terraform docs:

moved { from = random_password.db to = random_password.database } 

The plan then read random_password.db has moved to random_password.database and Plan: 0 to add, 0 to change, 0 to destroy. In Pulumi you add an alias, as described on the aliases page:

db = random.RandomPassword( "database", length=16, opts=pulumi.ResourceOptions(aliases=[pulumi.Alias(name="db")]), ) 

The rename disappeared from the preview, and only the qa/staging change from the earlier test remained. Pulumi's docs say you can remove the alias once every stack has migrated.

Can You Convert Terraform to Pulumi?

Yes. pulumi convert --from terraform --language python reads a directory of .tf files and writes a Pulumi program. I ran it on the HCL above. It produced a working-looking __main__.py, and a pulumi preview of the result showed the same 5 to create.

The output reads like a translation, not like code a person would write. It turned my for_each into a loop over sorted(...) items and wrapped the list in config.get_object("envs"). You will want to tidy it by hand.

One part failed on my machine. The converter tried to pip install its dependencies into my system Python 3.9 and hit a resolver conflict. The program was still generated, and I ran it in a virtualenv instead. Use --generate-only if you want to manage dependencies yourself.

For a first Pulumi project in Python, see creating an AWS S3 bucket with Pulumi. The older Pulumi overview covers the concepts.

Terraform vs Pulumi: Which Should You Choose?

Pick the tool that matches how your team already works. The test results point to three clear rules:

  • Choose Terraform or OpenTofu if most of your team writes infrastructure but not application code. HCL is small, and every cloud engineer you hire has probably read it. Pick OpenTofu if the BSL licence is a problem for your company.
  • Choose Pulumi if you want real loops, functions and unit tests, or if your platform team already writes Python or TypeScript. Encrypted secrets by default is the other strong reason.
  • Stay where you are if a working Terraform setup has no pain. Converting 200 modules costs more than any language benefit, and moved and aliases solve the same refactor problem.

Both support AWS, Azure and GCP through providers, so the cloud you use rarely decides this. The pace of the tools is similar. As of November 2026, the latest releases on GitHub were Terraform 1.17.2 (November 11), Pulumi 3.274.1 and OpenTofu 1.13.9 (both November 5).

What I Did Not Test

I did not run Terraform itself. The HCL ran on OpenTofu 1.13.9, which forked from Terraform, so I expect the plan output to match. Treat the Terraform-side claims as coming from its docs. I did not test state encryption in OpenTofu, Pulumi Cloud, remote backends, or any real cloud provider. I also did not test terraform test or Pulumi's unit-test mocks. All three need an account or more setup than this guide covers.

Frequently Asked Questions

Q: Is Pulumi better than Terraform?
A: Neither is better in general. Pulumi wins when you want a general-purpose language and encrypted state by default. Terraform wins when you want one small declarative language, a huge module ecosystem and the easiest hiring. In my test both handled loops and renames equally well.

Q: Does Pulumi use Terraform providers?
A: Often, yes. Pulumi's docs describe a Terraform bridge that adapts an existing Terraform provider into a Pulumi provider. In my test, the random provider ran as pulumi-random 4.21.6. Check that the provider you need has a Pulumi package before you commit.

Q: Can Terraform encrypt its state file?
A: Not by default. My OpenTofu run left a generated password in plain text in terraform.tfstate. OpenTofu has an opt-in encryption block for state and plan files. Pulumi's comparison page notes that HCP Terraform encrypts state at rest.

Q: How do I migrate from Terraform to Pulumi?
A: Run pulumi convert --from terraform --language python in your Terraform directory. It generates a Pulumi program from your .tf files, which you then review and test. It does not move your existing state, so plan the cutover per stack.

Q: Is OpenTofu the same as Terraform?
A: Close, but not identical. OpenTofu forked from Terraform 1.6.x under the MPL 2.0 licence and has since added features such as state encryption. My HCL ran unchanged, but check any version-specific syntax.

Quick Summary:

  • Terraform uses HCL only, and Pulumi supports Python, TypeScript, Go, .NET and Java
  • OpenTofu left a generated password in plain text in state, and Pulumi stored it encrypted
  • A rename without help plans a destroy and create in both tools
  • moved blocks in OpenTofu and aliases in Pulumi both turned the rename into zero changes
  • pulumi convert --from terraform produced a runnable Python program from my HCL
  • I tested OpenTofu 1.13.9 and Pulumi 3.274.1, not Terraform itself, and no real cloud resources

Before you settle terraform vs pulumi, take one real module and run it both ways. Rename a resource in it and check which tool warns you before it recreates anything.