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.
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
| Aspect | Pulumi | Terraform |
|---|---|---|
| Language | Real programming languages | HCL (domain-specific) |
| Testing | Unit tests with standard frameworks | Limited to plan validation |
| Abstraction | Functions, classes, loops natively | Modules with limited logic |
| IDE support | Full autocomplete, types, refactoring | Limited LSP support |
| Ecosystem | Growing but smaller | Massive provider library |
| Learning curve | Steeper for non-programmers | Gentler for ops engineers |
| State management | Managed service or self-managed | Self-managed backends |
| Community | Smaller | Large, established |
Production Failure Scenarios
Common Pulumi Failures
| Failure | Impact | Mitigation |
|---|---|---|
| Program error crashes apply | Infrastructure left in unknown state | Always use Pulumi escape hatch for complex resources |
| Stack reference pointing to deleted stack | Deployment fails across dependent stacks | Validate stack existence before referencing |
| Unintended resource deletion | Pulumi destroys resources outside its scope | Review preview carefully, use targeted pulumi up |
| Language runtime bugs | Unexpected behavior in resource provisioning | Pin language runtime versions in CI |
| State desynchronization | Pulumi loses track of resources | Use 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
| Aspect | Pulumi | Terraform | Ansible |
|---|---|---|---|
| Paradigm | Imperative (how) | Declarative (what) | Procedural (steps) |
| Language | TypeScript, Python, Go, C# | HCL | YAML |
| State management | Managed or self-hosted | Managed or self-hosted | Stateless (by default) |
| Plan/apply | Yes | Yes | No (push-based) |
| Testing | Native unit tests | Terratest | Ansible tests |
| Cloud providers | All major | All major | All major |
| Learning curve | Steeper (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.
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.
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.