Pulumi is an infrastructure-as-code tool like Terraform, with one big difference: you write infrastructure in general-purpose programming languages, such as TypeScript, Python, Go, C# or Java, instead of HCL. Loops, functions, classes, packages and tests work the way you’re used to. Under the hood, Pulumi uses many of the same providers as Terraform.
Core concepts
| Concept | Meaning |
|---|---|
| Project | A folder with Pulumi.yaml and your program |
| Stack | An instance of the project (e.g. dev, prod), with its own config and state |
| Resources | Objects your program declares (DNS records, buckets, servers) |
| State backend | Where Pulumi stores state: Pulumi Cloud, or self-managed (S3-compatible, local files) |
| Config / secrets | Per-stack settings; secrets are encrypted |
Install and create a project
curl -fsSL https://get.pulumi.com | sh
pulumi version
mkdir infra && cd infra
pulumi login s3://pulumi-state?endpoint=s3.example-provider.com&s3ForcePathStyle=true # self-managed backend
# or: pulumi login --local (state in ~/.pulumi; personal use only)
pulumi new typescript # or python, go, csharp…
Using a self-managed S3-compatible bucket (for example MinIO on your own server) keeps state under your control. See MinIO.
A small program (TypeScript)
Managing DNS records for several sites with the Cloudflare provider:
// index.ts
import * as cloudflare from "@pulumi/cloudflare";
import * as pulumi from "@pulumi/pulumi";
const config = new pulumi.Config();
const zoneId = config.require("zoneId");
const serverIp = config.require("serverIp");
const sites = ["blog", "shop", "status"];
export const records = sites.map(name =>
new cloudflare.Record(name, {
zoneId,
name,
type: "A",
content: serverIp,
proxied: true,
}).hostname
);
npm install @pulumi/cloudflare
pulumi config set zoneId <zone-id>
pulumi config set serverIp 203.0.113.10
pulumi config set --secret cloudflare:apiToken <token>
pulumi preview # like terraform plan
pulumi up # apply (asks for confirmation)
pulumi stack output
Resource property names depend on the provider version. Check the provider’s API docs.
Stacks for environments
pulumi stack init staging
pulumi config set serverIp 203.0.113.20
pulumi up
pulumi stack select prod
Each stack has its own Pulumi.<stack>.yaml config, and separate state.
Secrets
pulumi config set --secret encrypts values in the stack config file (safe to commit). With self-managed backends, choose a secrets provider: a passphrase (PULUMI_CONFIG_PASSPHRASE), or a KMS/Vault key. Secret outputs stay encrypted in state. See secrets management.
Pulumi vs Terraform/OpenTofu
| Pulumi | Terraform / OpenTofu | |
|---|---|---|
| Language | TypeScript, Python, Go, C#, Java, YAML | HCL |
| Abstractions | Classes, functions, packages | Modules |
| Testing | Unit tests in your language’s test framework | terraform test, Terratest |
| Ecosystem | Many providers (some bridged from Terraform) | Largest provider and module ecosystem |
| State | Pulumi Cloud or self-managed backends | Remote backends (S3, etc.) |
| Learning curve | Easy if you know the language; programming discipline needed | Simple declarative language |
Choose Pulumi if your team prefers real programming languages, and wants to share libraries and tests. Choose Terraform/OpenTofu for the largest ecosystem and a simpler declarative style. See OpenTofu and Terraform modules.
Tips
- Keep programs deterministic: avoid random values or API calls that change on every run (Pulumi has
randomresources for stable randomness). - Review
pulumi previewoutput in CI beforepulumi up. See CI/CD pipeline explained. - Use component resources to package reusable patterns (similar to Terraform modules).
Quick reference
| Task | Command |
|---|---|
| New project | pulumi new typescript |
| Self-managed state | pulumi login s3://bucket?endpoint=… |
| Config / secret | pulumi config set [--secret] key value |
| Preview / apply | pulumi preview / pulumi up |
| Environments | pulumi stack init/select |
| Tear down | pulumi destroy |
Related: Terraform state and Ansible roles.