Ops Notes

Terraform to Pulumi Migration Guide: Why I Dumped HCL for Python

Cloud & DevOps Visualization

The Short Version: Who’s This For?

Last year our team inherited a mess—over 300 Terraform modules, HCL written like ancient runes, variable dependencies that’d make your head spin. Every new environment meant editing a dozen .tfvars files. And honestly, Terraform’s count and for_each logic? Painful after a while.

Pulumi isn’t a silver bullet. But if your team has Python or TypeScript engineers, the productivity boost is real. I personally led our migration from Terraform to Pulumi, hit every landmine along the way, and I’m sharing the raw experience here.

Core Differences: The Cheat Sheet

DimensionTerraformPulumi
LanguageHCL (DSL)Python/TypeScript/Go/C#/Java
State ManagementLocal/remote state filesCloud backend (Pulumi Cloud/self-hosted)
Provider Ecosystem2000+ official + community1800+ (includes Terraform Provider bridge)
Loops/Conditionalscount, for_each, templatesNative language for/if/functions
Debuggingterraform console + logsIDE breakpoints + print
Learning CurveModerate (HCL syntax)Low (general-purpose languages)
TestingTerratest (third-party)Native test framework + sandbox
Enterprise SupportHashiCorp (commercial)Pulumi (commercial)
LicenseBSL (not fully open source)Apache 2.0

Migration Strategies: We Tried Both

Works for small projects or well-modularized codebases. Our pilot was the CI/CD pipeline—just 10 resources.

Terraform Code (HCL):

resource "aws_s3_bucket" "data" {
  bucket = "my-app-data-${var.env}"
  tags = {
    Environment = var.env
    ManagedBy   = "Terraform"
  }
}

resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id
  versioning_configuration {
    status = "Enabled"
  }
}

Pulumi Code (Python):

import pulumi
from pulumi_aws import s3

env = pulumi.get_stack()

bucket = s3.Bucket("data",
    bucket=f"my-app-data-{env}",
    tags={
        "Environment": env,
        "ManagedBy": "Pulumi",
    })

s3.BucketVersioning("data-versioning",
    bucket=bucket.id,
    versioning_configuration=s3.BucketVersioningVersioningConfigurationArgs(
        status="Enabled",
    ))

The difference is immediate—Python if/else, for loops, no learning HCL’s clunky conditional syntax.

Approach Two: State Migration (pulumi-terraform-migrate)

We tested this tool. It works, but has limits.

# Export Terraform state
terraform state pull > terraform.tfstate

# Use Pulumi migration tool
pulumi terraform-migrate \
  --state-file terraform.tfstate \
  --stack-name production

# Verify migrated state
pulumi stack export

Gotchas:

  • Only supports flat state—nested modules will break
  • Resource alias mapping isn’t 100% accurate, expect manual fixes
  • Our 50-resource production migration left 3 resources needing manual import

The “Aha” Moments: What Made Pulumi Worth It

1. Dynamic Config Without HCL Templates

Generating a batch of similar resources in Terraform means wrestling with loops. Pulumi? Just Python:

def create_ec2_instances(env, count):
    instances = []
    for i in range(count):
        name = f"web-{env}-{i:03d}"
        instances.append(aws.ec2.Instance(name,
            ami="ami-0c55b159cbfafe1f0",
            instance_type="t3.micro",
            tags={"Name": name, "Env": env}))
    return instances

# 10 for prod, 2 for test
instances = create_ec2_instances("prod", 10) if env == "prod" else create_ec2_instances("test", 2)

2. Debugging That Doesn’t Suck

Terraform debugging means terraform console and TF_LOG=debug logs. Pulumi lets you set breakpoints in your IDE, inspect variables in real time. We had an IAM policy issue that took 10 minutes to find in Pulumi versus 30+ in Terraform.

3. Native Testing Support

import pulumi
from pulumi_aws import ec2

def test_vpc_created():
    # Create resources without deploying
    vpc = ec2.Vpc("test-vpc", cidr_block="10.0.0.0/16")
    # Assert
    assert vpc.cidr_block == "10.0.0.0/16"

With Terraform, you need Terratest and Go knowledge. Higher barrier to entry.

The Wreckage: Migration Nightmares

1. Provider Version Mismatch

Pulumi’s AWS Provider version numbers don’t align with Terraform’s. We found aws_s3_bucket_public_access_block had slightly different parameter names in Pulumi. Two hours of head-scratching.

Fix: Test in a small environment first, validate resources one by one. Don’t go all-in at once.

2. State Lock Conflicts

Pulumi defaults to Pulumi Cloud for state. When multiple team members operate simultaneously, the lock mechanism isn’t as mature as Terraform’s DynamoDB locking. We hit two lock release failures.

Fix: Self-host the Pulumi backend (S3 + DynamoDB), similar to Terraform setup.

3. Community Provider Quality Variance

Terraform’s community providers go through HashiCorp review. Pulumi’s bridged providers? Mixed bag. We found a bug in pulumi-kubernetes that broke Ingress configs.

Fix: Stick with official providers when possible. Run community providers in test for a week before production.

FAQ: The Questions You’re Actually Asking

Q: Is the migration expensive? Does the whole team need to learn new stuff? A: If your team knows Python or TypeScript, the learning curve is shallow. Our 10-person team was productive in 2 weeks. But the code rewrite itself—300 modules took us 3 weeks.

Q: Does Pulumi have fewer providers than Terraform? A: Numbers-wise, Terraform has 2000+ providers, Pulumi has 1800+. But Pulumi has a Terraform bridge that theoretically works with most Terraform providers. Reality check: bridge stability isn’t as good as native.

Q: Is production migration risky? A: Yes. We recommend phased migration—start with non-critical resources (dev VPCs, test databases), stabilize, then move production. The state migration tool isn’t foolproof. Always verify manually.

Q: Is Pulumi faster than Terraform? A: Deployment speed is similar. Development speed? Pulumi wins by 30-40% for the same module. Debugging speed is even more noticeable.

Q: What about pricing? A: Open-source version is free. Commercial version is per-user pricing. Terraform Cloud is similar. Small teams can run on open-source + self-hosted state backend, which is what we do.

Final Take

If your whole team is HCL veterans and your project is small and simple? Don’t bother migrating. But if your team knows general-purpose languages, or your project is getting complex (nested conditionals, deep loops), Pulumi is worth the switch.

My call: New projects go Pulumi. Old projects migrate gradually. Don’t try to eat the whole elephant in one bite. The teams that start first get the efficiency gains first.