Pulumi: Infrastructure as Actual Code

Use Pulumi to define infrastructure using real programming languages—TypeScript, Python, Go, C#—enabling loops, conditionals, and full IDE support for IaC.

published: reading time: 12 min read

Pulumi: Infrastructure as Actual Code

Pulumi takes a fundamentally different approach to infrastructure as code. Instead of learning a domain-specific language like HCL, you write regular programs in TypeScript, Python, Go, C#, or Java. Your infrastructure becomes a real application with all the expressiveness that implies: loops, conditionals, functions, classes, and the full debugging toolkit your language provides.

DSLs like HCL are limited to what their designers anticipated. When you hit a use case the DSL does not handle well, you end up working around it with external scripts or code generation. With Pulumi, you use the same language you use everywhere else, and you can express any logic the language supports.

Pulumi vs Terraform Comparison

The Terraform ecosystem is larger and older. You will find more providers, more modules, and more community examples for Terraform than for Pulumi. Terraform uses a declarative model where you describe the desired state. Pulumi uses an imperative model where you describe the steps to create resources.

Terraform HCL is purpose-built for configuration and easier to learn for beginners who have not programmed before. Pulumi requires knowing how to program, but rewards that investment with far greater flexibility.

Pulumi stores state on your behalf in the Pulumi Cloud, similar to how Terraform Cloud works. You can also use self-managed backends like Terraform, or run Pulumi in a fully disconnected offline mode with the open-source engine.

For teams with strong software engineering backgrounds, Pulumi often feels more natural. You get autocomplete, type checking, unit tests, and refactoring tools that do not exist in the Terraform world. The tradeoff is a steeper learning curve if your team is not familiar with the chosen programming language.

Setting Up Pulumi Projects

Installing Pulumi takes a single command, and then you create a new project with pulumi new. The CLI walks you through choosing a language, template, and cloud provider.

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

# Create a new TypeScript project
pulumi new aws-typescript --dir ./my-infra
cd my-infra

# Install dependencies
npm install

# Preview the stack
pulumi preview

Each project contains a Pulumi.yaml file that describes the project settings, and a Stack that represents a deployment target. Stacks are like Terraform workspaces—dev, staging, production—each with its own state and configuration.

The entry point to your infrastructure is a standard program file. In TypeScript, that is index.ts. You export a function called main that receives a ctx context object and returns an array of resources to create.

Defining Resources in TypeScript

Resources in Pulumi look like class constructors. The AWS provider maps each resource type to a corresponding Pulumi class.

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

const config = new pulumi.Config();
const environment = config.require("environment");

// Create an S3 bucket
const bucket = new aws.s3.Bucket("app-bucket", {
  bucketPrefix: `app-${environment}-`,
  tags: {
    Environment: environment,
    ManagedBy: "Pulumi",
  },
});

// Create IAM role for EC2
const ec2Role = new aws.iam.Role("ec2-role", {
  name: `ec2-app-role-${environment}`,
  assumeRolePolicy: JSON.stringify({
    Version: "2012-10-17",
    Statement: [
      {
        Action: "sts:AssumeRole",
        Effect: "Allow",
        Principal: { Service: "ec2.amazonaws.com" },
      },
    ],
  }),
});

// Create an EC2 instance
const server = new aws.ec2.Instance("web-server", {
  instanceType: environment === "production" ? "t3.large" : "t3.micro",
  ami: "ami-0c55b159cbfafe1f0", // Amazon Linux 2 LTS
  tags: {
    Name: `web-server-${environment}`,
  },
});

// Export values
export const bucketName = bucket.id;
export const instanceId = server.id;
export const instanceIp = server.publicIp;

The new aws.ec2.Instance call creates an EC2 instance. Pulumi compares the current state with the desired state and determines which API calls to make. If you change the instance type and run pulumi up, Pulumi updates the instance in place rather than recreating it.

State Management with Pulumi Backend

Pulumi manages state automatically in the Pulumi Service. Each stack gets its own state file, and the service handles locking, history, and access control. This makes collaboration straightforward—everyone uses the same CLI, and the service coordinates who can make changes when.

For organizations that cannot use a SaaS service, Pulumi supports three self-managed backend options. You can use a local backend that stores state in the filesystem, an AWS S3 backend with DynamoDB for locking, or a Kubernetes backend that stores state in Kubernetes secrets.

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

const config = new pulumi.Config();

// Use stack references to access outputs from other stacks
const otherStack = new pulumi.StackReference("acme/webapp/production");

// Reference outputs from the other stack
const vpcId = otherStack.getOutput("vpcId");

Stack references let you compose infrastructure across teams and stacks. One stack can export values that another stack imports, enabling modular infrastructure at scale.

