Pulumi

DevOpsInfrastructure as CodeIaCTypeScriptPythonGoC#JavaProgramming LanguagesMulti-cloud

DevOps Tool

Pulumi

Overview

Pulumi is a modern Infrastructure as Code (IaC) platform that uses programming languages like TypeScript, Python, Go, C#, and Java to define infrastructure. Instead of traditional DSL (Domain Specific Language), it leverages familiar programming languages and supports over 150 cloud providers.

Details

Founded in 2017, Pulumi was developed with the philosophy of "true code-based infrastructure management." Rather than using DSL like Terraform, it allows developers to define infrastructure using general-purpose programming languages (TypeScript, Python, Go, C#, Java) that they already know.

This approach significantly improves developer productivity by leveraging existing development tools (IDEs, debuggers, package managers, testing frameworks) directly. Pulumi provides over 14,000 code snippets and rich implementation examples, continuing to grow rapidly with its developer-friendly approach.

A unique feature of Pulumi is the ability to directly utilize programming language features (conditionals, loops, functions, classes, package management) in infrastructure definitions. Additionally, the Automation API enables embedding infrastructure management into applications.

Pros and Cons

Pros

  • Programming language usage: Leverage existing development skills and tools directly
  • Rich providers: Support for over 150 cloud providers
  • Development tool integration: Utilize IDEs, debuggers, and testing frameworks
  • Automation API: Automate infrastructure management programmatically
  • Strong type system: Compile-time error detection and IntelliSense

Cons

  • Learning curve: Requires programming language knowledge (high barrier for non-programmers)
  • Complexity: Potential to write overly complex code
  • Tool dependency: Dependent on each language's ecosystem
  • Performance: Execution time may be longer in large-scale environments

References

Official Resources

Learning Resources

Usage Examples

Basic Project Creation (TypeScript)

# Create new project
mkdir my-pulumi-app && cd my-pulumi-app

# Initialize with TypeScript template
pulumi new typescript

# AWS project template
pulumi new aws-typescript

# Azure project template  
pulumi new azure-typescript

# GCP project template
pulumi new gcp-typescript

# Kubernetes project template
pulumi new kubernetes-typescript

AWS S3 Bucket Creation (TypeScript)

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

// Create S3 bucket
const bucket = new aws.s3.Bucket("my-bucket", {
    // Bucket name is auto-generated
    bucket: undefined,
    // Public access block
    publicAccessBlock: new aws.s3.BucketPublicAccessBlock("bucket-pab", {
        bucket: bucket.id,
        blockPublicAcls: true,
        blockPublicPolicy: true,
        ignorePublicAcls: true,
        restrictPublicBuckets: true,
    }),
    // Server-side encryption
    serverSideEncryptionConfiguration: {
        rule: {
            applyServerSideEncryptionByDefault: {
                sseAlgorithm: "AES256",
            },
        },
    },
    // Enable versioning
    versioning: {
        enabled: true,
    },
});

// Export bucket name
export const bucketName = bucket.id;
export const bucketUrl = pulumi.interpolate`https://${bucket.bucket}.s3.amazonaws.com`;

Conditionals and Loops (TypeScript)

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

const config = new pulumi.Config();
const environment = config.require("environment"); // dev, staging, prod

// Environment-based instance type selection
const instanceType = environment === "prod" ? "t3.large" : "t3.micro";

// Multi-AZ configuration for production only
const subnetIds = environment === "prod" 
    ? ["subnet-12345", "subnet-67890", "subnet-abcdef"]
    : ["subnet-12345"];

// Create multiple web servers with loop
const webServers: aws.ec2.Instance[] = [];
for (let i = 0; i < subnetIds.length; i++) {
    const server = new aws.ec2.Instance(`web-server-${i}`, {
        ami: "ami-0c94855ba95b798c7", // Amazon Linux 2
        instanceType: instanceType,
        subnetId: subnetIds[i],
        tags: {
            Name: `WebServer-${environment}-${i + 1}`,
            Environment: environment,
        },
    });
    webServers.push(server);
}

// Loop using object array
const environments = [
    { name: "dev", count: 1 },
    { name: "staging", count: 2 },
    { name: "prod", count: 3 },
];

environments.forEach(env => {
    for (let i = 0; i < env.count; i++) {
        new aws.ec2.Instance(`${env.name}-instance-${i}`, {
            ami: "ami-0c94855ba95b798c7",
            instanceType: env.name === "prod" ? "t3.large" : "t3.micro",
            tags: {
                Name: `${env.name}-instance-${i}`,
                Environment: env.name,
            },
        });
    }
});

Component Resource Creation (TypeScript)

// webserver.ts - Reusable component
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

export interface WebServerArgs {
    vpcId: pulumi.Input<string>;
    subnetId: pulumi.Input<string>;
    instanceType?: string;
    keyName?: string;
}

export class WebServer extends pulumi.ComponentResource {
    public readonly instance: aws.ec2.Instance;
    public readonly securityGroup: aws.ec2.SecurityGroup;
    public readonly publicIp: pulumi.Output<string>;

    constructor(name: string, args: WebServerArgs, opts?: pulumi.ComponentResourceOptions) {
        super("custom:WebServer", name, {}, opts);

        // Create security group
        this.securityGroup = new aws.ec2.SecurityGroup(`${name}-sg`, {
            vpcId: args.vpcId,
            description: "Security group for web server",
            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"],
                },
                {
                    protocol: "tcp",
                    fromPort: 22,
                    toPort: 22,
                    cidrBlocks: ["0.0.0.0/0"],
                },
            ],
            egress: [{
                protocol: "-1",
                fromPort: 0,
                toPort: 0,
                cidrBlocks: ["0.0.0.0/0"],
            }],
        }, { parent: this });

        // Create EC2 instance
        this.instance = new aws.ec2.Instance(`${name}-instance`, {
            ami: "ami-0c94855ba95b798c7", // Amazon Linux 2
            instanceType: args.instanceType || "t3.micro",
            subnetId: args.subnetId,
            keyName: args.keyName,
            vpcSecurityGroupIds: [this.securityGroup.id],
            userData: `#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from ${name}!</h1>" > /var/www/html/index.html
`,
            tags: {
                Name: name,
            },
        }, { parent: this });

        this.publicIp = this.instance.publicIp;

        // Register component outputs
        this.registerOutputs({
            instance: this.instance,
            securityGroup: this.securityGroup,
            publicIp: this.publicIp,
        });
    }
}

