All Articles

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.

DevOpsBoysMar 19, 20266 min read
Share:Tweet

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

bash
brew install pulumi

Linux

bash
curl -fsSL https://get.pulumi.com | sh

Windows

powershell
winget install pulumi

Verify:

bash
pulumi version
# v3.x.x

Step 2 — Set Up Your Cloud Credentials

For AWS:

bash
# 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-1

Pulumi uses the same cloud credentials as the CLI tools — no separate authentication needed.

Step 3 — Create Your First Project

bash
mkdir my-infra && cd my-infra
pulumi new aws-typescript

Pulumi 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:

typescript
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

bash
# Preview changes (like terraform plan)
pulumi preview

Output 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:

bash
pulumi up

Confirm 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

typescript
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

typescript
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

typescript
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:

bash
# 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 up

Access stack config in code:

typescript
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

bash
pulumi login s3://my-pulumi-state-bucket

Local Backend

bash
pulumi login --local

Step 9 — Secrets Management

Pulumi encrypts secrets automatically:

bash
# Set a secret (encrypted in state)
pulumi config set --secret dbPassword "super-secret-123"

Access in code:

typescript
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

yaml
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

bash
pulumi destroy  # Removes all resources
pulumi stack rm dev  # Removes the stack

Wrapping 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.

Newsletter

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

Comments