IaC Ecosystem

Pulumi: IaC with Real Languages

● Intermediate ⏱ 25 min read iac

Terraform defines infrastructure in HCL — a purpose-built configuration language. Pulumi takes a different approach: write infrastructure in TypeScript, Python, Go, Java, or C# using the full power of a general-purpose programming language. The same loops, conditionals, functions, classes, and package managers you use for application code apply directly to infrastructure.

What Is Pulumi?

Pulumi is an infrastructure-as-code framework with a runtime engine and a CLI. When you run pulumi up, Pulumi:

  1. Executes your program (TypeScript/Python/Go) to build an in-memory resource graph
  2. Compares the graph against the last deployed state
  3. Calls cloud provider APIs to converge real infrastructure to the desired state

The cloud-side mechanics are similar to Terraform — desired state, state file, provider plugins — but the authoring experience is a programming language rather than a DSL.

Pulumi vs Terraform

Terraform / OpenTofuPulumi
LanguageHCL (domain-specific)TypeScript, Python, Go, C#, Java, YAML
Logic (loops, conditions)count, for_each, ternaryNative language constructs
AbstractionModulesClasses, functions, npm/pip packages
Testingterraform test frameworkStandard test frameworks (Jest, pytest)
Provider ecosystemregistry.terraform.io (large)pulumi.com/registry (large, wraps Terraform providers)
StateJSON fileJSON file (compatible backends)
Managed cloudHCP TerraformPulumi Cloud
Learning curveLearn HCLKnow a supported language already

Neither is universally better. Terraform's HCL is explicit and readable for infrastructure reviewers who aren't developers. Pulumi's general-purpose languages enable complex abstractions and integrate naturally into software engineering workflows.

Setup & First Program

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

# Create a new TypeScript project
mkdir my-infra && cd my-infra
pulumi new aws-typescript

# This creates:
# index.ts          — your infrastructure program
# package.json      — Node.js dependencies
# Pulumi.yaml       — project config
# Pulumi.dev.yaml   — stack config for 'dev'

Deploy:

pulumi up       # preview + deploy
pulumi preview  # preview only (like terraform plan)
pulumi destroy  # tear down all resources

Core Concepts

Resources

Each cloud resource is an object. Construct it to declare it:

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

const bucket = new aws.s3.BucketV2("my-bucket", {
    tags: { Environment: "production" },
});

Outputs

Resource properties that aren't known until after deployment are wrapped in Output<T> — Pulumi's equivalent of Terraform's unknown values. Use .apply() to transform them:

const bucketName = bucket.bucket;        // Output
const url = bucket.bucket.apply(name =>
    `https://${name}.s3.amazonaws.com`
);

export const bucketUrl = url;            // export as stack output

Stack References

Read outputs from another stack (equivalent to terraform_remote_state):

const networkStack = new pulumi.StackReference("myorg/networking/prod");
const vpcId = networkStack.getOutput("vpcId");

TypeScript Example

A complete program that creates an S3 bucket with versioning, a CloudFront distribution, and exports the distribution URL:

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

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

// S3 bucket
const bucket = new aws.s3.BucketV2("site-bucket", {
    tags: {
        Environment: environment,
        ManagedBy: "pulumi",
    },
});

// Enable versioning
new aws.s3.BucketVersioningV2("bucket-versioning", {
    bucket: bucket.bucket,
    versioningConfiguration: { status: "Enabled" },
});

// CloudFront distribution in front of S3
const distribution = new aws.cloudfront.Distribution("cdn", {
    origins: [{
        domainName: bucket.bucketRegionalDomainName,
        originId: bucket.bucket,
        s3OriginConfig: {
            originAccessIdentity: "",
        },
    }],
    enabled: true,
    defaultCacheBehavior: {
        targetOriginId: bucket.bucket,
        viewerProtocolPolicy: "redirect-to-https",
        allowedMethods: ["GET", "HEAD"],
        cachedMethods: ["GET", "HEAD"],
        forwardedValues: {
            queryString: false,
            cookies: { forward: "none" },
        },
    },
    restrictions: {
        geoRestriction: { restrictionType: "none" },
    },
    viewerCertificate: {
        cloudfrontDefaultCertificate: true,
    },
});

export const distributionUrl = pulumi.interpolate`https://${distribution.domainName}`;
export const bucketName = bucket.bucket;

This uses real TypeScript: pulumi.interpolate (a tagged template literal that handles Output<T> values), type checking, and IDE autocomplete — none of which exist in HCL.

Python Example

import pulumi
import pulumi_aws as aws

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,
    tags={"Environment": environment, "ManagedBy": "pulumi"},
)

# Create subnets in multiple AZs using a loop
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
subnets = []
for i, az in enumerate(azs):
    subnet = aws.ec2.Subnet(f"public-subnet-{i}",
        vpc_id=vpc.id,
        cidr_block=f"10.0.{i}.0/24",
        availability_zone=az,
        map_public_ip_on_launch=True,
        tags={"Name": f"public-{az}"},
    )
    subnets.append(subnet)

pulumi.export("vpc_id", vpc.id)
pulumi.export("subnet_ids", [s.id for s in subnets])

The Python loop over AZs is idiomatic Python — no equivalent of for_each or count needed. The type system and pip ecosystem are available for validation, testing, and sharing abstractions.

State & Backends

Pulumi state works like Terraform state — a JSON file mapping your program's resources to real cloud objects. Backends:

BackendUse case
Pulumi Cloud (default)Managed, free tier, concurrency control, secrets encryption
S3 / GCS / Azure BlobSelf-managed, no Pulumi account required
Local filesystemSingle-developer, no sharing

Configure a self-managed S3 backend:

pulumi login s3://my-pulumi-state-bucket

Pulumi Cloud has a free tier covering unlimited stacks for individuals, with team features (RBAC, audit logs, secrets) on paid plans.

Stacks & Config

A stack is an isolated deployment of a Pulumi program — equivalent to a Terraform workspace. Each stack has its own state and config:

# Create stacks for each environment
pulumi stack init dev
pulumi stack init staging
pulumi stack init prod

# Set per-stack config
pulumi config set environment dev   --stack dev
pulumi config set environment prod  --stack prod

# Set secrets (encrypted in state)
pulumi config set --secret db_password "s3cr3t"  --stack prod

Stack config is read in your program with pulumi.Config():

const config = new pulumi.Config();
const dbPassword = config.requireSecret("db_password");  // Output, value encrypted
const environment = config.require("environment");        // string, plaintext

Config values are stored in Pulumi.<stackname>.yaml files which are safe to commit — secrets are encrypted before being stored.

Key Takeaways