Skip to content

Pulumi — Basics

IaC using real programming languages (TS/JS, Python, Go, .NET, Java, YAML). Same providers as Terraform under the hood (Pulumi can reuse TF providers via bridge).

  • Real loops, conditionals, helper functions, classes.
  • IDE features (types, completions, refactoring).
  • Test infrastructure with regular unit/integration tests (mocha, pytest, go test).
  • Easier multi-cloud abstractions.
  • Component model — package and reuse infra abstractions.

Trade-offs:

  • More opinionated, smaller community than TF.
  • Code can become as hairy as any program (TF’s restriction protects against that).
  • State is stored in Pulumi Cloud (free for individuals) or self-hosted backend (S3/GCS/Azure).
  • Project — directory with Pulumi.yaml + code.
  • Stack — independent instance (env): dev, staging, prod. Each has own state.
  • Resource — provider-managed thing (new aws.s3.Bucket(...)).
  • Provider — plugin (aws, gcp, kubernetes, github, etc.).
  • Output — async-typed value flowing between resources.
  • Component — composite resource exposing multiple primitives.
  • Config — per-stack settings (pulumi config set foo bar).
  • Backend — state storage (Pulumi Cloud default, or self-hosted).
Terminal window
pulumi new aws-typescript # scaffold
pulumi stack init dev # create stack
pulumi config set aws:region eu-west-1
pulumi up # plan + apply (interactive)
pulumi preview # plan only
pulumi destroy
pulumi stack select prod
pulumi state # state ops
pulumi import aws:s3/bucket:Bucket logs my-bucket

Outputs are promises — you can’t just use them directly as strings. Use apply() or interpolation:

const bucket = new aws.s3.Bucket("data");
const arnLog = bucket.arn.apply(a => `arn=${a}`);
const url = pulumi.interpolate`https://${bucket.bucketDomainName}/path`;
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
const cfg = new pulumi.Config();
const env = pulumi.getStack();
const bucket = new aws.s3.Bucket("data", {
versioning: { enabled: true },
serverSideEncryptionConfiguration: {
rule: { applyServerSideEncryptionByDefault: { sseAlgorithm: "AES256" } },
},
tags: { Env: env, Owner: "platform" },
});
export const bucketName = bucket.id;
import pulumi
import pulumi_aws as aws
bucket = aws.s3.Bucket("data",
versioning={"enabled": True},
tags={"env": pulumi.get_stack()},
)
pulumi.export("bucket_name", bucket.id)

Reusable abstractions:

class WebApp extends pulumi.ComponentResource {
public url: pulumi.Output<string>;
constructor(name: string, args: WebAppArgs, opts?) {
super("custom:WebApp", name, args, opts);
const lb = new aws.lb.LoadBalancer(...);
const tg = new aws.lb.TargetGroup(...);
// wire up
this.url = lb.dnsName;
this.registerOutputs({ url: this.url });
}
}
// unit (no cloud calls)
pulumi.runtime.setMocks({
newResource(args) { return { id: args.name + "_id", state: args.inputs }; },
call(args) { return args.inputs; },
});
import * as infra from "../index";
test("bucket has versioning", async () => {
const v = await new Promise(res => (infra.bucket.versioning as any).apply(res));
expect(v.enabled).toBe(true);
});

Also: integration tests via pulumi up against a temp stack.

  • Pulumi Cloud (default) — managed, has UI, free for individuals.
  • Self-hosted: pulumi login s3://bucket, gs://..., azblob://..., file://.

Pulumi:

  • Engineering team, comfortable with TS/Python/Go.
  • Heavy logic in infra (loops, complex transformations).
  • Want infra unit tests without TF’s mock limitations.
  • Polyglot stack already (Node services, Go services).

Terraform:

  • Mixed teams (ops + dev).
  • Want HCL’s enforced simplicity.
  • Battle-tested community modules already cover your needs.