IaC with Pulumi and TypeScript: Infrastructure as Code for Developers
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
Recommended Tools
- JSON Formatter: Use /en/json/format to format Pulumi configs
- Base64 Encoder: Use /en/encode/base64 for cloud credentials and secrets
- Hash Calculator: Use /en/encode/hash for config file integrity verification
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 →