Pulumi Best Practices

Medium 22 min read

Project Organization

Why Organization Matters

The Problem: As infrastructure grows, monolithic Pulumi programs become unmaintainable. Teams step on each other's toes and deployments take forever.

The Solution: Follow proven patterns for splitting projects, organizing stacks, and creating reusable components that scale with your team.

Real Impact: Well-organized Pulumi projects reduce deployment times by 70% and enable teams to work independently on shared infrastructure.

Real-World Analogy

Think of Pulumi project organization like organizing a large company:

  • Projects = Departments (networking, compute, databases) with clear responsibilities
  • Stacks = Environments (dev, staging, prod) running the same department blueprints
  • Component Resources = Standard operating procedures reused across departments
  • Stack References = Inter-department communication channels

Project Structure Patterns

Micro-Stacks

Split infrastructure into small, focused stacks (networking, compute, database). Reduces blast radius and enables parallel development.

Mono-Repo

Keep all Pulumi projects in one repository. Simplifies cross-project changes, shared libraries, and atomic commits.

Component Libraries

Extract common patterns into shared npm packages. Standardize VPC configs, database setups, and monitoring across teams.

Config-Driven

Use Pulumi config and stack-specific settings to parameterize deployments. Same code, different environments.

Recommended Project Structure
infra/ (mono-repo) shared/ component-resources utility functions networking/ VPC, subnets, DNS dev | staging | prod compute/ EKS, EC2, Lambda dev | staging | prod database/ RDS, DynamoDB dev | staging | prod Stack References connect projects policy-pack/ (CrossGuard rules) .github/workflows/ (CI/CD pipelines)

Recommended Directory Layout

project-structure
infra/
  shared/
    components/          # Reusable component resources
      vpc.ts
      database.ts
      monitoring.ts
    package.json         # Shared as npm package
  networking/
    index.ts             # VPC, subnets, DNS
    Pulumi.yaml
    Pulumi.dev.yaml
    Pulumi.prod.yaml
  compute/
    index.ts             # EKS, EC2, Lambda
    Pulumi.yaml
  database/
    index.ts             # RDS, DynamoDB
    Pulumi.yaml
  policy-pack/
    index.ts             # CrossGuard policies
  .github/workflows/
    preview.yml
    deploy.yml

State Management

State Backend Options

state-management.sh
# Use Pulumi Cloud (recommended for teams)
pulumi login

# Use S3 backend for self-managed state
pulumi login s3://my-state-bucket

# Use Azure Blob Storage
pulumi login azblob://my-state-container

# Refresh state to detect drift
pulumi refresh

# Export state for backup
pulumi stack export --file state-backup.json

# Import state from backup
pulumi stack import --file state-backup.json

Security Best Practices

security.ts
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();

// ALWAYS use secrets for sensitive values
const dbPassword = config.requireSecret("dbPassword");
const apiKey = config.requireSecret("apiKey");

// Set secrets via CLI (encrypted in state)
// pulumi config set --secret dbPassword "super-secret"

// Use OIDC for CI/CD instead of long-lived credentials
// Configure in Pulumi Cloud: ESC environments with OIDC

// Apply least-privilege IAM policies
const appRole = new aws.iam.Role("app-role", {
    assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Effect: "Allow",
            Principal: { Service: "lambda.amazonaws.com" },
            Action: "sts:AssumeRole",
        }],
    }),
    // Use specific actions, never use "*"
});

Performance Optimization

TechniqueImpactHow To
Micro-StacksFaster deploys, smaller blast radiusSplit by service or team ownership
Parallel OperationsFaster resource creationPulumi auto-parallelizes independent resources
Targeted UpdatesDeploy only changed resourcespulumi up --target urn:...
Refresh SparinglyAvoid slow API callsOnly refresh when drift is suspected
Provider VersionsPredictable buildsPin exact versions in package.json

Team Collaboration

Team Workflow Recommendations

  • Code Review: Treat infrastructure PRs like application PRs. Review the preview output alongside code changes.
  • Stack Ownership: Assign clear stack ownership. Use Pulumi teams and RBAC to control who can deploy where.
  • Naming Conventions: Use consistent naming: org/project/stack format (e.g., acme/networking/prod).
  • Documentation: Document stack outputs and their consumers. Use stack tags for metadata.
  • Drift Detection: Schedule periodic pulumi refresh to catch manual changes made outside Pulumi.
  • Rollback Plan: Always have a rollback strategy. Use pulumi stack export before risky changes.

Quick Reference

Tagging Strategy

tagging.ts
// Apply consistent tags using transformations
pulumi.runtime.registerStackTransformation((args) => {
    if (args.props.tags !== undefined) {
        args.props.tags = {
            ...args.props.tags,
            "pulumi:project": pulumi.getProject(),
            "pulumi:stack": pulumi.getStack(),
            "ManagedBy": "Pulumi",
            "Environment": pulumi.getStack(),
        };
    }
    return { props: args.props, opts: args.opts };
});