Pulumi Infrastructure as Code Introduction

Pulumi is a modern infrastructure-as-code platform that lets you define cloud resources using general-purpose programming languages like Python, Go, TypeScript, and C#. Unlike declarative configuration languages, Pulumi leverages full programming language capabilities for infrastructure definition, enabling more expressive, reusable, and maintainable infrastructure code. This guide covers installation, language support, stack management, configuration, secrets handling, state management, and comparison with Terraform.

Table of Contents

  1. Pulumi Overview and Philosophy
  2. Installation and Setup
  3. Python Infrastructure as Code
  4. TypeScript Infrastructure as Code
  5. Stack and Project Management
  6. Configuration and Secrets
  7. State Management
  8. Comparison with Terraform
  9. Pulumi Best Practices
  10. Conclusion

Pulumi Overview and Philosophy

Pulumi shifts infrastructure-as-code paradigm by using real programming languages instead of domain-specific languages. This approach provides several advantages: access to full language features, easier code reuse through functions and classes, type safety, IDE support, and powerful abstraction patterns.

Key benefits:

  • Real Languages: Use Python, Go, TypeScript, C# with all language features
  • Type Safety: Static type checking catches errors early
  • Reusability: Functions, classes, and libraries for infrastructure patterns
  • IDE Support: Full IDE features, autocomplete, refactoring
  • Testing: Unit tests and integration tests with standard testing frameworks
  • Familiar Patterns: Leverage existing programming knowledge

Architecture:

