IaC with Pulumi and TypeScript: Infrastructure as Code for Developers

DevOps

Why Pulumi + TypeScript Beats Terraform for Developers in 2026

Terraform uses HCL to define infrastructure — a domain-specific language. Developers must learn new syntax, handle string interpolation, and suffer limited IDE support. Pulumi lets you define infrastructure in real programming languages — TypeScript means type checking, IDE autocompletion, npm ecosystem, conditional logic, and loops. Everything you already know.

Dimension Terraform (HCL) Pulumi (TypeScript) CDK (TypeScript)
Language HCL (DSL) TypeScript TypeScript
Type Safety None Full Full
IDE Support Limited Full Full
Conditional Logic count/for_each if/else/for if/else/for
Module Reuse Terraform Module npm package Construct
State Management State File Pulumi Cloud/Self-hosted CDK Cloud
Secret Management .tfvars Config secrets Context
Testing terraform test Mocha/Jest Jest
Dependency Mgmt None package.json package.json
Error Messages Runtime Compile-time + Runtime Compile-time + Runtime
Learning Curve Low (HCL simple) Low (familiar language) Medium (Construct abstraction)
Vendor Lock-in None Low AWS-bound

Pulumi Architecture: Engine, Language Host, Resource Provider

┌─────────────────────────────────────────────────────┐
│              Pulumi CLI (pulumi)                      │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐     │
│  │ Engine     │  │ Language   │  │ Resource   │     │
│  │ (Orchest.) │──│ Host       │──│ Provider   │     │
│  │            │  │ (Node.js)  │  │ (gRPC)     │     │
│  └────────────┘  └────────────┘  └────────────┘     │
└──────┬───────────────────────────────────┬──────────┘
       │                                   │
┌──────▼──────┐  ┌────────────┐  ┌────────▼────────┐
│ Pulumi Cloud │  │ AWS/GCP/   │  │ State Backend   │
│ (optional)   │  │ Azure API  │  │ (local/S3/Cloud)│
└─────────────┘  └────────────┘  └─────────────────┘

Complete Project Setup with TypeScript

curl -fsSL https://get.pulumi.com | sh
pulumi login
mkdir my-infra && cd my-infra
pulumi new aws-typescript

package.json

{
  "name": "my-infra",
  "main": "index.ts",
  "scripts": {
    "preview": "pulumi preview",
    "up": "pulumi up --yes",
    "destroy": "pulumi destroy --yes",
    "test": "mocha -r ts-node/register test/**/*.spec.ts"
  },
  "dependencies": {
    "@pulumi/pulumi": "^3.130.0",
    "@pulumi/aws": "^6.60.0",
    "@pulumi/awsx": "^2.10.0"
  },
  "devDependencies": {
    "@types/mocha": "^10.0.0",
    "mocha": "^10.0.0",
    "ts-node": "^10.9.0",
    "typescript": "^5.5.0"
  }
}

Deploy AWS Resources

VPC + ECS + RDS

import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
import * as pulumi from "@pulumi/pulumi";

const env = pulumi.getStack();

const vpc = new awsx.ec2.Vpc("main-vpc", {
    cidrBlock: "10.0.0.0/16",
    subnets: [
        { type: "public", cidrMask: 24 },
        { type: "private", cidrMask: 24 },
    ],
    tags: { Environment: env },
});

const dbSubnetGroup = new aws.rds.SubnetGroup("db-subnet", {
    subnetIds: vpc.privateSubnetIds,
    tags: { Environment: env },
});

const db = new aws.rds.Instance("app-db", {
    engine: "postgres",
    engineVersion: "16.4",
    instanceClass: "db.t3.medium",
    allocatedStorage: 100,
    storageType: "gp3",
    dbName: "appdb",
    username: "appadmin",
    password: pulumi.Config.requireSecret("db-password"),
    dbSubnetGroupName: dbSubnetGroup.name,
    vpcSecurityGroupIds: [vpc.vpc.defaultSecurityGroupId],
    skipFinalSnapshot: true,
    tags: { Environment: env },
});

const repo = new aws.ecr.Repository("app-repo", {
    imageScanningConfiguration: { scanOnPush: true },
    tags: { Environment: env },
});

const cluster = new aws.ecs.Cluster("app-cluster", {
    tags: { Environment: env },
});

const appService = new awsx.ecs.FargateService("app-service", {
    cluster: cluster.arn,
    desiredCount: env === "prod" ? 3 : 1,
    taskDefinitionArgs: {
        container: {
            name: "app",
            image: repo.latestImageDigest,
            portMappings: [{ containerPort: 8080 }],
            environment: [
                { name: "DATABASE_URL", value: pulumi.interpolate`postgresql://${db.username}:${db.password}@${db.endpoint}/appdb` },
                { name: "NODE_ENV", value: env },
            ],
            cpu: 256,
            memory: 512,
        },
    },
    subnetIds: vpc.privateSubnetIds,
    securityGroups: [vpc.vpc.defaultSecurityGroupId],
});

export const dbEndpoint = db.endpoint;
export const ecrRepoUrl = repo.repositoryUrl;

Deploy GCP Resources

import * as gcp from "@pulumi/gcp";

const cluster = new gcp.container.Cluster("app-cluster", {
    project: gcp.config.project,
    location: gcp.config.region || "us-central1",
    initialNodeCount: 3,
    nodeConfig: {
        machineType: "e2-medium",
        oauthScopes: ["https://www.googleapis.com/auth/cloud-platform"],
    },
});

const db = new gcp.sql.DatabaseInstance("app-db", {
    project: gcp.config.project,
    region: gcp.config.region || "us-central1",
    databaseVersion: "POSTGRES_16",
    settings: { tier: "db-f1-micro", diskSize: 20 },
});

