Pulumi lets you define infrastructure using real programming languages — TypeScript, Python, Go, and C#. Unlike HCL-based tools, you get full access to loops, conditionals, functions, classes, and package managers. This guide covers using Pulumi with TypeScript for infrastructure provisioning.
Installation
# Install Pulumi
curl -fsSL https://get.pulumi.com | sh
# Create a new project
mkdir my-infra && cd my-infra
pulumi new typescript
# Or with a specific cloud
pulumi new aws-typescript
Basic Infrastructure
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
// VPC
const vpc = new aws.ec2.Vpc("main-vpc", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
tags: { Name: `${environment}-vpc`, Environment: environment },
});
// Subnets with loops
const azs = ["us-east-1a", "us-east-1b", "us-east-1c"];
const publicSubnets = azs.map((az, i) =>
new aws.ec2.Subnet(`public-${i}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i}.0/24`,
availabilityZone: az,
mapPublicIpOnLaunch: true,
tags: { Name: `${environment}-public-${az}` },
})
);
// Security Group
const webSg = new aws.ec2.SecurityGroup("web-sg", {
vpcId: vpc.id,
ingress: [
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
{ protocol: "tcp", fromPort: 443, toPort: 443, cidrBlocks: ["0.0.0.0/0"] },
],
egress: [
{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
],
});
// EC2 Instance
const server = new aws.ec2.Instance("web-server", {
instanceType: environment === "production" ? "t3.medium" : "t3.micro",
ami: "ami-0c55b159cbfafe1f0",
subnetId: publicSubnets[0].id,
vpcSecurityGroupIds: [webSg.id],
tags: { Name: `${environment}-web`, Environment: environment },
});
// Exports
export const vpcId = vpc.id;
export const publicIp = server.publicIp;
export const publicSubnetIds = publicSubnets.map(s => s.id);
Component Resources (Reusable)
// components/web-cluster.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface WebClusterArgs {
vpcId: pulumi.Input<string>;
subnetIds: pulumi.Input<string>[];
instanceType: string;
minSize: number;
maxSize: number;
environment: string;
}
export class WebCluster extends pulumi.ComponentResource {
public readonly securityGroupId: pulumi.Output<string>;
public readonly asgName: pulumi.Output<string>;
constructor(name: string, args: WebClusterArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:WebCluster", name, {}, opts);
const sg = new aws.ec2.SecurityGroup(`${name}-sg`, {
vpcId: args.vpcId,
ingress: [
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
],
}, { parent: this });
this.securityGroupId = sg.id;
const lt = new aws.ec2.LaunchTemplate(`${name}-lt`, {
instanceType: args.instanceType,
vpcSecurityGroupIds: [sg.id],
}, { parent: this });
const asg = new aws.autoscaling.Group(`${name}-asg`, {
minSize: args.minSize,
maxSize: args.maxSize,
vpcZoneIdentifiers: args.subnetIds,
launchTemplate: { id: lt.id, version: "$Latest" },
}, { parent: this });
this.asgName = asg.name;
this.registerOutputs({ securityGroupId: this.securityGroupId, asgName: this.asgName });
}
}
// Usage
const cluster = new WebCluster("web", {
vpcId: vpc.id,
subnetIds: publicSubnets.map(s => s.id),
instanceType: "t3.medium",
minSize: 2,
maxSize: 6,
environment: "production",
});
Stack Management
# Create environments as stacks
pulumi stack init dev
pulumi stack init staging
pulumi stack init production
# Set configuration per stack
pulumi config set environment dev --stack dev
pulumi config set environment production --stack production
pulumi config set aws:region us-east-1
# Deploy
pulumi up --stack dev # Preview and deploy dev
pulumi up --stack production # Deploy production
# Destroy
pulumi destroy --stack dev
Testing
// __tests__/infra.test.ts
import * as pulumi from "@pulumi/pulumi";
import "jest";
pulumi.runtime.setMocks({
newResource: (args) => ({ id: args.name + "_id", state: args.inputs }),
call: (args) => args.inputs,
});
describe("Infrastructure", () => {
let infra: typeof import("../index");
beforeAll(async () => {
infra = await import("../index");
});
test("VPC has correct CIDR", async () => {
const cidr = await new Promise((resolve) =>
pulumi.all([infra.vpcId]).apply(() => resolve("10.0.0.0/16"))
);
expect(cidr).toBe("10.0.0.0/16");
});
});
Pulumi vs Terraform
- Language: Pulumi uses real languages (TS, Python, Go); Terraform uses HCL
- State: Pulumi cloud or self-managed; Terraform uses backends (S3, etc.)
- Loops/Conditions: Native in Pulumi; limited in Terraform (count, for_each)
- Testing: Standard test frameworks in Pulumi; terraform test in Terraform
- Community: Terraform has larger community and more providers
Summary
Pulumi brings software engineering practices to infrastructure management. TypeScript provides type safety, IDE autocomplete, and the full power of a real programming language for defining infrastructure. Component resources enable reusable abstractions, and standard testing frameworks validate your infrastructure code. For teams already using TypeScript, Pulumi eliminates the need to learn a separate DSL while providing equivalent or better infrastructure management capabilities.