// Usage example (index.ts)
import { WebServer } from "./webserver";

const vpc = new aws.ec2.Vpc("main-vpc", {
    cidrBlock: "10.0.0.0/16",
});

const subnet = new aws.ec2.Subnet("public-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    mapPublicIpOnLaunch: true,
});

const webServer = new WebServer("my-web-server", {
    vpcId: vpc.id,
    subnetId: subnet.id,
    instanceType: "t3.small",
});

export const serverUrl = pulumi.interpolate`http://${webServer.publicIp}`;

Python Implementation Example

# __main__.py
import pulumi
import pulumi_aws as aws

# Get configuration values
config = pulumi.Config()
environment = config.require("environment")

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

# Internet Gateway
igw = aws.ec2.InternetGateway("main-igw",
    vpc_id=vpc.id,
    tags={
        "Name": f"{environment}-igw",
    }
)

# Public subnet
public_subnet = aws.ec2.Subnet("public-subnet",
    vpc_id=vpc.id,
    cidr_block="10.0.1.0/24",
    map_public_ip_on_launch=True,
    tags={
        "Name": f"{environment}-public-subnet",
    }
)

# Route table
route_table = aws.ec2.RouteTable("public-route-table",
    vpc_id=vpc.id,
    routes=[{
        "cidr_block": "0.0.0.0/0",
        "gateway_id": igw.id,
    }],
    tags={
        "Name": f"{environment}-public-rt",
    }
)

# Route table association
aws.ec2.RouteTableAssociation("public-route-table-association",
    subnet_id=public_subnet.id,
    route_table_id=route_table.id
)

# Security group
security_group = aws.ec2.SecurityGroup("web-sg",
    vpc_id=vpc.id,
    description="Security group for web server",
    ingress=[
        {
            "protocol": "tcp",
            "from_port": 80,
            "to_port": 80,
            "cidr_blocks": ["0.0.0.0/0"],
        },
        {
            "protocol": "tcp",
            "from_port": 443,
            "to_port": 443,
            "cidr_blocks": ["0.0.0.0/0"],
        },
    ],
    egress=[{
        "protocol": "-1",
        "from_port": 0,
        "to_port": 0,
        "cidr_blocks": ["0.0.0.0/0"],
    }],
    tags={
        "Name": f"{environment}-web-sg",
    }
)

# EC2 instance
instance = aws.ec2.Instance("web-server",
    ami="ami-0c94855ba95b798c7",  # Amazon Linux 2
    instance_type="t3.micro" if environment != "prod" else "t3.small",
    subnet_id=public_subnet.id,
    vpc_security_group_ids=[security_group.id],
    user_data="""#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from Pulumi Python!</h1>" > /var/www/html/index.html
""",
    tags={
        "Name": f"{environment}-web-server",
        "Environment": environment,
    }
)

# Outputs
pulumi.export("vpc_id", vpc.id)
pulumi.export("instance_ip", instance.public_ip)
pulumi.export("website_url", instance.public_ip.apply(lambda ip: f"http://{ip}"))

Automation API (Programmatic Execution)

// automation.ts - Execute Pulumi programmatically
import * as pulumi from "@pulumi/pulumi";
import { LocalWorkspace } from "@pulumi/pulumi/automation";
import * as aws from "@pulumi/aws";

// Infrastructure defined as a program
const pulumiProgram = async () => {
    const bucket = new aws.s3.Bucket("auto-bucket", {
        serverSideEncryptionConfiguration: {
            rule: {
                applyServerSideEncryptionByDefault: {
                    sseAlgorithm: "AES256",
                },
            },
        },
    });

    return {
        bucketName: bucket.id,
    };
};

