
Why Choose Pulumi Over Terraform?
Terraform is a proven infrastructure as code tool with a large provider and module ecosystem. Many teams choose Pulumi when they want to keep that infrastructure as code model, but write and maintain infrastructure with general-purpose programming languages, familiar package managers, IDEs, testing, and software engineering patterns, while still understanding the refactoring tradeoffs in Terraform’s own module refactoring guidance.
Why choose Pulumi over Terraform? Pulumi’s language SDKs let teams define cloud infrastructure in TypeScript, Python, Go, C#, Java, or YAML while adding first-class workflows for refactoring with Pulumi aliases, secrets, protect, retainOnDelete, deleteBeforeReplace, replaceOnChanges, provider resources, Pulumi stacks, testing, and incremental migration with pulumi import. Pulumi does not remove every hard problem in cloud infrastructure, but it gives teams stronger tools for many day-to-day pain points.
Executive summary
Pulumi is often a better fit when infrastructure code needs to behave like application code: reviewed, tested, packaged, refactored, and shared across teams. The biggest advantages show up when teams need safer refactors, encrypted secret values, reusable components, clearer provider resources, and guardrails around destructive changes.
The tradeoff is important: Pulumi is still an infrastructure as code engine. Provider bugs, cloud API eventual consistency, drift, preview-time unknowns, and poorly designed abstractions still require engineering discipline. The advantage is not magic. The advantage is a stronger programming model and a more familiar developer workflow.
| Need | Terraform pattern | Pulumi advantage | What still needs care |
|---|---|---|---|
| Languages and tooling | HCL plus Terraform-specific functions and expressions | Pulumi supports general-purpose languages, YAML, and now HCL natively, with the normal IDE, test, and package workflows for each | Teams still need code review and shared conventions |
| Refactoring | Moved blocks or state commands for resource identity changes | Pulumi aliases can map old resource identities to new ones during refactors | Aliases must model the old identity correctly |
| Secrets | Sensitive values can still require careful state and plan handling | Pulumi tracks secrets and encrypts secret values in state | Secrets are still available to code at runtime |
| Lifecycle safety | Lifecycle meta-arguments and plan review | Pulumi resource options such as protect, retainOnDelete, deleteBeforeReplace, and replaceOnChanges make intent explicit | Provider behavior and replacement semantics still matter |
| Environments | Workspaces or separate configurations | Pulumi stacks model environments with per-stack config, secrets, history, and outputs | Stack boundaries still need thoughtful design |
| Code reuse | Modules and HCL composition patterns | Pulumi components and packages use normal language abstractions | Over-abstracted components can become hard to use |
| Imports and migration | Import blocks, generated config, and state operations | pulumi import and migration tooling support gradual adoption | Imported code still needs review and cleanup |
| Terraform state | S3, Azure Blob, or Terraform Cloud backends | Pulumi Cloud can serve as a Terraform state backend with encrypted storage, locking, history, and RBAC, while you keep using the Terraform or OpenTofu CLI | State still needs a clean migration plan as resources move to Pulumi |
| Provider wiring | Provider inheritance and aliases inside modules | Explicit provider resources make multi-region and multi-account wiring visible in code review | Provider versions and bugs can still affect deployments |
| Testing | Validation, plan review, and external test harnesses | Pulumi programs can use normal unit and integration test frameworks | Tests complement previews, they do not replace them |
| Drift detection | Refresh and refresh-only plans, plus external scheduling | pulumi refresh reconciles state, and Pulumi Cloud adds scheduled drift detection and remediation on a configurable cadence | Detection still depends on provider behavior and a healthy state backend |
| Caveats | Declarative planning still has unknowns and drift | Pulumi improves the workflow around many pain points | It does not eliminate provider bugs or eventual consistency |
Use programming languages and familiar tools
Terraform modules are powerful, but larger HCL codebases can require teams to maintain separate conventions for composition, validation, and reuse. Pulumi lets infrastructure teams use the features of whichever supported programming language they choose, such as classes, functions, types, loops, package managers, linters, and test frameworks.
Teams with HCL muscle memory are not left out, either. Pulumi now supports HCL natively as one of its languages, alongside Python, TypeScript, Go, C#, Java, and YAML. Part of a team can keep authoring in HCL while the project still benefits from Pulumi components, testing, and the same engine and package workflows as every other language.
For example, a platform team can wrap a standard storage pattern in a ComponentResource and share it like any other TypeScript abstraction:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface WorkQueueArgs {
visibilityTimeoutSeconds: number;
}
class WorkQueue extends pulumi.ComponentResource {
public readonly queueUrl: pulumi.Output<string>;
constructor(name: string, args: WorkQueueArgs, opts?: pulumi.ComponentResourceOptions) {
super("platform:queue:WorkQueue", name, {}, opts);
const queue = new aws.sqs.Queue(`${name}-queue`, {
visibilityTimeoutSeconds: args.visibilityTimeoutSeconds,
}, { parent: this });
this.queueUrl = queue.url;
this.registerOutputs({ queueUrl: this.queueUrl });
}
}
const jobs = new WorkQueue("image-jobs", {
visibilityTimeoutSeconds: 60,
});
Refactor infrastructure without surprise replacement
Renaming a resource, moving it into a component, or reorganizing a project should not automatically mean replacing production infrastructure. Pulumi aliases let you tell Pulumi how a resource used to be addressed, so refactors can preserve resource identity when the old and new resources represent the same cloud object.
const queue = new aws.sqs.Queue("app-jobs", {
visibilityTimeoutSeconds: 60,
}, {
aliases: [{ name: "jobs" }],
});
Aliases are not a substitute for review. They work when the old identity is modeled correctly, including details such as name, parent, type, project, and stack when those changed.
Handle secrets with encrypted configuration and secret outputs
Secrets are one of the most common places where infrastructure workflows become risky. Terraform has sensitive values and backend guidance, but teams still need to understand how plans, state, outputs, and backend access interact. Pulumi treats secrets as first-class values, encrypts them in state, and preserves secrecy as values flow through outputs.
const config = new pulumi.Config();
const dbPassword = config.requireSecret("dbPassword");
const passwordParameter = new aws.ssm.Parameter("db-password", {
type: "SecureString",
value: dbPassword,
});
This improves the default experience, but it’s not runtime isolation. In the TypeScript SDK, config.requireSecret("dbPassword") retrieves secret configuration, and your program can still access the decrypted value while it runs, so reviews, least privilege, and secret-provider choices still matter. If a value starts as a plain input instead of coming from requireSecret, pulumi.secret(...) can mark it as secret.
For secrets that shouldn’t live in stack config at all, Pulumi ESC (Environments, Secrets, and Configuration) centralizes them in environments that your stacks consume through config. ESC can pull secrets dynamically from external stores such as HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, and 1Password, and mint short-lived OIDC credentials for AWS, Azure, and GCP. That lets you wire an environment into a Pulumi program instead of storing long-lived credentials in state.
Add safer lifecycle controls for destructive changes
Infrastructure mistakes are expensive when they replace or delete stateful resources. Pulumi gives teams explicit resource options for safety-sensitive intent, such as protect, retainOnDelete, deleteBeforeReplace, and replaceOnChanges.
const database = new aws.rds.Instance("orders-db", {
instanceClass: "db.t4g.micro",
allocatedStorage: 20,
engine: "postgres",
username: "app",
password: dbPassword,
}, {
protect: true,
retainOnDelete: true,
});
These controls are guardrails, not guarantees. Provider bugs, cloud API eventual consistency, and replacement behavior still require preview review and operational judgment. replaceOnChanges applies to custom resources only, not component resources.
Model environments with stacks
Terraform workspaces can represent environments, but many teams eventually need stronger boundaries for configuration, secrets, history, and cross-environment outputs. Pulumi stacks make environment boundaries explicit and pair them with per-stack config and outputs.
const networking = new pulumi.StackReference("acme/networking/prod");
const vpcId = networking.requireOutput("vpcId");
const appSecurityGroup = new aws.ec2.SecurityGroup("app-sg", {
vpcId,
ingress: [{
protocol: "tcp",
fromPort: 443,
toPort: 443,
cidrBlocks: ["10.0.0.0/8"],
}],
egress: [{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
}],
});
Cross-stack references are cleaner than sharing an entire remote state file, but they are still dependencies. Keep stack outputs intentional and avoid turning one stack into a global variable bag.
Test infrastructure with normal language tooling
Because Pulumi programs are real programs, infrastructure testing can use familiar test runners and assertions. Teams can test component defaults, naming conventions, required tags, and policy assumptions before a deployment reaches production.
pulumi.runtime.setMocks({
newResource: (args) => ({
id: `${args.name}_id`,
state: args.inputs,
}),
call: (args) => args.inputs,
});
Testing does not replace previews. It catches a different class of problems: broken abstractions, missing required inputs, invalid defaults, and regressions in shared components.
Wire providers explicitly
Provider configuration is another place where explicit code can reduce ambiguity. In Terraform, provider inheritance and aliases are often managed across module boundaries. In Pulumi, provider resources are normal resources that can be passed through resource options, which makes multi-region or multi-account deployments easier to follow in code review.
const west = new aws.Provider("west", {
region: "us-west-2",
});
const east = new aws.Provider("east", {
region: "us-east-1",
});
const primary = new aws.sqs.Queue("primary-jobs", {}, {
provider: west,
});
const replica = new aws.sqs.Queue("replica-jobs", {}, {
provider: east,
});
The provider object does not remove provider versioning or schema-change risk, but it makes provider wiring visible in the same language and review flow as the rest of the program.
Detect and reconcile drift
Infrastructure drifts when something changes outside your IaC tool: a hotfix in the console, another controller, or a manual break-glass change. Pulumi gives you first-class tools to find and fix it instead of leaving it to chance. pulumi refresh reconciles your state with what is actually running in the cloud, and pulumi preview --diff shows what the next update would change.
For teams that want this continuously, Pulumi Cloud adds scheduled drift detection and remediation that runs on a configurable cadence and can automatically remediate drift when it is detected. That turns finding and correcting out-of-band changes into a managed workflow rather than something a person has to remember to check.
Drift detection still depends on accurate provider behavior and a healthy state backend, so it complements review and operational discipline rather than replacing them.
Import and migrate incrementally
Teams rarely get to rebuild infrastructure from scratch. Pulumi supports incremental adoption with pulumi import, generated code, and Terraform interoperability paths. That makes it possible to start with one resource, one component, or one stack instead of forcing a big-bang migration.
Pulumi also supports interoperability paths for teams that need to bring existing Terraform assets forward over time, including Terraform provider access and migration workflows. That makes migration an engineering sequence, not an all-or-nothing rewrite.
const importedRepository = new aws.ecr.Repository("app", {
name: "existing-app-repo",
}, {
import: "existing-app-repo",
});
The CLI flow can also generate declarations with pulumi import. Generated code is a starting point, not a finished architecture. Review names, options, providers, secrets, and component boundaries before treating imported resources as production-ready Pulumi code.
You also don’t have to do it alone. Pulumi provides self-serve conversion tools and expert migration services, and for teams leaving HashiCorp, credits you can apply toward Pulumi to avoid paying for two tools at once, so budget and timing don’t have to block getting started.
Manage Terraform state in Pulumi Cloud
You don’t have to rewrite anything to start getting value from Pulumi. Pulumi Cloud can serve as a Terraform state backend, letting you store and manage Terraform state alongside your Pulumi stacks. Your team can keep using the Terraform or OpenTofu CLI for day-to-day operations while gaining encrypted state storage, update history, state locking, role-based access control, audit policies, and unified resource visibility through Pulumi Insights — with agentic infrastructure coding through Neo on top.
It uses Terraform’s standard remote backend, so you point the CLI at Pulumi Cloud without changing your infrastructure code:
terraform {
backend "remote" {
hostname = "api.pulumi.com"
organization = "your-pulumi-org"
workspaces {
name = "_dev"
}
}
}
From there, Pulumi can also read outputs from that state, so a new Pulumi stack can consume Terraform-managed values such as VPC IDs and subnet IDs while you migrate one piece at a time. When you’re ready to bring resources fully across, pulumi import --from terraform reads a .tfstate file and generates matching declarations, and pulumi convert --from terraform translates the surrounding configuration into your chosen language.
Storing Terraform state in Pulumi Cloud keeps both tools pointed at one system of record, which is what makes a gradual, low-risk migration practical.
What Pulumi does not magically fix
Pulumi does not eliminate cloud API eventual consistency. A deployment can finish successfully while downstream reads, controllers, or other operators still see stale state for a short window. That is a property of the cloud control plane, not of the IaC tool.
Pulumi also does not eliminate provider bugs. If a provider has a schema issue, a bad default, or a flaky update path, Pulumi still has to ride that provider behavior. Drift will still happen when infrastructure changes outside Pulumi, too. The difference is that Pulumi gives you first-class tooling to detect and reconcile it, as covered in Detect and reconcile drift above — but the drift itself, and the discipline to act on it, do not disappear.
Pulumi does not eliminate preview-time unknowns either. Some values are not known until deployment, so the plan can still contain uncertainty. Bad project decomposition and side-effect-heavy deployment code remain risks too, which is why testing, clear stack boundaries, and disciplined component design still matter.
OpenTofu compatibility caveat
OpenTofu users face many of the same infrastructure-as-code concerns around state, providers, drift, secrets, lifecycle behavior, and migration planning. This post stays Terraform-focused because that is where most teams begin the Pulumi comparison, but the same Pulumi capabilities are relevant when evaluating OpenTofu alternatives or hybrid migration paths.
Pulumi can also work with Terraform provider ecosystems, including long-tail providers that may not have a native Pulumi package yet. Treat that as an interoperability path, not a promise that every Terraform or OpenTofu workflow maps one-for-one without design work.
Get started with Pulumi
If your team is evaluating infrastructure as code options, start with the workflow that creates the most leverage: write infrastructure in the language your team already uses, test shared components, protect critical resources, and migrate one stack at a time.
To go deeper, get started with Pulumi or read the Terraform migration guide.