# Pulumi or Terraform?

> We're putting our infrastructure in code. Pulumi or Terraform, and what if we already run one of them? Pulumi for new work: real code, state in your own S3 bucket, and every Terraform provider through its bridge, so it costs no coverage. Terraform when the client's team already runs it well; I keep it. Often both: Pulumi already runs on Terraform's providers, and new pieces go in Pulumi next to a client's existing Terraform, each resource with one owner.

- A choice between tools from The Stack by Nicolas Dao (Cloudless Labs, Sydney): when to pick each, and when to run both
- Web page: https://cloudlesslabs.com/stack/guides/pulumi-or-terraform/. The whole stack in one file: https://cloudlesslabs.com/stack/brief.md
- Tools: [Pulumi](https://cloudlesslabs.com/stack/pulumi/index.md) (My default), [Terraform / OpenTofu](https://cloudlesslabs.com/stack/terraform/index.md) (Use when it fits), [AWS CloudFormation](https://cloudlesslabs.com/stack/aws-cloudformation/index.md) (Never), [GitHub Actions](https://cloudlesslabs.com/stack/github-actions/index.md) (Use when it fits)

## When to pick which

Pick Pulumi when:

- New work with no Terraform history: the same providers, in a real language.
- Infrastructure with real logic: resources generated per tenant, shared components with options.
- One codebase across vendors: AWS, Cloudflare, GitHub, Tailscale, Neon, Resend, PostHog, OpenRouter, Stripe.

Pick Terraform when:

- The client's team already runs Terraform.
- A platform team has working modules, policies and review habits in HCL: rewriting infrastructure that works is a waste of time.
- OpenTofu rather than Terraform when an open-source licence matters to the client, or you want state encryption built in.

Both, when:

- Pulumi already uses Terraform's providers: anything with a Terraform or OpenTofu provider is provisionable from Pulumi code.
- At a client that runs Terraform, it keeps what it owns and new pieces go in Pulumi, which reads outputs straight from Terraform state.
- Never two owners for one resource. When a module is worth moving, convert it and import its resources into Pulumi, then Terraform lets it go.

Questions that change the answer:

- Does the team already run Terraform, and run it well? Then I keep it and add Pulumi only for new pieces.
- Does the infrastructure have logic: per-tenant resources, shared components with options? count, for_each and dynamic blocks get painful fast.
- Does an open-source licence matter to the client? Terraform is under the Business Source License; OpenTofu is MPL, under the CNCF.
- Who runs the applies: you on a small stack, or CI on every merge? It decides whether GitHub Actions is in the picture.

## The short version

| | Pulumi | Terraform / OpenTofu | CloudFormation |
|---|---|---|---|
| Verdict | My default | Use when it fits | Never |
| Written in | Real code | HCL configuration | YAML or JSON templates |
| Logic | Loops and conditions in the language | count, for_each, dynamic blocks | `Fn::If`, `Fn::Sub`, `Conditions` |
| Reach | Every Terraform or OpenTofu provider, bridged | Every provider | AWS only |
| State | Your own S3 bucket, no Pulumi Cloud fees | S3 with `use_lockfile`, or HCP Terraform | Held by AWS |
| Secrets in state | Encrypted with KMS | Plaintext, sensitive values included; OpenTofu can encrypt | No state file |
| Licence | Open source | Terraform BSL since August 2023; OpenTofu MPL, CNCF since April 2025 | AWS service |
| Price | Free with an S3 backend | Both CLIs free; HCP Terraform free up to 500 resources | Free for AWS resource types |

Price doesn't decide this one: all three are free to run the way I run them. The language, the reach and the state decide it.

## Why Pulumi is my default

Every piece of infrastructure I build is defined in [Pulumi](https://cloudlesslabs.com/stack/pulumi/index.md), with state in S3, and I've had no unsolved pain with it. Terraform is config files; Pulumi is real code. When infrastructure has logic, a loop is a loop and a shared component is a function with options.

The part people miss is that Pulumi can use any Terraform provider. One command, `pulumi package add terraform-provider`, generates an SDK from it. So choosing Pulumi costs me no coverage, only HCL. That's what lets me say "if I can't provision it, it loses points" and still cover nearly the whole market. [Pulumi everywhere](https://cloudlesslabs.com/stack/guides/pulumi-everywhere/index.md) shows one app across six providers.

What it asks of you: pin community providers, which are often one person's project (Neon, Resend, Clerk). Use `@pulumi/aws-native` next to the classic provider for new AWS services that land there first. Build Cloudflare Workers with Wrangler and deploy the output with Pulumi, with one owner per Worker.

## When I keep Terraform

[Terraform](https://cloudlesslabs.com/stack/terraform/index.md) is fine, and its providers power half of Pulumi's ecosystem. I tried it and prefer Pulumi, but when a client already runs it, I keep it. Rewriting working infrastructure to suit my taste is the kind of consultant waste I criticise. A team that's happy and fluent in Terraform doesn't need me to move it.

Terraform or OpenTofu? Both work, and the licence change doesn't stop a client using Terraform. For a new HCL codebase I'd lean OpenTofu: an open-source licence under the CNCF, and state encryption built in. Pick one per codebase, pin its version in CI and on every laptop, and never run both binaries on the same state.

Its traps: state holds every secret in plaintext, so it goes in an encrypted S3 bucket with tight access. The S3 lockfile that replaces DynamoDB locking is off by default: set `use_lockfile = true`. And commit `.terraform.lock.hcl`, with an exact version on every module, or modules upgrade themselves on the next init.

## When it's both

Side by side, not a rewrite, and every resource has one owner. The client's Terraform keeps what it owns, new work starts in Pulumi on the same providers, and Pulumi reads Terraform's outputs from its state, so nobody copies values by hand. When a module is worth moving, `pulumi convert --from terraform` translates the HCL (Pulumi expects 90–95% to convert cleanly and marks the rest), and `pulumi import --from terraform` adopts the live resources without recreating them.

Either way, the pipeline is the same. With [GitHub Actions](https://cloudlesslabs.com/stack/github-actions/index.md), every pull request runs a preview (`pulumi preview` posted as a comment, or a plan), every merge applies, and a concurrency group per stack stops two applies fighting over one lock. On a small stack I still read the preview and run `pulumi up` myself.

## Why not CloudFormation

[CloudFormation](https://cloudlesslabs.com/stack/aws-cloudformation/index.md) is Never. It does real things well: AWS holds the state, rollback is automatic, StackSets reach every account. But templates are configuration pretending to be code, they hit hard limits, and they stop at AWS's edge while every client also has Cloudflare, Neon, GitHub or Tailscale. The CDK fixes the format with real code, but it compiles to CloudFormation and inherits the same engine, limits and stuck rollbacks.

When I meet it at a client, I don't rip it out in a week. A vendor's template gets deployed from Pulumi as one resource (`aws.cloudformation.Stack`). Old stacks get imported into Pulumi one at a time, then deleted with their resources retained.

Sources, checked on 25 October 2026: [use any Terraform provider](https://www.pulumi.com/docs/iac/concepts/providers/any-terraform-provider/), [Terraform S3 backend](https://developer.hashicorp.com/terraform/language/backend/s3), [sensitive data in Terraform](https://developer.hashicorp.com/terraform/language/manage-sensitive-data), [OpenTofu state encryption](https://opentofu.org/docs/language/state/encryption/), [migrating from Terraform to Pulumi](https://www.pulumi.com/docs/iac/guides/migration/migrating-to-pulumi/from-terraform/), [CloudFormation quotas](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html), [migrating from CloudFormation to Pulumi](https://www.pulumi.com/docs/iac/guides/migration/migrating-to-pulumi/from-cloudformation/), [Pulumi with GitHub Actions](https://www.pulumi.com/docs/iac/guides/continuous-delivery/github-actions/).

## Related

- [Pulumi](https://cloudlesslabs.com/stack/pulumi/index.md): the full entry (verdict, pricing, gotchas)
- [Terraform / OpenTofu](https://cloudlesslabs.com/stack/terraform/index.md): the full entry (verdict, pricing, gotchas)
- [AWS CloudFormation](https://cloudlesslabs.com/stack/aws-cloudformation/index.md): the full entry (verdict, pricing, gotchas)
- [GitHub Actions](https://cloudlesslabs.com/stack/github-actions/index.md): the full entry (verdict, pricing, gotchas)
- [Every choice between tools](https://cloudlesslabs.com/stack/choices/index.md)
- [Map of every Markdown file on cloudlesslabs.com, with what each one answers](https://ask.cloudlesslabs.com)
