How to Set Up Pulumi and Deploy Infrastructure from Code in 2026
Step-by-step guide to getting started with Pulumi — write infrastructure in TypeScript, Python, or Go instead of HCL. Covers setup, first deployment, state management, and CI/CD integration.
Terraform uses HCL. Pulumi uses real programming languages — TypeScript, Python, Go, Java, C#. That means loops, conditionals, functions, type checking, IDE autocomplete, and unit tests — all working with your infrastructure code.
If you've ever wished you could write infrastructure code the same way you write application code, Pulumi is for you. This guide takes you from zero to deployed infrastructure.
Why Pulumi in 2026
Pulumi has hit a tipping point. Here's what changed:
- Pulumi ESC (Environments, Secrets, Config) unified secrets management across all environments
- Pulumi Copilot generates infrastructure code from natural language
- Pulumi Deployments offers managed CI/CD for infrastructure
- Infrastructure-from-Code (experimental) infers infrastructure from your application code
The ecosystem matured. The provider coverage matches Terraform (same underlying providers through Pulumi's bridge). And the developer experience is significantly better for teams that already write TypeScript or Python.
Step 1 — Install Pulumi
macOS
brew install pulumiLinux
curl -fsSL https://get.pulumi.com | shWindows
winget install pulumiVerify:
pulumi version
# v3.x.xStep 2 — Set Up Your Cloud Credentials
For AWS:
# Option 1: AWS CLI (recommended)
aws configure
# Option 2: Environment variables
export AWS_ACCESS_KEY_ID=your-key
export AWS_SECRET_ACCESS_KEY=your-secret
export AWS_REGION=us-east-1Pulumi uses the same cloud credentials as the CLI tools — no separate authentication needed.
Step 3 — Create Your First Project
mkdir my-infra && cd my-infra
pulumi new aws-typescriptPulumi asks a few questions:
project name: my-infra
project description: My first Pulumi project
stack name: dev
aws:region: us-east-1
This generates a project with:
my-infra/
├── Pulumi.yaml # Project metadata
├── Pulumi.dev.yaml # Stack config (dev)
├── index.ts # Infrastructure code
├── package.json
└── tsconfig.json
Step 4 — Write Infrastructure Code
Open index.ts. Here's a complete example deploying a VPC, subnet, and EC2 instance:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Create a VPC
const vpc = new aws.ec2.Vpc("main-vpc", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
tags: { Name: "pulumi-vpc" },
});
// Create a public subnet
const subnet = new aws.ec2.Subnet("public-subnet", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
availabilityZone: "us-east-1a",
mapPublicIpOnLaunch: true,
tags: { Name: "pulumi-public" },
});
// Create an internet gateway
const igw = new aws.ec2.InternetGateway("igw", {
vpcId: vpc.id,
});
// Create route table
const routeTable = new aws.ec2.RouteTable("public-rt", {
vpcId: vpc.id,
routes: [{
cidrBlock: "0.0.0.0/0",
gatewayId: igw.id,
}],
});
// Associate subnet with route table
new aws.ec2.RouteTableAssociation("public-rta", {
subnetId: subnet.id,
routeTableId: routeTable.id,
});
// Create a security group
const sg = 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: 22,
toPort: 22,
cidrBlocks: ["10.0.0.0/16"], // SSH only from VPC
}],
egress: [{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
}],
});
// Get the latest Amazon Linux 2 AMI
const ami = aws.ec2.getAmi({
mostRecent: true,
owners: ["amazon"],
filters: [{
name: "name",
values: ["amzn2-ami-hvm-*-x86_64-gp2"],
}],
});
// Create EC2 instance
const server = new aws.ec2.Instance("web-server", {
ami: ami.then(a => a.id),
instanceType: "t3.micro",
subnetId: subnet.id,
vpcSecurityGroupIds: [sg.id],
tags: { Name: "pulumi-web-server" },
userData: `#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from Pulumi!</h1>" > /var/www/html/index.html
`,
});
// Export outputs
export const vpcId = vpc.id;
export const publicIp = server.publicIp;
export const publicDns = server.publicDns;Notice: this is just TypeScript. You get full IDE autocomplete, type checking, and refactoring support.
Step 5 — Preview and Deploy
# Preview changes (like terraform plan)
pulumi previewOutput shows exactly what will be created:
Previewing update (dev):
Type Name Plan
+ pulumi:pulumi:Stack my-infra-dev create
+ ├─ aws:ec2:Vpc main-vpc create
+ ├─ aws:ec2:Subnet public-subnet create
+ ├─ aws:ec2:InternetGateway igw create
+ ├─ aws:ec2:RouteTable public-rt create
+ ├─ aws:ec2:RouteTableAssociation public-rta create
+ ├─ aws:ec2:SecurityGroup web-sg create
+ └─ aws:ec2:Instance web-server create
Resources:
+ 8 to create
Deploy:
pulumi upConfirm with yes. Pulumi creates all resources and shows outputs:
Outputs:
publicDns: "ec2-54-xx-xx-xx.compute-1.amazonaws.com"
publicIp : "54.xx.xx.xx"
vpcId : "vpc-0123456789abcdef"
Step 6 — Use Real Programming Features
This is where Pulumi shines over HCL. Use loops, functions, and conditionals:
Create Multiple Resources with Loops
const azs = ["us-east-1a", "us-east-1b", "us-east-1c"];
const subnets = azs.map((az, index) => {
return new aws.ec2.Subnet(`subnet-${az}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${index + 1}.0/24`,
availabilityZone: az,
tags: { Name: `subnet-${az}` },
});
});Reusable Components
interface AppServerArgs {
vpcId: pulumi.Input<string>;
subnetId: pulumi.Input<string>;
instanceType?: string;
}
class AppServer extends pulumi.ComponentResource {
public readonly publicIp: pulumi.Output<string>;
constructor(name: string, args: AppServerArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:AppServer", 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"] }],
egress: [{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] }],
}, { parent: this });
const instance = new aws.ec2.Instance(`${name}-instance`, {
instanceType: args.instanceType || "t3.micro",
ami: "ami-0123456789abcdef",
subnetId: args.subnetId,
vpcSecurityGroupIds: [sg.id],
}, { parent: this });
this.publicIp = instance.publicIp;
}
}
// Use it
const api = new AppServer("api", { vpcId: vpc.id, subnetId: subnet.id });
const worker = new AppServer("worker", { vpcId: vpc.id, subnetId: subnet.id, instanceType: "t3.large" });Conditional Resources
const config = new pulumi.Config();
const enableMonitoring = config.getBoolean("enableMonitoring") ?? false;
if (enableMonitoring) {
new aws.cloudwatch.MetricAlarm("cpu-alarm", {
comparisonOperator: "GreaterThanThreshold",
evaluationPeriods: 2,
metricName: "CPUUtilization",
namespace: "AWS/EC2",
period: 300,
statistic: "Average",
threshold: 80,
dimensions: { InstanceId: server.id },
});
}Step 7 — Manage Stacks (Environments)
Stacks are Pulumi's equivalent of Terraform workspaces:
# Create a production stack
pulumi stack init prod
# Set stack-specific config
pulumi config set aws:region us-west-2
pulumi config set instanceType t3.large
# Deploy to prod
pulumi upAccess stack config in code:
const config = new pulumi.Config();
const instanceType = config.get("instanceType") || "t3.micro";Step 8 — State Management
By default, Pulumi stores state in Pulumi Cloud (free for individuals). For self-managed state:
S3 Backend
pulumi login s3://my-pulumi-state-bucketLocal Backend
pulumi login --localStep 9 — Secrets Management
Pulumi encrypts secrets automatically:
# Set a secret (encrypted in state)
pulumi config set --secret dbPassword "super-secret-123"Access in code:
const config = new pulumi.Config();
const dbPassword = config.requireSecret("dbPassword");
new aws.rds.Instance("db", {
password: dbPassword, // Stays encrypted in state
// ...
});Step 10 — CI/CD Integration
GitHub Actions
name: Deploy Infrastructure
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- uses: pulumi/actions@v5
with:
command: up
stack-name: prod
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Destroy When Done
pulumi destroy # Removes all resources
pulumi stack rm dev # Removes the stackWrapping Up
Pulumi gives you infrastructure-as-code with real programming languages. The learning curve is lower if you already know TypeScript or Python, and the developer experience — autocomplete, type checking, testing, reusable components — is significantly better than HCL.
Start with a simple project, get comfortable with stacks and config, then gradually move your infrastructure over.
Want to build strong cloud and IaC fundamentals? KodeKloud's Terraform and cloud courses teach the concepts that apply to both Terraform and Pulumi. For an affordable cloud to practice deployments, DigitalOcean has a well-maintained Pulumi provider.
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
AWS CloudWatch: The Complete Monitoring Guide for DevOps Engineers (2026)
AWS CloudWatch is the central monitoring service for everything running on AWS. This guide covers metrics, logs, alarms, dashboards, Container Insights, and production best practices.
AWS DevOps Tools — CodePipeline to EKS Complete Overview
A complete guide to AWS DevOps services — CI/CD pipelines, container orchestration, infrastructure as code, monitoring, and security best practices.
AWS EKS vs Google GKE vs Azure AKS — Which Managed Kubernetes to Use in 2026?
Honest comparison of EKS, GKE, and AKS in 2026: pricing, developer experience, networking, autoscaling, and which one to pick for your use case.