export const clusterName = cluster.name;
export const dbName = db.name;

Deploy Azure Resources

import * as azure from "@pulumi/azure";

const resourceGroup = new azure.core.ResourceGroup("app-rg", {
    location: "East Asia",
    tags: { Environment: pulumi.getStack() },
});

const aks = new azure.containerservice.KubernetesCluster("app-aks", {
    resourceGroupName: resourceGroup.name,
    location: resourceGroup.location,
    defaultNodePool: {
        name: "default",
        nodeCount: 3,
        vmSize: "Standard_D2s_v3",
    },
    identity: { type: "SystemAssigned" },
});

export const aksFqdn = aks.fqdn;

Stack Management and Configuration

pulumi stack init dev
pulumi stack init prod

pulumi config set aws:region us-east-1
pulumi config set db-password "secret-value" --secret

pulumi stack select prod
pulumi preview
pulumi up
pulumi stack output apiUrl

Using Configuration in Code

const config = new pulumi.Config();
const instanceCount = config.requireNumber("instance-count");
const dbPassword = config.requireSecret("db-password");

const isProd = pulumi.getStack() === "prod";
const dbInstanceClass = isProd ? "db.r6g.xlarge" : "db.t3.medium";

Testing Infrastructure with Pulumi Testing Framework

Unit Tests

import * as pulumi from "@pulumi/pulumi";
import { describe, it, expect } from "mocha";

pulumi.runtime.setMocks({
    newResource: (args) => ({
        id: `${args.name}-id`,
        state: args.inputs,
    }),
    call: (args) => args.inputs,
});

describe("Infrastructure", () => {
    it("should use larger DB in production", () => {
        const isProd = true;
        const dbInstanceClass = isProd ? "db.r6g.xlarge" : "db.t3.medium";
        expect(dbInstanceClass).to.equal("db.r6g.xlarge");
    });

    it("should create ECS service with correct desired count", () => {
        const env = "prod";
        const desiredCount = env === "prod" ? 3 : 1;
        expect(desiredCount).to.equal(3);
    });
});

Policy Tests

import { PolicyPack, validateResource } from "@pulumi/policy";

new PolicyPack("infra-policies", {
    policies: [
        {
            name: "no-public-s3-buckets",
            description: "S3 buckets must not be publicly accessible",
            enforcementLevel: "mandatory",
            validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args) => {
                if (args.acl === "public-read") {
                    return "S3 bucket must not be publicly accessible";
                }
            }),
        },
        {
            name: "required-tags",
            description: "All resources must have Environment tag",
            enforcementLevel: "mandatory",
            validateResource: (args) => {
                const tags = args.props.tags;
                if (!tags || !tags.Environment) {
                    return "All resources must have an Environment tag";
                }
            },
        },
    ],
});

Pulumi vs Terraform vs CDK

Dimension Pulumi Terraform CDK
Languages TS/Python/Go/C# HCL TS/Python/Java/C#
State Pulumi Cloud/Self-hosted State File CDK Cloud
Providers 200+ 3000+ AWS-primary
Testing Native terraform test Jest
Secrets Config secrets SOPS/Vault Context
Import Existing pulumi import terraform import cdk import
Multi-cloud Native Native AWS-primary
Business Model Open Source + Cloud Open Source + Cloud Open Source

5 Common Pitfalls

1. Output Values Not Available at Runtime

// Wrong
const endpoint = db.endpoint;
console.log(endpoint);  // Promise, not actual value

// Correct
const dbUrl = pulumi.interpolate`postgresql://${db.endpoint}/appdb`;
db.endpoint.apply(ep => console.log(`DB endpoint: ${ep}`));

2. Resource Dependencies Not Correctly Declared

const app = new aws.ecs.Service("app", {
    // ...
}, { dependsOn: [db] });

3. Storing Secrets in Plain Text

# Wrong
pulumi config set db-password "my-secret"

# Correct
pulumi config set db-password "my-secret" --secret

4. Ignoring Destroy Protection

const db = new aws.rds.Instance("app-db", {
    deletionProtection: true,
    skipFinalSnapshot: false,
    finalSnapshotIdentifier: `app-db-final-${Date.now()}`,
});

5. Race Conditions from Parallel Deploys

pulumi login  # Pulumi Cloud manages locks automatically

10 Error Troubleshooting

# Symptom Possible Cause Resolution
1 requireSecret returns encrypted value Used outside apply Use apply or interpolate
2 Resource creation timeout Cloud API rate limiting Check parallelism, add retry
3 Provider version incompatible Outdated package.json pulumi plugin install
4 Stack state corrupted Interrupted deploy pulumi state unprotect
5 Type errors TypeScript type mismatch tsc --noEmit
6 Circular dependency Resources reference each other Restructure dependency graph
7 Missing configuration Stack config not set pulumi config
8 Insufficient permissions Cloud credentials lack perms Check IAM roles
9 Drift detected Manual cloud resource changes pulumi refresh
10 Import existing resource fails Incorrect resource ID Check ID format
pulumi stack
pulumi preview --diff
pulumi stack history
pulumi refresh
pulumi stack --show-resources


Summary: Pulumi + TypeScript lets developers define infrastructure in familiar programming languages, enjoying type safety, IDE support, and the npm ecosystem. In 2026, Pulumi supports 200+ providers covering AWS, GCP, Azure, and more. Key practices: handle Output's async nature correctly, use --secret for sensitive configs, enable deletion protection, write Policy checks for compliance, and use the Testing Framework to validate infrastructure logic. For developers familiar with TypeScript, Pulumi is a more natural and efficient IaC choice than Terraform.

Try these browser-local tools — no sign-up required →

#Pulumi#IaC#TypeScript#基础设施即代码#AWS#GCP#Azure#云资源管理