┌─────────────────────┐
│   Pulumi Program    │
│  (Python/Go/TS/C#)  │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Pulumi Runtime     │
│  (Language Runtime) │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Pulumi Engine      │
│  (State Mgmt, etc)  │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Cloud Providers    │
│  (AWS, Azure, GCP)  │
└─────────────────────┘

Installation and Setup

Install Pulumi and configure cloud provider credentials.

Install Pulumi CLI:

# macOS
brew install pulumi

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

# Windows
choco install pulumi

# Verify installation
pulumi version
# Output: v3.80.0

Configure cloud provider credentials:

# AWS
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...

# Or use AWS CLI configuration
aws configure

# Azure
az login

# GCP
gcloud auth login
gcloud config set project my-project

Create Pulumi account:

# Sign up at https://app.pulumi.com

# Login to Pulumi
pulumi login

# Or use self-managed backend
pulumi login s3://my-pulumi-state-bucket
pulumi login file://~/.pulumi-state

Python Infrastructure as Code

Define infrastructure using Python for full language capabilities.

Create Python project:

# Create new project
mkdir my-infra && cd my-infra
pulumi new aws-python

# This creates:
# __main__.py      - Main infrastructure code
# Pulumi.yaml      - Project configuration
# Pulumi.dev.yaml  - Stack configuration
# requirements.txt - Python dependencies
# venv/            - Virtual environment

Basic Python infrastructure:

# __main__.py
import pulumi
import pulumi_aws as aws

# Get stack reference for configuration
config = pulumi.Config()
environment = config.require("environment")

# Create VPC
vpc = aws.ec2.Vpc("my-vpc",
    cidr_block="10.0.0.0/16",
    enable_dns_hostnames=True,
    tags={
        "Name": f"vpc-{environment}",
        "Environment": environment,
    }
)

# Create subnet
subnet = aws.ec2.Subnet("public-subnet",
    vpc_id=vpc.id,
    cidr_block="10.0.1.0/24",
    availability_zone=f"{config.require('region')}a",
    map_public_ip_on_launch=True
)

# Create security group
security_group = aws.ec2.SecurityGroup("web-sg",
    vpc_id=vpc.id,
    description="Security group for web servers",
    ingress=[
        aws.ec2.SecurityGroupIngressArgs(
            protocol="tcp",
            from_port=80,
            to_port=80,
            cidr_blocks=["0.0.0.0/0"],
        ),
        aws.ec2.SecurityGroupIngressArgs(
            protocol="tcp",
            from_port=443,
            to_port=443,
            cidr_blocks=["0.0.0.0/0"],
        ),
    ]
)

# Create EC2 instance
instance = aws.ec2.Instance("web-server",
    ami="ami-0c55b159cbfafe1f0",  # Ubuntu 20.04
    instance_type="t3.micro",
    subnet_id=subnet.id,
    vpc_security_group_ids=[security_group.id],
    tags={
        "Name": f"web-{environment}",
    }
)

# Export outputs
pulumi.export("vpc_id", vpc.id)
pulumi.export("instance_id", instance.id)
pulumi.export("instance_public_ip", instance.public_ip)

Python with functions for reusability:

# __main__.py
import pulumi
import pulumi_aws as aws

def create_vpc(name: str, cidr: str, environment: str) -> aws.ec2.Vpc:
    """Create VPC with common configuration"""
    return aws.ec2.Vpc(name,
        cidr_block=cidr,
        enable_dns_hostnames=True,
        enable_dns_support=True,
        tags={
            "Name": name,
            "Environment": environment,
        }
    )

def create_subnet(name: str, vpc_id: pulumi.Input[str], 
                 cidr: str, az: str) -> aws.ec2.Subnet:
    """Create subnet with configuration"""
    return aws.ec2.Subnet(name,
        vpc_id=vpc_id,
        cidr_block=cidr,
        availability_zone=az,
        map_public_ip_on_launch=True,
        tags={"Name": name}
    )

def create_security_group(name: str, vpc_id: pulumi.Input[str],
                         ingress_rules: list) -> aws.ec2.SecurityGroup:
    """Create security group with rules"""
    return aws.ec2.SecurityGroup(name,
        vpc_id=vpc_id,
        description=f"Security group for {name}",
        ingress=ingress_rules,
        tags={"Name": name}
    )

# Use functions
config = pulumi.Config()
environment = config.require("environment")

vpc = create_vpc("my-vpc", "10.0.0.0/16", environment)
subnet = create_subnet("public-subnet", vpc.id, "10.0.1.0/24", "us-east-1a")
sg = create_security_group("web-sg", vpc.id, [
    aws.ec2.SecurityGroupIngressArgs(
        protocol="tcp", from_port=80, to_port=80, cidr_blocks=["0.0.0.0/0"]
    )
])

Python classes for infrastructure components:

# vpc.py
import pulumi
import pulumi_aws as aws
from typing import Optional

class VpcConfig:
    def __init__(self, 
                 name: str,
                 cidr_block: str,
                 public_subnets: dict,
                 private_subnets: dict):
        self.name = name
        self.cidr_block = cidr_block
        self.public_subnets = public_subnets
        self.private_subnets = private_subnets

class VpcComponent(pulumi.ComponentResource):
    """VPC component encapsulating VPC, subnets, and gateways"""
    
    def __init__(self, name: str, config: VpcConfig, opts=None):
        super().__init__("custom:network:Vpc", name, None, opts)
        
        # Create VPC
        self.vpc = aws.ec2.Vpc(f"{name}-vpc",
            cidr_block=config.cidr_block,
            tags={"Name": name},
            opts=pulumi.ResourceOptions(parent=self)
        )
        
        # Create public subnets
        self.public_subnets = {}
        for subnet_name, subnet_config in config.public_subnets.items():
            self.public_subnets[subnet_name] = aws.ec2.Subnet(
                f"{name}-{subnet_name}",
                vpc_id=self.vpc.id,
                cidr_block=subnet_config["cidr"],
                availability_zone=subnet_config["az"],
                map_public_ip_on_launch=True,
                tags={"Name": f"{name}-{subnet_name}", "Type": "public"},
                opts=pulumi.ResourceOptions(parent=self)
            )
        
        # Create internet gateway
        self.igw = aws.ec2.InternetGateway(f"{name}-igw",
            vpc_id=self.vpc.id,
            tags={"Name": f"{name}-igw"},
            opts=pulumi.ResourceOptions(parent=self)
        )
        
        # Export outputs
        self.register_outputs({
            "vpc_id": self.vpc.id,
            "public_subnet_ids": {k: v.id for k, v in self.public_subnets.items()},
            "igw_id": self.igw.id,
        })

# Usage in main
# __main__.py
import pulumi
from vpc import VpcComponent, VpcConfig

config = pulumi.Config()

vpc_config = VpcConfig(
    name="prod-vpc",
    cidr_block="10.0.0.0/16",
    public_subnets={
        "us-east-1a": {"cidr": "10.0.1.0/24", "az": "us-east-1a"},
        "us-east-1b": {"cidr": "10.0.2.0/24", "az": "us-east-1b"},
    },
    private_subnets={}
)

vpc = VpcComponent("prod", vpc_config)
pulumi.export("vpc_id", vpc.vpc.id)

TypeScript Infrastructure as Code

Define infrastructure using TypeScript with type safety.

Create TypeScript project:

pulumi new aws-typescript
cd my-project

# Install dependencies
npm install

Basic TypeScript 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");

// Create VPC
const vpc = new aws.ec2.Vpc("my-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
    tags: {
        Name: `vpc-${environment}`,
        Environment: environment,
    },
});

