Terraform or Pulumi, Pick From the Team You Have
Blog article/Blog archive

Terraform or Pulumi, Pick From the Team You Have

Both tools preview a change, apply it, and write state. Two terminals show the same deploy. Pick from the team that has to review it.

Oct 19, 202613 min read0 comments2 views
TerraformAWSDevops

The argument started as a logo fight. Terraform on one side, Pulumi on the other, as if the cloud would behave once we picked the right binary.

Both tools do the same loop. You describe the resources. You preview. You apply. State records what actually exists. The two terminals below run that deploy side by side for the same S3 bucket. Speed and reset sit at the bottom.

Terraform HCL and Pulumi TypeScript both feed one preview step, then apply to the same S3 bucket, and both record what exists in a private, locked remote state
Same loop either way: describe it, preview it, apply it, record it in state. Only the language you describe it in changes.
Animated demo of the interactive figure

This figure is interactive on the original post, where you can play with it live. Open the interactive demo on harun.dev.

The loop is the product

I stop people when the debate is only about HCL versus TypeScript. The product is the loop. Pulumi can now run existing Terraform HCL too (runtime: hcl), so even the language is no longer a hard line:

Step Terraform Pulumi What you are checking
Describe main.tf / modules index.ts (or Python, Go, HCL, …) Desired state is readable by the people who must review it
Preview terraform plan pulumi preview Creates, updates, and deletes before anything changes
Apply terraform apply pulumi up Cloud APIs run with credentials you intended
State terraform.tfstate (remote) stack state (remote) Real IDs, and often secrets, stay locked down

Miss the preview and you get surprise destroys. Miss the state lock and two applies race. Miss who can read state and you leak database passwords. The CLI brand does not fix those.

Same bucket, two descriptions

Here is an S3 bucket both ways. Prefixes stay hrr_.

resource "aws_s3_bucket" "hrr_uploads" {
  bucket = "hrr-uploads-example"

  tags = {
    Project = "hrr-demo"
  }
}
import * as aws from "@pulumi/aws";

const hrrUploads = new aws.s3.Bucket("hrrUploads", {
  bucket: "hrr-uploads-example",
  tags: {
    Project: "hrr-demo",
  },
});

Same object in AWS after apply. The review question is who can read the diff in the pull request. If half the reviewers do not write TypeScript, the Pulumi preview is noise. Pulumi can also run your existing .tf files through its HCL runtime, which softens that gap. It is new, so try it on a non-critical stack first. If the app team already owns the account and the repo, HCL is the foreign language.

State does not care which CLI wrote it

Both tools need a memory of what they created. That memory is more sensitive than a .env file, and switching CLIs does not delete the risk. It renames the file.

The Terraform state caveat

terraform.tfstate is JSON that maps your configuration to real cloud objects. After an apply it commonly holds:

  • resource IDs, ARNs, and relationships across the stack
  • database usernames and passwords passed as resource arguments
  • IAM access key secrets Terraform created
  • private keys from the TLS provider
  • API tokens, webhook URLs, and Lambda environment variables

Marking a variable sensitive = true hides it from normal CLI output. It does not remove the value from state. Terraform needs those attributes to plan the next diff. Treat every production state snapshot as if it contains credentials, even when last month's file looked clean. Providers change. Modules grow. A data source you used while debugging can leave a secret behind.

Two operators applying at once without a lock can corrupt that file. A state file in git, in a CI artifact, or in a bucket every developer can read is a credential dump with a map of your account attached. I wrote the fuller Terraform treatment in Why Your Terraform State Is More Sensitive Than Your .env File.

My minimum remote backend looks like this:

terraform {
  backend "s3" {
    bucket       = "hrr-terraform-state"
    key          = "demo/production/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

Dedicated bucket. Encryption on. Lock on. Read access only for the people and CI roles that must run plan and apply. Not the uploads bucket. Not a laptop copy "just for today."

The Pulumi equivalent

Pulumi's equivalent is stack state. Same job as Terraform state: remember which cloud objects belong to this stack so the next preview and up are honest.

Where it lives depends on the backend you chose: Pulumi Cloud, an S3/DIY backend, or another supported store. The product name changes. The contents do not get safer. Stack state still carries resource identities and can carry secrets from configuration and provider outputs. Pulumi can encrypt secret values in state and config, which is useful. Encryption at rest is not the same as "anyone on the team can download the stack export."

So I apply the same rules:

  • Do not keep stack state on a developer laptop as the source of truth.
  • Use a remote backend you can lock and audit.
  • Limit who can read the stack, export it, or decrypt secrets.
  • Prefer a passphrase or KMS-backed secrets provider you control, not a shared Slack password.

What is not the same: the file format, the CLI commands, and the migration path. You do not "rename" terraform.tfstate into a Pulumi stack and call it done. Importing or rebuilding state is a project of its own. The caveat that transfers is the security model, not the bytes on disk.

In the terminals above, both sides end on a locked remote state line for a reason. The green "1 added" / "1 created" is incomplete until that record is somewhere you would trust with a production password.

Preview is the review

terraform plan and pulumi preview are the diff. I do not apply from a laptop against production without that diff in the pull request. CI should run the preview on every change that touches infra. Humans read the create/update/delete list. The language of the description changes who can read it. It does not remove the need for one.

When the plan says destroy on a database, stop. When the preview is empty and you expected a change, stop. The green "1 added" / "1 created" lines in the terminals only show up after that gate. Apply is only honest after it.

Which one should you pick?

Pick from the team you have, not the conference talk you liked.

  • Stay on Terraform when infrastructure reviewers already write and review HCL, when you have a module library that works, and when operators live in the Terraform workflow.
  • Choose Pulumi when the same people already own the app and the account, when loops and types would cut real duplication, and when the preview will be read by TypeScript (or Python) developers.

I have used both. The outages I remember came from state that anyone could read, and from an apply that nobody previewed. They did not come from the extension on the file.

Hope you enjoyed this one. If your team is mid-argument about this, find me on X at https://x.com/harundotdev, and grab every other link from my bio at https://harun.dev/bio.

Get the next note

I email when a new post goes up. One send a week, and only if there's something new.

Want this applied on your account? Start with Infrastructure as Code.

Related reading

More posts, sliding underneath the article

Kept below the post instead of in a sidebar, with a slow continuous motion for a cleaner editorial feel.

On this post

Comments

A reply stays under the note it answers.

0 comments

No comments yet.

If you have a note on Terraform or Pulumi, Pick From the Team You Have, sign in and leave it.