Using Programming Constructs for DRY Code

This is where Pulumi shines. When your infrastructure is code, you can use loops, conditionals, and functions to eliminate repetition.

// Create multiple resources with a loop
const environments = ["dev", "staging", "production"];

environments.forEach((env) => {
  new aws.ec2.Instance(`server-${env}`, {
    instanceType: env === "production" ? "t3.large" : "t3.micro",
    ami: "ami-0c55b159cbfafe1f0",
    tags: { Name: `server-${env}` },
  });
});

// Create resources conditionally
if (environment === "production") {
  new aws.rds.Instance("database", {
    instanceClass: "db.r6g.large",
    allocatedStorage: 100,
    engine: "postgres",
    engineVersion: "15.3",
  });
}

// Abstract common patterns into functions
function createAppServer(name: string, env: string, scale: number) {
  const asg = new aws.autoscaling.Group(name, {
    minSize: scale,
    maxSize: scale * 3,
    instanceType: "t3.micro",
    tags: [{ key: "Name", value: `${name}-${env}`, propagateAtLaunch: true }],
  });
  return asg;
}

const apiServers = createAppServer("api", environment, 2);

You can also define custom components that encapsulate multiple resources into a single reusable abstraction. A Network component might create a VPC, subnets, NAT gateways, and route tables, exposing only the configuration it really needs.

Testing Infrastructure Code

Since Pulumi uses real programming languages, you can use standard testing frameworks to validate your infrastructure.

// Unit test with Jest
import { describe, it, expect } from "@jest/globals";
import * as aws from "@pulumi/aws";

describe("EC2 Instance", () => {
  it("should use t3.micro for non-production", () => {
    // Programmatically verify instance type logic
    const env = "staging";
    const expectedType = env === "production" ? "t3.large" : "t3.micro";
    expect(expectedType).toBe("t3.micro");
  });
});

Pulumi also provides a testing library specifically for infrastructure. You can create resource tests that verify the actual resources created match expectations, or policy tests that enforce compliance rules before deployment.

The ability to write tests for infrastructure catches bugs before they reach production. Type errors, logic errors, and missing dependencies surface during development rather than during a failed deployment.

When to Use / When Not to Use

When Pulumi makes sense

Pulumi is the right choice when your team writes application code in TypeScript, Python, Go, or C#. If your engineers already live in a programming language, forcing them to learn HCL just to manage infrastructure creates an unnecessary barrier.

Use Pulumi when your infrastructure logic is complex. If you need loops over dynamically sized arrays, conditional resource creation based on environment, or reusable functions that abstract patterns, HCL hits its limits fast. Pulumi does not constrain what you can express.

Testing infrastructure is another strong argument for Pulumi. Real unit tests with Jest or pytest catch bugs before they reach production. You cannot do that with Terraform.

When to use Terraform instead

If your team has no programming background, HCL is simpler to learn than a general-purpose language. Terraform has a gentler learning curve for ops-focused engineers who have not written application code.

If you need a specific Terraform provider that has no Pulumi equivalent, Terraform wins. The ecosystem gap matters. Check provider coverage before committing to Pulumi.

If your organization has existing Terraform modules and runbooks, the migration cost to Pulumi may not be worth it. Pulumi is better for new projects where you can design from scratch.

Pulumi Architecture Flow

flowchart TD
    A[Pulumi Program .ts/.py/.go] --> B[pulumi up]
    B --> C[Pulumi Engine]
    C --> D[Compare Desired vs Actual]
    D --> E[Generate Resource Graph]
    E --> F[Execute in Order]
    F --> G[Cloud Provider APIs]
    G --> H[Real Infrastructure]
    H --> I[State Updated]
    I --> D

Pulumi vs Terraform Trade-off

AspectPulumiTerraform
LanguageReal programming languagesHCL (domain-specific)
TestingUnit tests with standard frameworksLimited to plan validation
AbstractionFunctions, classes, loops nativelyModules with limited logic
IDE supportFull autocomplete, types, refactoringLimited LSP support
EcosystemGrowing but smallerMassive provider library
Learning curveSteeper for non-programmersGentler for ops engineers
State managementManaged service or self-managedSelf-managed backends
CommunitySmallerLarge, established

Production Failure Scenarios

Common Pulumi Failures

FailureImpactMitigation
Program error crashes applyInfrastructure left in unknown stateAlways use Pulumi escape hatch for complex resources
Stack reference pointing to deleted stackDeployment fails across dependent stacksValidate stack existence before referencing
Unintended resource deletionPulumi destroys resources outside its scopeReview preview carefully, use targeted pulumi up
Language runtime bugsUnexpected behavior in resource provisioningPin language runtime versions in CI
State desynchronizationPulumi loses track of resourcesUse pulumi refresh, maintain state backups

