

The IaC tool market has shifted significantly over the past two years. HashiCorp moved Terraform to a Business Source License, restricting use in competing SaaS products. OpenTofu forked it under Apache 2.0 as the community-maintained alternative. And in January 2026, Pulumi announced native Terraform and HCL support, meaning teams can now run existing Terraform codebases inside Pulumi's orchestration layer without converting anything. The tools that once felt like distinct choices are converging.
This is a good moment to understand what Pulumi actually is, what it does differently from Terraform, and when it makes sense to use it: either as your primary IaC tool or alongside Terraform and OpenTofu in an enterprise environment.
At a glance Pulumi is an open-source Infrastructure as Code (IaC) platform that lets you define cloud resources using general-purpose programming languages. Current version: v3.230.0 (April 8, 2026). Supported languages: TypeScript, Python, Go, .NET, Java, and YAML. Pulumi manages state, supports 75+ cloud providers, and as of 2026 natively supports running existing Terraform/HCL configurations. It's maintained by Pulumi Corporation, founded in 2018.
What is Pulumi?
Pulumi is an IaC tool that takes a different approach from Terraform and its descendants. Where Terraform uses HCL (a purpose-built declarative language), Pulumi lets you write infrastructure code in the programming language your team already knows: TypeScript, Python, Go, .NET, Java, or YAML.
The resource model is similar. You declare what infrastructure you want, Pulumi plans the changes, and applies them against your cloud provider. State is tracked between runs to reconcile the actual cloud environment with your desired configuration. The underlying mechanics (state files, providers, dependency graphs) are close enough to Terraform's that engineers familiar with one can understand the other quickly.
The difference is ergonomics and expressiveness. With Pulumi, your infrastructure code is a real program. You get loops, conditionals, functions, classes, type-checking, and the full ecosystem of your language's package manager. Dynamic resource creation that requires Terraform workarounds (like count hacks or for_each over complex maps) tends to be straightforward in Pulumi.
Pulumi also bundles a broader platform than just the CLI. Pulumi Cloud provides hosted state management, team access controls, deployment automation, secrets management (Pulumi ESC), policy enforcement (CrossGuard), and infrastructure insights, making it a direct competitor to Terraform Cloud as well as a standalone IaC tool.
One capability that has no Terraform equivalent is the Automation API: a programmatic interface that lets you embed Pulumi deployments inside application code. Rather than shelling out to the CLI, you import Pulumi as a library and drive stack operations from TypeScript or Python. Platforms that need to create ephemeral environments on demand (say, a self-service portal that provisions a dev stack per pull request) use the Automation API to wire infrastructure lifecycle to application events.
Related reading: Infrastructure as Code 101: concepts, tools, and getting started. Covers the foundational concepts common to Pulumi, Terraform, and other IaC frameworks before you go deeper on any one tool.
What changed in Pulumi in 2026
The biggest development in Pulumi's history happened quietly in January 2026. Pulumi announced that its CLI now natively interprets HCL code through a Terraform bridge, and that Pulumi Cloud can serve as a state backend for existing Terraform and OpenTofu projects.
This matters for enterprises with mixed infrastructure codebases. A platform team can now run Terraform-authored modules alongside Pulumi components in the same orchestration layer, without migrating anything. HCL becomes just another language Pulumi understands, sitting alongside TypeScript and Python. The stated motivation from Pulumi's CEO: "We are not dogmatic about languages. As soon as we see enough market demand for a given language, we will add it." HCL, as it turns out, is widely spoken.
The practical implication for teams evaluating Pulumi: the historical argument against adopting it (we already have Terraform modules everywhere) is weaker than it used to be. You can adopt Pulumi incrementally, running new services in TypeScript or Python while existing HCL infrastructure stays as-is.
Pulumi also shipped several new platform capabilities that weren't in earlier versions of this post:
Pulumi ESC reached general availability. It's a centralized secrets and configuration management product that pulls from any secrets store (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, 1Password, and others) and makes those values available to Pulumi stacks, CI/CD pipelines, and applications through a single access layer. Every access is logged. Environments in ESC are versioned, with immutable revision history.
Pulumi Deployments is now the recommended way to run Pulumi in CI/CD. It handles cloud-hosted runs, drift detection against live infrastructure, state management, and a CI/CD integration assistant that configures common pipelines automatically.
Pulumi Insights adds compliance and search across all cloud infrastructure managed by Pulumi, including cross-account and cross-region visibility.
The current stable release is v3.230.0, which added AI agent detection in CLI metadata, ESC environment resolution for policy packs, and cancel handler support for Python and Node.js providers.
Related reading: OpenTofu vs. Terraform: a practical guide for enterprise infrastructure teams. The licensing fork context helps explain why Pulumi's HCL support is landing at this particular moment.
How Pulumi works
Projects, stacks, and state
A Pulumi project is a directory containing your infrastructure code and a Pulumi.yaml file that declares the project name, runtime, and description. When you run pulumi up, Pulumi executes your program, builds a resource graph from the declared cloud resources, compares it to the last known state, and applies the diff.
The state equivalent in Pulumi is a stack. Each stack has its own state file stored in a backend (Pulumi Cloud by default, or self-managed using S3, Azure Blob Storage, or GCS). A stack corresponds to a deployment target: dev, staging, production, or a per-engineer ephemeral environment.
State is what Pulumi uses to track what's currently deployed. Unlike Terraform, where the state file is a plain JSON document you manage yourself, Pulumi Cloud wraps state with access controls, history, locking, and a web UI for viewing resource graphs. For teams that prefer self-managed state, the Terraform remote backend patterns translate directly to Pulumi's S3 backend configuration.
Providers and the resource model
Pulumi providers are plugins that translate your high-level resource declarations into API calls against cloud providers. The provider ecosystem covers AWS, Azure, GCP, Kubernetes, Cloudflare, Datadog, and 70+ others. You can browse and install them via the Pulumi Registry. Providers are versioned and pinned in your project configuration, similar to Terraform providers.
Resources in Pulumi are objects. When you declare an S3 bucket in TypeScript, you're instantiating a Bucket class from the @pulumi/aws package. The constructor accepts a resource name (unique within the stack) and a configuration object with the bucket's properties. Pulumi tracks this resource's physical ID and properties in state.
Component resources are Pulumi's equivalent of Terraform modules: reusable abstractions that group multiple resources into a named unit. A WebApplication component might internally create an ECS task, a load balancer, security groups, and DNS records, and expose only the URL as an output. Consumers of the component don't need to know what's inside.
Here's a minimal component resource in TypeScript — an S3 static website that encapsulates the bucket and website configuration, exposing only the URL:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
class S3Website extends pulumi.ComponentResource {
public readonly websiteUrl: pulumi.Output<string>;
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("myorg:storage:S3Website", name, {}, opts);
const bucket = new aws.s3.BucketV2(`${name}-bucket`, {
tags: { ManagedBy: "pulumi" },
}, { parent: this });
new aws.s3.BucketWebsiteConfigurationV2(`${name}-website`, {
bucket: bucket.id,
indexDocument: { suffix: "index.html" },
errorDocument: { key: "error.html" },
}, { parent: this });
this.websiteUrl = bucket.websiteEndpoint;
this.registerOutputs({ websiteUrl: this.websiteUrl });
}
}
// Consumers see only the output URL, not the bucket internals
const site = new S3Website("marketing-site");
export const url = site.websiteUrl;
The { parent: this } option on each child resource is important: it tells Pulumi that those resources belong to the component, which keeps the resource graph readable and makes destroying the whole component work as expected.
Getting started with Pulumi
Install the Pulumi CLI (macOS with Homebrew):
brew install pulumi
pulumi version
# v3.230.0
On Linux:
curl -fsSL https://get.pulumi.com | sh
Create a new TypeScript project:
mkdir my-infra && cd my-infra
pulumi new aws-typescript
This scaffolds a project with a Pulumi.yaml, a package.json, and an index.ts with a simple S3 bucket. Running pulumi up plans and applies the changes:
pulumi up
# Previewing update...
# + aws:s3:Bucket my-bucket create
# Do you want to perform this update? yes
# Updating...
# Resources: 1 created
Each subsequent pulumi up computes a diff between your current program and the last known state. Resources not referenced by your program will be destroyed; new resources will be created; modified resources will be updated in place where possible.
To see the diff without applying it, use pulumi preview. It runs your program, computes the resource graph, and prints every planned create/update/delete without touching your cloud account. Running pulumi preview before every apply is a good habit in production environments, and it's what the pulumi/actions GitHub Action runs on pull requests by default.
Stacks for different environments are created with pulumi stack init:
pulumi stack init staging
pulumi stack select staging
pulumi up
Configuration values per stack:
pulumi config set aws:region us-west-2
pulumi config set --secret dbPassword hunter2
The --secret flag encrypts the value in state using a per-stack encryption key.
Related reading: Top Terraform tools to know in 2026. Several tools in the Terraform ecosystem (linting, testing, policy) have Pulumi equivalents or work across both.
Pulumi example: deploying an EKS cluster
Deploying a managed Kubernetes cluster is a good representation of Pulumi's programming model because it involves dependent resources (VPC, subnets, node groups) that need to be created in order and wired together.
import * as eks from "@pulumi/eks";
import * as aws from "@pulumi/aws";
// Create an EKS cluster with managed node groups
const cluster = new eks.Cluster("prod-cluster", {
instanceType: "t3.medium",
desiredCapacity: 3,
minSize: 1,
maxSize: 5,
tags: { Environment: "prod", ManagedBy: "pulumi" },
deployDashboard: false,
});
// Export the cluster kubeconfig and endpoint
export const kubeconfig = cluster.kubeconfig;
export const clusterName = cluster.eksCluster.name;
export const clusterEndpoint = cluster.eksCluster.endpoint;
The @pulumi/eks package is a component resource that creates and connects the underlying VPC, subnets, IAM roles, the EKS control plane, and node groups internally. You declare what you want at the logical level and Pulumi resolves the dependency graph and creation order.
After pulumi up, the exported values are available to other stacks through stack references:
import * as pulumi from "@pulumi/pulumi";
// Reference the cluster stack from another stack
const clusterStack = new pulumi.StackReference("org/prod-cluster/prod");
const kubeconfig = clusterStack.getOutput("kubeconfig");
This is the Pulumi alternative to Terraform's remote_state data sources. Stack references are type-safe, meaning your IDE can tell you what outputs are available before you run anything. See our guide to Terraform state files for a comparison of how Terraform and Pulumi each approach cross-module output sharing.
The Kubernetes namespaces and Terraform environments guide covers a complementary approach if you're managing the cluster's internal resources with a different tool.
Pulumi in CI/CD
Running pulumi up locally is fine for getting started, but production deployments need a reproducible pipeline where the plan is visible to reviewers before anything applies.
The official pulumi/actions GitHub Action covers both halves: run pulumi preview on pull requests (with the diff posted as a PR comment) and pulumi up on merge to main. Here's a working workflow:
name: Pulumi Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
preview:
name: Preview
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- uses: pulumi/actions@v5
with:
command: preview
stack-name: org/my-infra/prod
comment-on-pr: true
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
deploy:
name: Deploy
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- uses: pulumi/actions@v5
with:
command: up
stack-name: org/my-infra/prod
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
The comment-on-pr: true flag is the reason teams reach for pulumi/actions over a plain shell script. Every PR gets a formatted preview diff inline, so reviewers see what will change in the cloud before approving the merge. No separate Atlantis server, no custom comment formatting.
PULUMI_ACCESS_TOKEN authenticates against Pulumi Cloud for state management. If you're using a self-managed S3 backend, replace it with the appropriate PULUMI_BACKEND_URL environment variable instead.
Related reading: Implement Atlantis-style Terraform and Terragrunt workflows in env zero. env zero provides the same PR-preview-then-merge workflow for Terraform, Terragrunt, and Pulumi from a single platform.
Pulumi vs. Terraform
The choice between Pulumi and Terraform used to be cleaner. Pulumi for teams that want to write real code; Terraform for teams that prefer a declarative DSL and a mature ecosystem. In 2026, that distinction is blurring.
Pulumi now runs HCL natively, so existing Terraform modules don't need to be rewritten. Terraform (via OpenTofu) remains fully open-source. Both tools support the same cloud providers through similar plugin architectures. The decision points that remain:
Language. Terraform HCL is purpose-built for infrastructure: declarative, constrained, and readable by engineers who don't write much code. Pulumi gives you the full power of TypeScript, Python, or Go, which is genuinely useful for complex conditional logic and dynamic resource generation. It also means you bring in all the footguns of general-purpose programming: accidentally shared mutable state, unintended loops, type errors caught only at runtime (in Python). TypeScript reduces this significantly through compile-time checking.
Ecosystem maturity. Terraform's provider and module ecosystem is larger and older. Community Terraform modules exist for nearly every AWS service pattern. Pulumi's provider coverage is equivalent (they often generate from the same Terraform provider SDKs), but community-authored reusable components are less common.
Licensing. As of 2026, Pulumi remains Apache 2.0. HashiCorp Terraform is Business Source License (BSL), which restricts building competing SaaS products on top of it. OpenTofu is the community fork under Apache 2.0 with equivalent functionality. This matters to managed service vendors; it matters less to enterprises using the tool internally.
The convergence. With Pulumi's HCL support and Pulumi Cloud accepting Terraform state, the two tools can coexist in the same platform. Many enterprises running the four stages of Terraform automation can add Pulumi for new services without disrupting existing workflows.
For teams that do want to migrate existing HCL, Pulumi ships pulumi convert:
# Convert an existing Terraform directory to TypeScript
pulumi convert --from terraform --language typescript --out ./pulumi-infra
The command reads your .tf files and produces an equivalent Pulumi TypeScript (or Python, Go, .NET) program. It handles providers, variables, outputs, and resource dependencies. The output isn't always production-ready — complex for_each expressions and dynamic blocks often need manual cleanup — but it gives you a working starting point rather than a blank file.
For a deeper comparison, see our Pulumi vs. Terraform in-depth guide.
Related reading: Terraform workspaces guide: commands, examples, and best practices. Understanding Terraform workspaces helps clarify how Pulumi's stack model solves the same multi-environment problem differently.
Common Pulumi pitfalls
Language choice paralysis is the most common obstacle when teams evaluate Pulumi. Every language has a supported SDK, but not every language is an equally good choice. TypeScript is the default for a reason: it has the best documentation coverage, the largest community of Pulumi examples, strong type checking (so errors surface before pulumi up), and the most complete provider types. Python is a reasonable second choice if your team is Python-first and you're disciplined about type annotations. Go is excellent for component resource libraries. .NET and Java exist and work, but the community support is thinner.
State backend decisions made early are hard to change later. Pulumi Cloud is the path of least resistance and adds access controls, history, and locking without configuration. Self-managed S3 backends work well but require you to manage bucket versioning, locking via DynamoDB, and encryption yourself. Make the decision explicitly at project setup; migrating state between backends later is possible but tedious.
Provider version drift causes the same pain in Pulumi as in Terraform. Pin provider versions in your package.json for Node.js projects, or in requirements files for Python. An npm update that pulls in a breaking provider change will surface as a diff on the next pulumi up, sometimes destroying and recreating resources. Running pulumi refresh before pulumi up surfaces the current gap between state and live infrastructure before you make any changes.
Stack output debugging is different in Pulumi than in Terraform. When a dependent resource references an output that isn't resolved yet during preview, Pulumi shows it as <output>. This confuses engineers expecting to see actual values in plan output. The behavior is by design: outputs are promises that resolve at apply time, not plan time. Leaning into Pulumi's pulumi.Output.apply() for derived values reduces confusion when reading preview output.
Testing Pulumi code
Infrastructure bugs found in production cost more to fix than those caught before pulumi up runs. Pulumi has a built-in mocking framework that lets you write unit tests against your stack without provisioning any real cloud resources — no AWS account, no billable activity.
The pattern: call pulumi.runtime.setMocks() before importing your infrastructure module to intercept all resource creation and return predictable synthetic IDs. Then assert against the resource properties in your test suite of choice (Jest works well for TypeScript projects):
import * as pulumi from "@pulumi/pulumi";
// Set up mocks BEFORE importing your infrastructure module
pulumi.runtime.setMocks(
{
newResource: (args: pulumi.runtime.MockResourceArgs) => ({
id: `${args.name}-id`,
state: { ...args.inputs },
}),
call: (args: pulumi.runtime.MockCallArgs) => args.inputs,
},
"project", "stack", false
);
// Import after mocks are set — the module will use mocked providers
import { S3Website } from "./website";
describe("S3Website component", () => {
it("exports a website URL output", async () => {
const site = new S3Website("test");
const url = await new Promise((resolve) =>
site.websiteUrl.apply(resolve)
);
expect(url).toBeTruthy();
});
});
The mocking setup intercepts every new aws.s3.BucketV2(...) call and returns a fake resource with a deterministic ID. Your component logic still executes fully, so you can verify that outputs are wired together correctly, required tags are applied, and naming conventions hold — without touching a cloud provider.
For integration tests that need to verify real provisioning behavior, Pulumi recommends using the Automation API to deploy a real stack into a dedicated test account, run assertions against live outputs, then destroy it. It's slower and has a cost, but it catches provider-level issues that mocks can't surface. The Pulumi testing documentation covers both approaches in full.
Pulumi with env zero
Pulumi solves the authoring problem. Writing infrastructure in TypeScript or Python, with real programming constructs, is genuinely more productive than HCL for complex systems. What it doesn't solve is the governance layer: who can deploy to production, which policies must pass before a stack applies, what happened to a stack that was last modified six months ago, and whether the live infrastructure still matches the last apply.
env zero supports Pulumi natively alongside Terraform, OpenTofu, Terragrunt, Kubernetes, and Helm. For enterprises running a mix of tools (and most are), this means governance applies uniformly regardless of which IaC framework a team uses. A policy that requires resource tagging or restricts instance types enforces the same way whether the stack is a Pulumi TypeScript program or a Terraform module.
The governance gap shows up most clearly at scale. A team of five can track Pulumi deployments by watching Pulumi Cloud. A team of fifty deploying to a hundred stacks across multiple AWS accounts can't. env zero adds the layer that Pulumi Cloud doesn't provide out of the box: approval workflows for production deployments, OPA policy enforcement before any stack applies, automated drift detection that surfaces when live infrastructure diverges from stack state, and a full audit trail of every deployment across every stack.
The self-service with guardrails solution is particularly relevant for Pulumi shops: application teams get the flexibility to write infrastructure in their language of choice, while the platform team sets the guardrails through env zero rather than trying to enforce conventions through code review alone. See the cloud governance and risk management solution page for how policy enforcement and drift detection work across a multi-framework environment.
Related reading: Terraform modules guide: best practices for reusable IaC. Pulumi's component resources solve the same problem as Terraform modules, and understanding the parallels helps when evaluating which abstraction model fits your team.
Try it with env zero
Pulumi gives your team the flexibility to write infrastructure in the language they already know. env zero adds the governance layer that makes that flexibility safe at scale: policy enforcement, drift detection, approval workflows, and a full audit trail across every Pulumi stack and every other IaC framework your teams use.
Start a free trial or book a demo.
References
- Pulumi adds native support for Terraform and HCL. InfoQ, January 2026
- HashiCorp adopts Business Source License. HashiCorp blog, August 2023
- The OpenTofu fork is now available. OpenTofu blog, September 2023
- Pulumi releases: v3.230.0. GitHub, April 8, 2026
- Pulumi ESC: environments, secrets, and configuration. Pulumi documentation
- Pulumi Deployments. Pulumi documentation
- Pulumi CrossGuard policy as code. Pulumi documentation
- Pulumi state and backends. Pulumi documentation
- Pulumi Registry. Pulumi provider and component registry
- Pulumi Automation API. Pulumi documentation
- Pulumi testing. Pulumi documentation
- Migrating from Terraform: pulumi convert. Pulumi documentation
- pulumi/actions GitHub Action. GitHub, Pulumi
- pulumi refresh CLI reference. Pulumi documentation
- env zero Pulumi integration. env zero documentation
FAQ
What is Pulumi? Pulumi is an open-source Infrastructure as Code platform that lets you define cloud resources using general-purpose programming languages including TypeScript, Python, Go, .NET, Java, and YAML. It manages state, supports 75+ cloud providers, and as of 2026 also runs existing Terraform and HCL configurations natively. The current version is v3.230.0 (April 2026).
Does Pulumi support Terraform? Yes, as of January 2026. Pulumi added native HCL support through a Terraform bridge, allowing the Pulumi CLI to interpret and run existing HCL configurations. Pulumi Cloud also functions as a state backend for Terraform and OpenTofu projects, meaning teams can manage both Pulumi and Terraform state in one place without migrating their HCL code.
What languages does Pulumi support?
TypeScript, Python, Go, .NET (C#/F#), Java, and YAML. TypeScript is recommended for most teams: it has the strongest community support, the best documentation coverage, and compile-time type checking that catches errors before pulumi up runs.
What is Pulumi ESC? Pulumi ESC (Environments, Secrets, Configuration) is a centralized secrets management product that integrates with AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, GCP Secret Manager, 1Password, and other stores. It makes secrets available to Pulumi stacks, CI/CD pipelines, and applications through a single versioned access layer. Every access is logged. ESC reached general availability in 2025.
How is Pulumi different from Terraform? The primary difference is the language model. Terraform uses HCL, a purpose-built declarative DSL. Pulumi uses general-purpose programming languages, giving you full programming constructs (loops, conditionals, classes, type checking) for infrastructure definitions. In 2026, Pulumi also runs HCL natively, making the tools increasingly interoperable. Terraform (via OpenTofu) remains Apache 2.0; Pulumi is Apache 2.0; the original HashiCorp Terraform is BSL.
Can Pulumi manage Kubernetes resources? Yes. Pulumi has a Kubernetes provider that manages resources in any Kubernetes cluster. You can write Kubernetes manifests as TypeScript or Python objects rather than YAML, which enables type-safe configuration and programmatic generation of complex manifests. Pulumi also integrates with Helm through its Helm provider.
Does Pulumi work with env zero? Yes. env zero supports Pulumi natively alongside Terraform, OpenTofu, Terragrunt, Kubernetes, and Helm. Governance policies, approval workflows, drift detection, and audit logging apply to Pulumi stacks the same way they apply to Terraform runs. For enterprises managing both Pulumi and Terraform infrastructure, env zero provides a single governance layer across all frameworks.
Can I migrate existing Terraform code to Pulumi?
Yes. The pulumi convert --from terraform --language typescript command reads your .tf files and produces an equivalent Pulumi TypeScript (or Python, Go, .NET) program. It handles providers, variables, outputs, and most resource dependencies. Complex for_each expressions and dynamic blocks often need manual cleanup after conversion, but it produces a working starting point rather than a blank file. Pulumi Cloud can also serve as a remote state backend for existing Terraform and OpenTofu projects, letting you consolidate state management without rewriting any HCL.
What is Pulumi Deployments?
Pulumi Deployments is Pulumi Cloud's managed CI/CD service for running pulumi up in the cloud rather than on local machines or self-hosted runners. It includes drift detection, a CI/CD integration assistant for common pipeline configurations, and a deployment history with resource-level output capture. It's Pulumi's answer to the governance and auditability gap that Pulumi CLI alone doesn't close.

.webp)

![Using Open Policy Agent (OPA) with Terraform: Tutorial and Examples [2026]](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/69d6a3bde2ffe415812d9782_post_th.png)