// Create subnet
const subnet = new aws.ec2.Subnet("public-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    availabilityZone: `${config.require("region")}a`,
    mapPublicIpOnLaunch: true,
});

// Create security group
const securityGroup = new aws.ec2.SecurityGroup("web-sg", {
    vpcId: vpc.id,
    description: "Security group for web servers",
    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"],
        },
    ],
});

// Create EC2 instance
const instance = new aws.ec2.Instance("web-server", {
    ami: "ami-0c55b159cbfafe1f0",
    instanceType: "t3.micro",
    subnetId: subnet.id,
    vpcSecurityGroupIds: [securityGroup.id],
    tags: {
        Name: `web-${environment}`,
    },
});

// Export outputs
export const vpcId = vpc.id;
export const instanceId = instance.id;
export const instancePublicIp = instance.publicIp;

TypeScript with interfaces and classes:

// vpc.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

interface SubnetConfig {
    cidr: string;
    az: string;
}

interface VpcComponentArgs {
    name: string;
    cidrBlock: string;
    publicSubnets: Record<string, SubnetConfig>;
}

export class VpcComponent extends pulumi.ComponentResource {
    public readonly vpcId: pulumi.Output<string>;
    public readonly subnetIds: pulumi.Output<Record<string, string>>;

    constructor(name: string, args: VpcComponentArgs,
                opts?: pulumi.ComponentResourceOptions) {
        super("custom:network:Vpc", name, {}, opts);

        // Create VPC
        const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
            cidrBlock: args.cidrBlock,
            tags: { Name: name },
        }, { parent: this });

        // Create subnets
        const subnetIds: Record<string, string> = {};
        for (const [subnetName, subnetConfig] of 
             Object.entries(args.publicSubnets)) {
            const subnet = new aws.ec2.Subnet(
                `${name}-${subnetName}`,
                {
                    vpcId: vpc.id,
                    cidrBlock: subnetConfig.cidr,
                    availabilityZone: subnetConfig.az,
                    mapPublicIpOnLaunch: true,
                    tags: { Name: `${name}-${subnetName}` },
                },
                { parent: this }
            );
            subnetIds[subnetName] = subnet.id;
        }

        this.vpcId = vpc.id;
        this.subnetIds = pulumi.output(subnetIds);

        this.registerOutputs({
            vpcId: vpc.id,
            subnetIds: pulumi.output(subnetIds),
        });
    }
}

// index.ts - Usage
import { VpcComponent } from "./vpc";

const vpc = new VpcComponent("prod", {
    name: "prod-vpc",
    cidrBlock: "10.0.0.0/16",
    publicSubnets: {
        "us-east-1a": { cidr: "10.0.1.0/24", az: "us-east-1a" },
        "us-east-1b": { cidr: "10.0.2.0/24", az: "us-east-1b" },
    },
});

export const vpcId = vpc.vpcId;

Stack and Project Management

Manage multiple infrastructure stacks and configurations.

Project structure:

my-infrastructure/
├── Pulumi.yaml           # Project metadata
├── Pulumi.dev.yaml       # Dev stack config
├── Pulumi.staging.yaml   # Staging stack config
├── Pulumi.prod.yaml      # Production stack config
├── __main__.py           # Infrastructure code
├── vpc.py                # VPC component
├── requirements.txt
└── .pulumi/              # Pulumi state

Pulumi.yaml:

name: my-infrastructure
runtime: python
description: Infrastructure for web application

config:
  aws:region:
    description: AWS region
    default: us-east-1

Stack-specific configuration:

# Pulumi.dev.yaml
config:
  environment: dev
  instance_type: t2.micro
  instance_count: 1

# Pulumi.staging.yaml
config:
  environment: staging
  instance_type: t2.small
  instance_count: 2

# Pulumi.prod.yaml
config:
  environment: production
  instance_type: m5.large
  instance_count: 5

Create and manage stacks:

# Create new stack
pulumi stack init staging

# List stacks
pulumi stack ls

# Select stack
pulumi stack select staging

# Show stack info
pulumi stack output
pulumi stack output instance_id

# Delete stack (dangerous)
pulumi stack rm --yes

Deploy to different stacks:

# Deploy to dev
pulumi stack select dev
pulumi up

# Deploy to staging
pulumi stack select staging
pulumi up