Policy Enforcement Flow

flowchart TD
    A[pulumi up] --> B[Preview Changes]
    B --> C[Pulumi Policy Engine]
    C --> D{All policies pass?}
    D -->|Yes| E[Apply Changes]
    D -->|No| F[Show Policy Violations]
    F --> G[Fix Code or Override]
    G --> B
    E --> H[Update State]

Observability Hooks

Track Pulumi operations to maintain operational awareness.

What to monitor:

  • Deployment frequency and duration per stack
  • Policy violation rate (catch non-compliant resources early)
  • Stack reference chain health
  • State file size and resource count per stack
  • Failed deployments and error types
# View deployment history
pulumi stack history

# Check resource count
pulumi stack --show-urns | wc -l

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

# Validate stack
pulumi validate

# Preview with policy
pulumi preview --policy-pack policies/

Common Pitfalls / Anti-Patterns

Over-abstracting infrastructure

Pulumi’s expressiveness tempts you into building elaborate class hierarchies for infrastructure. A three-level inheritance chain for a VPC is harder to debug than three simple resource declarations. Start concrete, abstract only when repetition demands it.

Ignoring escape hatches

Pullying every resource through Pulumi’s typed interface is clean but sometimes impossible. Using raw provider resources via the escape hatch bypasses Pulumi’s safety checks. Use it sparingly and document it clearly.

Not using policy-as-code

Pulumi CrossGuard or the Policy as Code feature catches problems before they deploy. Skipping policy enforcement means misconfigured resources reach production. Integrate policies into your CI pipeline.

Storing secrets in plain text

Pulumi configs can store secrets, but they are encrypted at rest only in Pulumi Cloud. Do not put database passwords or API keys in plain config files. Use the secret() function and ensure your state backend is secure.

Skipping pulumi preview

Always run preview before apply, even in CI. The cost of reviewing a preview is far lower than recovering from an unintended resource deletion.

Quick Recap

Key Takeaways

  • Pulumi uses real programming languages for infrastructure, enabling tests, abstractions, and IDE support
  • Stacks work like Terraform workspaces for managing dev, staging, and production
  • Stack references let teams compose infrastructure across stacks without coupling
  • Pulumi’s policy-as-code catches problems before they reach production
  • The tradeoff: requires programming knowledge, smaller ecosystem than Terraform

Pulumi Health Checklist

# Preview changes before applying
pulumi preview

# Check stack outputs
pulumi stack output

# Validate resources
pulumi validate

# Run policy checks
pulumi preview --policy-pack ./policies/

# List resources in stack
pulumi stack --show-urns

# Audit state
pulumi export > state-backup.json

# Test infrastructure code
npm test  # or pytest, go test, etc.

Trade-off Summary

AspectPulumiTerraformAnsible
ParadigmImperative (how)Declarative (what)Procedural (steps)
LanguageTypeScript, Python, Go, C#HCLYAML
State managementManaged or self-hostedManaged or self-hostedStateless (by default)
Plan/applyYesYesNo (push-based)
TestingNative unit testsTerratestAnsible tests
Cloud providersAll majorAll majorAll major
Learning curveSteeper (programming)Moderate (HCL)Low (YAML)

Conclusion

Pulumi makes infrastructure accessible to software engineers who already know how to program. The ability to write loops, functions, and classes to manage infrastructure means you can build abstractions that would be impossible in a DSL. Full IDE support, type checking, and unit tests bring software engineering practices to infrastructure management.

For securing your infrastructure, see Cloud Security for IAM policies, encryption, and access controls. For monitoring infrastructure changes and drift detection, see Observability Engineering.

The tradeoff is that Pulumi requires programming knowledge. If your team has never written code, Terraform HCL might be faster to learn. But for teams already writing TypeScript or Python, Pulumi feels like a natural extension of their existing skills.

For more on infrastructure approaches, see our post on Cost Optimization and other IaC topics in this roadmap.

Category

Related Posts

Terraform: Declarative Infrastructure Provisioning

Learn Terraform from the ground up—state management, providers, modules, and production-ready patterns for managing cloud infrastructure as code.

#terraform #iac #devops

AWS CDK: Cloud Development Kit for Infrastructure

Define AWS infrastructure using TypeScript, Python, or other programming languages with the AWS Cloud Development Kit, compiling to CloudFormation templates.

#aws #cdk #iac

IaC Module Design: Reusable and Composable Infrastructure

Design Terraform modules that are reusable, composable, and maintainable—versioning, documentation, and publish patterns for infrastructure building blocks.

#terraform #iac #modules