Docs / Cloud & DevOps / Pulumi TypeScript IaC

Pulumi TypeScript IaC

By Admin · Mar 15, 2026 · Updated Apr 24, 2026 · 214 views · 4 min read

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.

Was this article helpful?