# Deploy to production
pulumi stack select prod
pulumi up

Configuration and Secrets

Manage configuration values and sensitive data securely.

Configuration values:

# __main__.py
import pulumi

config = pulumi.Config()

# Required values
environment = config.require("environment")
region = config.require("aws:region")

# Optional with defaults
instance_type = config.get("instance_type") or "t2.micro"
instance_count = config.get_int("instance_count") or 1
enable_monitoring = config.get_bool("enable_monitoring") or True

# Nested configuration
db_config = config.require_object("database")
db_name = db_config["name"]
db_engine = db_config["engine"]

Secrets management:

# Create secret
pulumi config set --secret db_password "my-secure-password"

# View secret (masked)
pulumi config
# db_password: [secret]

# Use secret in code
db_password = config.require_secret("db_password")

# Mark output as sensitive
pulumi.export("db_password", db_password)  # Output marked sensitive automatically

Secrets in code:

# __main__.py
import pulumi
import pulumi_aws as aws

config = pulumi.Config()

# Create RDS instance with secret
db_password = config.require_secret("db_password")

db = aws.rds.Instance("mydb",
    allocated_storage=20,
    engine="mysql",
    instance_class="db.t3.micro",
    db_name="myapp",
    username="admin",
    password=db_password,
    skip_final_snapshot=False,
)

# Export password as sensitive
pulumi.export("db_password", db_password)

Environment-specific secrets:

# Store secrets per stack
pulumi config set --secret -s prod db_password "prod-password"
pulumi config set --secret -s staging db_password "staging-password"

# Secrets are encrypted and stored securely

State Management

Pulumi manages state similarly to Terraform but with different storage options.

State storage:

# Pulumi.com hosted state (default)
pulumi login

# Local file state
pulumi login file://~/.pulumi-state

# S3 backend
pulumi login s3://my-pulumi-state-bucket

# Azure Blob Storage
pulumi login azurblob://container@storageaccount

Inspect state:

# Show stack state
pulumi stack export

# Show in JSON
pulumi stack export | jq

# Show specific resource
pulumi export | jq '.resources[] | select(.name=="my-instance")'

State operations:

# Import state from another stack
pulumi stack export > backup.json

# Restore state
pulumi stack import < backup.json

# Refresh state
pulumi refresh

Comparison with Terraform

Understanding differences helps choose the right tool.

Terraform Strengths:

  • Declarative approach (what you want)
  • Smaller learning curve for operators
  • Larger provider ecosystem
  • Better multi-cloud uniformity
  • More widely adopted

Pulumi Strengths:

  • Real programming languages
  • Full language features (loops, conditions, functions)
  • Better code reuse and abstraction
  • IDE support and type safety
  • Easier testing with unit tests
  • Single language for infra and application code

Side-by-side comparison:

# Terraform (HCL)
variable "instance_count" {
  type = number
}

resource "aws_instance" "web" {
  count = var.instance_count
  ami = "ami-123"
}

# Pulumi (Python)
config = pulumi.Config()
instance_count = config.get_int("instance_count") or 3

instances = []
for i in range(instance_count):
    instances.append(aws.ec2.Instance(f"web-{i}",
        ami="ami-123"
    ))

Pulumi Best Practices

Organize code logically:

# components/vpc.py - Reusable components
class VpcComponent(pulumi.ComponentResource):
    pass

# components/database.py
class DatabaseComponent(pulumi.ComponentResource):
    pass

# __main__.py - Orchestrate components
vpc = VpcComponent("prod-vpc", args)
db = DatabaseComponent("prod-db", args)

Use secrets for sensitive data:

# Never hardcode
password = "my-password"  # WRONG

# Use config secrets
password = config.require_secret("db_password")  # RIGHT

Implement proper tagging:

def create_tags(environment: str, component: str) -> dict:
    return {
        "Environment": environment,
        "Component": component,
        "ManagedBy": "Pulumi",
        "CreatedAt": pulumi.get_stack(),
    }

instance = aws.ec2.Instance("web",
    tags=create_tags("prod", "web-server")
)

Conclusion

Pulumi modernizes infrastructure-as-code by leveraging real programming languages for infrastructure definition. The combination of type safety, full language capabilities, reusable components, and strong testing support makes Pulumi ideal for complex infrastructure projects where teams have strong programming backgrounds. Whether using Python for quick prototyping or TypeScript for large enterprise deployments, Pulumi provides a powerful platform for managing cloud infrastructure as code.