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: 17 min read author: GeekWorkBench

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.

Introduction

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

Trade-off Analysis

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.

Interview Questions

1. What programming languages does Pulumi support?

Expected answer points:

  • TypeScript, Python, Go, C#, and Java
  • All languages have full access to Pulumi SDK and providers
  • Language choice affects team productivity and existing skill sets
2. How does Pulumi's imperative model differ from Terraform's declarative approach?

Expected answer points:

  • Terraform: declarative - you describe the desired state, Terraform figures out how to get there
  • Pulumi: imperative - you describe the steps to create resources
  • Pulumi program executes sequentially; Terraform evaluates entire graph
  • Imperative model enables loops, conditionals, and full language expressiveness
3. What is a Pulumi Stack and how does it compare to Terraform workspaces?

Expected answer points:

  • Stack represents a deployment target (dev, staging, production)
  • Each stack has its own state file and configuration
  • Similar to Terraform workspaces in purpose
  • Stacks can reference each other via StackReference for cross-stack dependencies
4. How does Pulumi handle state management?

Expected answer points:

  • Pulumi Service manages state automatically (SaaS)
  • Self-managed backends: local filesystem, AWS S3 with DynamoDB, Kubernetes secrets
  • State file per stack with locking handled by the backend
  • Use pulumi export to audit state, pulumi import to recover
5. How do you create multiple resources with loops in Pulumi?

Expected answer points:

  • Use standard language loops (for, forEach, etc.)
  • Example: environments.forEach((env) => { new aws.ec2.Instance(...); })
  • Loops work with any language construct: for, while, map, filter
  • No special HCL syntax needed
6. What is the advantage of Pulumi's testing capabilities over Terraform?

Expected answer points:

  • Pulumi uses standard unit testing frameworks (Jest, pytest, Go test)
  • Native unit tests validate logic before deployment
  • Terraform limited to plan validation and Terratest (external)
  • Pulumi policy tests enforce compliance before apply
  • Type checking catches errors during development
7. How do stack references work in Pulumi?

Expected answer points:

  • StackReference imports outputs from another stack
  • Syntax: new pulumi.StackReference("org/project/stackname")
  • Use getOutput("outputName") to access outputs from referenced stack
  • Enables modular infrastructure across teams without coupling
8. What is the Pulumi escape hatch?

Expected answer points:

  • Raw provider resources bypass Pulumi's typed interface
  • Used when a resource type has no Pulumi SDK wrapper
  • Use sparingly - bypasses Pulumi's safety checks
  • Document escape hatch usage clearly for maintainability
9. How do you implement conditional resource creation in Pulumi?

Expected answer points:

  • Use standard if/else statements
  • Example: if (environment === "production") { new aws.rds.Instance(...); }
  • Conditional logic executes at program runtime
  • No special configuration syntax needed
10. What is Pulumi CrossGuard or Policy as Code?

Expected answer points:

  • Pulumi policy as code enforces compliance rules before deployment
  • Policy packs written in the same language as infrastructure
  • Run pulumi preview --policy-pack to check policies before apply
  • Skip at your own risk - misconfigured resources reach production
  • Integrate into CI pipeline for automated enforcement
11. How does Pulumi compare to Terraform on ecosystem size?

Expected answer points:

  • Terraform ecosystem is larger (more providers, modules, community examples)
  • Pulumi growing but smaller - check provider coverage before committing
  • Major cloud providers (AWS, Azure, GCP) have first-class Pulumi support
  • Community contributions increasing but behind Terraform
12. How do you handle secrets in Pulumi configuration?

Expected answer points:

  • Pulumi configs can store secrets, but encryption at rest only in Pulumi Cloud
  • Use the secret() function when storing sensitive config values
  • Do not put database passwords or API keys in plain config files
  • Ensure state backend is secure (S3 with encryption, access controls)
  • Consider Pulumi ESC for secrets management across environments
13. What is the Pulumi main function and how does it work?

Expected answer points:

  • Entry point exports a function called main that receives a ctx context object
  • main returns an array of resources to create
  • Pulumi engine compares current state with desired state and determines API calls
  • Changes only happen when actual state differs from defined resources
14. How do you abstract common infrastructure patterns into reusable components in Pulumi?

Expected answer points:

  • Define custom components encapsulating multiple resources
  • Example: Network component creates VPC, subnets, NAT gateways, route tables
  • Components expose only the configuration they need
  • Functions can also abstract patterns (createAppServer, createDatabase)
  • Avoid over-abstracting - three-level inheritance chains are hard to debug
15. What happens when a Pulumi program error crashes apply?

Expected answer points:

  • Infrastructure left in unknown state - partial resources may exist
  • Pulumi may not know which resources were created before crash
  • Always use Pulumi escape hatch for complex resources with known failure modes
  • Run pulumi refresh to reconcile state with actual infrastructure
  • Maintain state backups: pulumi export > state-backup.json
16. How does Pulumi handle resource updates in place vs recreation?

Expected answer points:

  • Pulumi engine determines if update is possible in place or requires recreation
  • Changing instance type updates in place for EC2
  • Changing VPC CIDR block requires recreation (not possible)
  • pulumi preview shows whether update or recreation will happen
17. What are the self-managed backend options for Pulumi?

Expected answer points:

  • Local backend: state in filesystem - good for testing only
  • AWS S3 backend with DynamoDB for locking - enterprise option
  • Kubernetes backend: state stored in Kubernetes secrets
  • Use self-hosted when SaaS is not allowed (air-gapped, compliance)
18. How do you recover from unintended resource deletion in Pulumi?

Expected answer points:

  • Pulumi destroys resources outside its scope if incorrectly targeted
  • Review preview carefully before confirming pulumi up
  • Use targeted pulumi up with specific resource selection
  • If deletion happened: restore from state backup, re-apply
  • Use protection mechanisms for critical resources
19. Why is skipping pulumi preview dangerous?

Expected answer points:

  • Preview shows exactly what will change before applying
  • Without preview, you approve changes without knowing what they are
  • Unintended resource deletion can happen if you misconfigure targets
  • Always run preview even in CI - review is low cost vs recovery
20. When should you prefer Terraform over Pulumi?

Expected answer points:

  • Team has no programming background - HCL is simpler to learn
  • Need a specific Terraform provider with no Pulumi equivalent
  • Organization has existing Terraform modules and runbooks - migration cost too high
  • Ops-focused engineers without application development experience
  • Terraform's gentler learning curve for infrastructure-focused teams

Further Reading

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