Pulumi: Infrastructure as Real Code in Python, Go, and TypeScript
Pulumi lets you provision cloud infra in a language you already know — with loops, functions, and tests. Here's how it differs from HCL and where it shines.
- #iac
- #pulumi
- #python
- #golang
- #typescript
- #cloud
Most infrastructure-as-code tools invent their own configuration language and then spend years bolting programming features back onto it — conditionals, loops, string interpolation, the works. Pulumi takes the opposite bet: use a real general-purpose language from the start, and let infrastructure be just another thing you write in Python, Go, or TypeScript.
I’ve shipped infra with both HCL-style tools and Pulumi, and the difference is most obvious the moment your config needs logic. Here’s how Pulumi works and when it’s the right call.
The core idea: a program that describes desired state
A Pulumi program isn’t a script that imperatively creates resources. You write code that declares resources, Pulumi builds a dependency graph from them, diffs it against the last known state, and applies the minimum set of changes. Same declarative model as any good IaC tool — you just author it in a language with functions, packages, and a debugger.
A minimal Python program:
# __main__.py
import pulumi
import pulumi_aws as aws
bucket = aws.s3.BucketV2("assets")
aws.s3.BucketVersioningV2("assets-versioning",
bucket=bucket.id,
versioning_configuration={"status": "Enabled"},
)
pulumi.export("bucket_name", bucket.id)
pulumi up shows a diff and applies it. pulumi.export surfaces outputs. State lives in Pulumi Cloud by default, or in your own S3/GCS/Azure backend if you’d rather self-manage it.
Where real code earns its keep
The payoff shows up when configuration gets repetitive or conditional. Compare “create a bucket per environment” in HCL’s for_each gymnastics versus a plain loop:
environments = ["dev", "staging", "prod"]
buckets = {
env: aws.s3.BucketV2(f"data-{env}",
tags={"Environment": env})
for env in environments
}
Need a CDN only in prod? It’s an if, not a count-based ternary hack:
if pulumi.get_stack() == "prod":
aws.cloudfront.Distribution("cdn", ...)
Want to factor out a reusable pattern — a tagged, encrypted, versioned bucket your whole org should use? It’s a function or a class, tested with your normal test framework. This is the actual selling point: your infrastructure abstractions are the same abstractions your language already gives you.
Outputs and the async gotcha
The one concept that trips up newcomers: resource properties are Output[T], not plain values, because they may not exist until the resource is created. You can’t just concatenate them as strings.
# WRONG — bucket.id isn't a string yet
url = "https://" + bucket.id # type error
# RIGHT — apply runs once the value is known
url = bucket.id.apply(lambda name: f"https://{name}.example.com")
# Or interpolate
url = pulumi.Output.concat("https://", bucket.id, ".example.com")
Once apply and Output.concat click, the async model fades into the background. But it’s the number-one source of early confusion, so name it up front.
Stacks: environments without copy-paste
A stack is an isolated instance of your program with its own config and state — typically one per environment. You parametrize with config instead of duplicating code:
pulumi stack init prod
pulumi config set aws:region us-east-1
pulumi config set instanceCount 5
config = pulumi.Config()
count = config.require_int("instanceCount")
pulumi up --stack dev # 1 instance
pulumi up --stack prod # 5 instances
One program, many stacks, zero duplicated infra code. Secrets in config are encrypted automatically with pulumi config set --secret.
Go and TypeScript, same model
The model is language-agnostic. The same bucket in Go:
bucket, err := s3.NewBucketV2(ctx, "assets", nil)
if err != nil {
return err
}
ctx.Export("bucketName", bucket.ID())
Pick the language your team already maintains. Python for data/ML-adjacent teams, TypeScript for full-stack shops, Go for platform teams that want static typing and a single compiled binary. The infra concepts don’t change.
Testing infrastructure like software
Because it’s real code, you can unit-test resource definitions with mocks — no cloud calls, runs in milliseconds in CI:
import pulumi
class Mocks(pulumi.runtime.Mocks):
def new_resource(self, args):
return [args.name + "_id", args.inputs]
def call(self, args):
return {}
pulumi.runtime.set_mocks(Mocks())
# then assert your resources have required tags, encryption, etc.
This is genuinely hard to do well in HCL and almost free in Pulumi. For teams that care about policy enforcement, you can assert “every bucket has encryption enabled” as a unit test that fails the PR.
When NOT to reach for Pulumi
Honesty matters here:
- A team that doesn’t write code will struggle more with Pulumi than with declarative HCL. The general-purpose language is power and rope.
- Heavy existing HCL investment is a real switching cost. Pulumi can adopt existing state and even convert HCL, but a working Terraform estate isn’t a reason to migrate by itself.
- You can write spaghetti. A real language means you can write genuinely bad, untestable infra code. Discipline that declarative tools enforce by limitation, you now have to enforce by convention.
Where AI fits
Pulumi plays unusually well with AI because the model is fluent in Python, Go, and TypeScript already — far more training data than any niche config DSL.
- Scaffolding a stack. “Generate a Pulumi Python program for an ECS Fargate service behind an ALB, parametrized by stack config” gets you a strong first draft.
- Fixing the
Outputconfusion. Paste the type error and the model usually spots the missing.applyimmediately. - Converting from HCL. Ask it to translate a Terraform module to Pulumi Python — it handles the structural mapping well, though you verify the resource arguments.
As always, the model doesn’t know your account layout or naming conventions, so treat output as a draft you run pulumi preview against before trusting. We keep a set of IaC prompts for provisioning and migration work like this.
The bottom line
Pulumi’s bet is that infrastructure is software, so you should write it in software. If your team is comfortable in a programming language and your config has outgrown what a DSL can express cleanly, that bet pays off — loops, functions, real tests, and one mental model across infra and application code. If your team would rather have guardrails than power, a declarative tool may serve you better. Know which team you are, and the choice makes itself.
AI-generated Pulumi programs are assistive, not authoritative. Always run pulumi preview and review the diff before applying to any environment.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.