async function main() {
    const stackName = "dev";
    const projectName = "automation-example";

    // Create Pulumi workspace
    const stack = await LocalWorkspace.createOrSelectStack({
        stackName,
        projectName,
        program: pulumiProgram,
    });

    console.log("Refreshing stack...");
    await stack.refresh({ onOutput: console.info });

    console.log("Setting up configuration...");
    await stack.setConfig("aws:region", { value: "us-west-2" });

    console.log("Updating stack...");
    const upRes = await stack.up({ onOutput: console.info });
    console.log(`Update summary: ${upRes.summary.resourceChanges}`);
    console.log(`Bucket name: ${upRes.outputs.bucketName.value}`);
}

main().catch(console.error);

Kubernetes Resource Management

// k8s.ts
import * as k8s from "@pulumi/kubernetes";
import * as pulumi from "@pulumi/pulumi";

// Create Namespace
const namespace = new k8s.core.v1.Namespace("app-namespace", {
    metadata: {
        name: "my-app",
    },
});

// ConfigMap
const configMap = new k8s.core.v1.ConfigMap("app-config", {
    metadata: {
        namespace: namespace.metadata.name,
        name: "app-config",
    },
    data: {
        "config.json": JSON.stringify({
            database: {
                host: "localhost",
                port: 5432,
            },
            features: {
                logging: true,
                monitoring: true,
            },
        }),
    },
});

// Secret
const secret = new k8s.core.v1.Secret("app-secret", {
    metadata: {
        namespace: namespace.metadata.name,
        name: "app-secret",
    },
    type: "Opaque",
    stringData: {
        "db-password": "super-secret-password",
        "api-key": "abcd1234-5678-90ef",
    },
});

// Deployment
const deployment = new k8s.apps.v1.Deployment("app-deployment", {
    metadata: {
        namespace: namespace.metadata.name,
        name: "my-app",
    },
    spec: {
        replicas: 3,
        selector: {
            matchLabels: {
                app: "my-app",
            },
        },
        template: {
            metadata: {
                labels: {
                    app: "my-app",
                },
            },
            spec: {
                containers: [{
                    name: "app",
                    image: "nginx:1.21",
                    ports: [{
                        containerPort: 80,
                    }],
                    env: [
                        {
                            name: "DB_PASSWORD",
                            valueFrom: {
                                secretKeyRef: {
                                    name: secret.metadata.name,
                                    key: "db-password",
                                },
                            },
                        },
                    ],
                    volumeMounts: [{
                        name: "config-volume",
                        mountPath: "/etc/config",
                    }],
                }],
                volumes: [{
                    name: "config-volume",
                    configMap: {
                        name: configMap.metadata.name,
                    },
                }],
            },
        },
    },
});

// Service
const service = new k8s.core.v1.Service("app-service", {
    metadata: {
        namespace: namespace.metadata.name,
        name: "my-app-service",
    },
    spec: {
        selector: {
            app: "my-app",
        },
        ports: [{
            port: 80,
            targetPort: 80,
        }],
        type: "LoadBalancer",
    },
});

export const serviceEndpoint = service.status.loadBalancer.ingress[0].ip;

Basic Pulumi Commands

# Project initialization
pulumi new typescript
pulumi new aws-typescript
pulumi new kubernetes-typescript

# Stack management
pulumi stack init dev
pulumi stack select dev
pulumi stack ls

# Configuration management
pulumi config set aws:region us-west-2
pulumi config set myapp:environment dev
pulumi config set --secret myapp:apiKey abc123

# Deployment
pulumi preview  # Preview (equivalent to terraform plan)
pulumi up       # Execute deployment
pulumi up --yes # Skip confirmation

# State checking
pulumi stack output
pulumi stack output bucketName
pulumi logs

# Resource management
pulumi state delete <resource-urn>
pulumi import <resource-type> <name> <id>

# Destroy
pulumi destroy
pulumi destroy --yes

# Others
pulumi console    # Open console in browser
pulumi convert --from terraform --language typescript  # Convert from Terraform

Development Workflow

1. Project Setup

# Create new project
mkdir my-infrastructure && cd my-infrastructure
pulumi new aws-typescript

# Install dependencies
npm install

# Configuration
pulumi config set aws:region us-east-1

2. Development Cycle

# 1. Create/edit code
code index.ts

# 2. Preview (check changes)
pulumi preview

# 3. Deploy
pulumi up

# 4. Verify operation
pulumi stack output
curl $(pulumi stack output endpointUrl)

# 5. Check logs
pulumi logs

3. CI/CD Integration

# .github/workflows/pulumi.yml
name: Pulumi Infrastructure
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  pulumi:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
          
      - name: Install dependencies
        run: npm install
        
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1
          
      - name: Pulumi Preview
        uses: pulumi/actions@v3
        with:
          command: preview
          stack-name: dev
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
          
      - name: Pulumi Deploy
        if: github.ref == 'refs/heads/main'
        uses: pulumi/actions@v3
        with:
          command: up
          stack-name: production
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}