Pulumi vs Terraform: choosing your IaC tool

Real programming languages vs HCL: how state handling, unit testing, blast radius, and your team's skills should decide between Pulumi and Terraform.

Jul 30, 2024·Updated ·5 min readBeginner·By SecOpsLog · documentation-verified

Both tools do the same thing at the core: a program describes resources, the engine diffs that description against a state file that maps names to cloud ids, and a plan shows what would change before anything does. The difference that gets argued about is the language: HCL, a declarative configuration language, against TypeScript, Python, Go or C#. The differences that decide whether a team is still happy in five years are elsewhere, and most of them are about who reads the plan and who maintains the state.

What differs, and what does not

DimensionTerraformOpenTofuPulumi
languageHCLHCL (a fork from 1.5.x)TypeScript, Python, Go, C#, Java, YAML
licenceBUSL 1.1 since 1.6: use is free, competing hosted services are notMPL 2.0 under the Linux FoundationApache 2.0 for the CLI and SDKs; Pulumi Cloud is a product
current release1.161.123.262, pulumi-aws 7.x
statea file in a backend, locked (S3 lockfile, HCP Terraform)the same backends, plus client-side state encryptionPulumi Cloud, or a self-managed backend such as S3
secrets in stateplaintext unless the backend encrypts at restend-to-end state encryption in the toolsecret values encrypted per stack in the state file
testingterraform test with run blocks against a plan or an applythe sameunit tests with mocks in the host language, plus integration tests
who reads the plananyone who can read HCL, including reviewers who write no codethe samesomeone who reads the language; loops and helpers can hide what the plan will contain
ecosystemthe registry, the module culture, the hiring poolthe same providers and modulesthe same providers through bridging, plus native ones

Three things are missing from the table because they do not differ, and they decide more incidents than the language does. State is a file that contains every attribute the provider ever read back, passwords included, and it needs a lock and encryption at rest in all three tools. The provider is the same code: Pulumi's AWS package is bridged from the Terraform provider, so a provider bug, a deprecation or a new resource arrives in both at about the same time. And the blast radius of a wrong apply is identical, because the API call that deletes a database does not care which language asked for it.

The same bucket, twice

main.tf
resource "aws_s3_bucket" "app" {
bucket = "acme-app-data"
}
resource "aws_s3_bucket_versioning" "app" {
bucket = aws_s3_bucket.app.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "app" {
bucket = aws_s3_bucket.app.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
__main__.py
import pulumi_aws as aws
bucket = aws.s3.Bucket("app", bucket="acme-app-data")
aws.s3.BucketVersioning(
"app-versioning",
bucket=bucket.id,
versioning_configuration={"status": "Enabled"},
)
aws.s3.BucketPublicAccessBlock(
"app-public-access",
bucket=bucket.id,
block_public_acls=True,
block_public_policy=True,
ignore_public_acls=True,
restrict_public_buckets=True,
)

The two files produce the same three API calls, and the plan in each tool says so. The Python version could be wrapped in a function secure_bucket(name) that every team calls, with a unit test asserting the public-access block is always set; the HCL version does the same with a module and a terraform test file. The Python version can also contain a while loop that creates buckets until an API call returns something, which no reviewer will spot in a plan. That is the trade in one sentence: a general-purpose language gives you tests and abstractions and gives everyone else the ability to write anything.

bash — the plan is the review artefact in both
terraform plan -no-color | tail -1
Plan: 3 to add, 0 to change, 0 to destroy.
pulumi preview --diff | tail -3
Resources:
+ 3 to create
same three resources; the question is which output your reviewers can read without the author in the room

Deciding for the organisation, not for the pilot

A pilot answers whether the tool works; it cannot answer whether the fourth team to adopt it will be able to review the third team's stacks. The questions that predict that are about people and blast radius. Who reviews infrastructure changes, and can they read the language? Where does platform code live: next to application code in a service repository (which favours the application team's language) or in a platform repository owned by an infrastructure team (which favours the declarative language and the registry)? How is a bad change caught: by a reviewer reading a plan, or by a test suite the author wrote? And when the licence question comes up in procurement, is a BUSL tool acceptable, or does the MPL fork remove a conversation?

Signals that point one way
Toward HCL (Terraform or OpenTofu)
A platform team owns infrastructure in its own repository
Reviewers are operators, not application developers
The registry modules already cover most of the estate
Licence matters: OpenTofu keeps the HCL and drops the BUSL
Toward Pulumi
Application teams own their infrastructure, in their language
Unit tests on infrastructure logic are a requirement, not a wish
Self-service platforms built on the Automation API
Shared libraries for naming, tagging and policy are wanted in code
Two tools in one organisation is the expensive answer
Every stack has to be reviewed, scanned, state-managed and upgraded in whichever tool it uses, and migrating a stack between tools means exporting state, rewriting resources and re-importing under a maintenance window. Pick a primary, allow the other only where a team owns its runtime end to end, and write down which is which so the next hire does not start a third.

Whichever language wins, the things that break are the same in all three tools and are not language features: remote state with a real lock, secrets that stay out of plaintext state, a policy gate that runs before apply, and modules or libraries with a versioned contract. The Terraform-versus-OpenTofu half of the question, licence and locking included, is its own comparison.

Related posts

Quick reference