Understanding the Terraform-to-Pulumi Migration
Migrating from Terraform to Pulumi means transitioning your Infrastructure as Code (IaC) from HashiCorp's HCL-based ecosystem to Pulumi's general-purpose programming language model. Instead of writing declarative configuration in HCL and relying on a separate state file, you write infrastructure definitions in TypeScript, Python, Go, C#, or Java, using real programming constructs like loops, functions, and classes. Pulumi then interprets these programs and manages your cloud resources through its own state management engine.
Pulumi provides a unified approach where infrastructure, application code, and configuration live together in a single codebase. It supports all major cloud providers, including AWS, Azure, GCP, and Kubernetes, while also offering a bridge to reuse existing Terraform providers directly. This tutorial walks you through the complete migration process, from assessing your current Terraform setup to running a fully converted Pulumi project.
Why Migrate from Terraform to Pulumi
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →The decision to migrate often stems from specific pain points with Terraform that Pulumi directly addresses. Understanding these motivations helps frame the migration strategy.
First-Class Programming Languages
Terraform's HCL is purpose-built and powerful, but it lacks the expressiveness of a general-purpose language. Complex logic, conditionals, and abstractions often require workarounds with modules, count expressions, and locals. Pulumi lets you use TypeScript, Python, Go, C#, or Java, giving you access to rich ecosystems, testing frameworks, package managers, and IDE tooling. You can share code between infrastructure and application projects seamlessly.
Reduced Drift and State Complexity
Pulumi stores state either in its managed service (Pulumi Cloud) or self-hosted backends like S3, but the engine's approach to state management reduces common drift issues. Because Pulumi programs are executed with a full programming runtime, refresh operations and state reconciliation become more predictable. The state file is never manually edited.
Multi-Cloud and Multi-Provider in One Stack
While Terraform can handle multi-cloud, Pulumi's ability to combine providers in a single function scope makes cross-cloud orchestration more natural. You can provision an AWS S3 bucket and an Azure Blob Storage container in the same deployment loop, sharing variables and logic directly.
Testing and Automation
Pulumi's language SDKs support unit tests, integration tests, and property-based testing using standard frameworks like Jest, pytest, or Go's testing package. Terraform's testing capabilities have improved but remain more limited in scope and require additional tooling.
Pre-Migration Assessment and Planning
Before writing any Pulumi code, conduct a thorough assessment of your Terraform codebase. This step prevents surprises during conversion and helps you scope the migration realistically.
Inventory Your Terraform Resources
Run terraform state list to enumerate all managed resources. Group them logically into stacks—Pulumi's equivalent of Terraform workspaces or root modules. Each Pulumi stack represents an independent deployment unit with its own state and configuration.
# List all resources in current Terraform state
terraform state list
# Example output:
# aws_instance.web_server
# aws_security_group.web_sg
# aws_elb.web_lb
# module.vpc.aws_vpc.main
# module.vpc.aws_subnet.public[0]
# module.vpc.aws_subnet.public[1]
Map Terraform Constructs to Pulumi Equivalents
Create a mapping document that translates Terraform primitives to Pulumi concepts:
- Terraform provider → Pulumi provider package (e.g.,
pulumi-aws) - Terraform resource → Pulumi resource object (e.g.,
aws.ec2.Instance) - Terraform module → Pulumi component resource or a function-based abstraction
- Terraform variable → Pulumi config value or typed input parameter
- Terraform output → Pulumi stack output or exported resource attribute
- Terraform data source → Pulumi getter function (e.g.,
aws.ec2.getAmi()) - Terraform remote state → Pulumi stack reference
- Terraform locals → Local variables or constants in your chosen language
Decide on a Migration Strategy
Choose one of three approaches based on your timeline and risk tolerance:
- Full rewrite: Convert everything at once, suitable for smaller codebases or greenfield components
- Incremental migration: Convert stack by stack, running Pulumi and Terraform side-by-side
- Resource import: Keep Terraform-managed resources live and import them into Pulumi using
pulumi import
Step 1: Install Pulumi and Set Up Your Environment
Begin by installing the Pulumi CLI and configuring your language runtime. The CLI manages stacks, deployments, and state. Choose the language that best fits your team's skills and the nature of your infrastructure.
# macOS (Homebrew)
brew install pulumi
# Linux (curl)
curl -fsSL https://get.pulumi.com | sh
# Windows (Chocolatey)
choco install pulumi
# Verify installation
pulumi version
Install the language runtime you plan to use. For this tutorial, we'll use TypeScript, but the concepts translate directly to Python, Go, C#, and Java.
# Ensure Node.js and npm are available
node --version # Requires Node.js 16+
npm --version
# Install Pulumi's TypeScript SDK (done per project, shown in next step)
# For Python users:
# python -m venv venv && source venv/bin/activate
Configure Pulumi Cloud access if you want managed state (recommended for teams). Alternatively, set up self-managed state on S3, Azure Blob, or GCS.
# Login to Pulumi Cloud (default)
pulumi login
# For self-managed S3 backend (example)
pulumi login s3://my-pulumi-state-bucket/migration-project
Step 2: Create a New Pulumi Project
Create a fresh Pulumi project that will eventually hold all converted resources. Initialize it in an empty directory with the cloud provider you're targeting. This example uses AWS, matching the earlier Terraform output.
# Create project directory
mkdir pulumi-migration && cd pulumi-migration
# Initialize a new TypeScript project for AWS
pulumi new aws-typescript
# Project name: migration-project
# Description: Migrated infrastructure from Terraform
# Stack name: dev
# AWS region: us-west-2
The command generates a boilerplate project with Pulumi.yaml, Pulumi.dev.yaml, package.json, and an index.ts entry point. The Pulumi.yaml file defines the project metadata and runtime:
name: migration-project
runtime: nodejs
description: Migrated infrastructure from Terraform
template:
config:
aws:region:
description: The AWS region to deploy into
default: us-west-2
Step 3: Convert Terraform Resources to Pulumi Code
Now comes the core conversion work. Start with a representative Terraform resource and rewrite it as a Pulumi resource in your chosen language. Below is a side-by-side comparison showing a typical Terraform configuration and its Pulumi equivalent.
Terraform Source (Example)
# terraform/main.tf
provider "aws" {
region = var.region
}
resource "aws_security_group" "web_sg" {
name = "web-server-sg"
description = "Allow HTTP and HTTPS traffic"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web_server" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.web_sg.id]
tags = {
Name = "web-server"
}
}
variable "instance_type" {
default = "t3.micro"
}
variable "region" {
default = "us-west-2"
}
output "instance_public_ip" {
value = aws_instance.web_server.public_ip
}
Pulumi Conversion (TypeScript)
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Configuration (replaces Terraform variables)
const awsConfig = new pulumi.Config("aws");
const appConfig = new pulumi.Config("app");
const region = awsConfig.require("region");
const instanceType = appConfig.get("instanceType") || "t3.micro";
// Data source: Get Ubuntu AMI (replaces data.aws_ami.ubuntu)
const ubuntuAmi = aws.ec2.getAmi({
mostRecent: true,
owners: ["099720109477"], // Canonical
filters: [
{ name: "name", values: ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] },
{ name: "virtualization-type", values: ["hvm"] },
],
});
// Security group (replaces aws_security_group.web_sg)
const webSecurityGroup = new aws.ec2.SecurityGroup("web-sg", {
name: "web-server-sg",
description: "Allow HTTP and HTTPS traffic",
ingress: [
{
fromPort: 80,
toPort: 80,
protocol: "tcp",
cidrBlocks: ["0.0.0.0/0"],
},
{
fromPort: 443,
toPort: 443,
protocol: "tcp",
cidrBlocks: ["0.0.0.0/0"],
},
],
egress: [{
fromPort: 0,
toPort: 0,
protocol: "-1",
cidrBlocks: ["0.0.0.0/0"],
}],
});
// EC2 instance (replaces aws_instance.web_server)
const webServer = new aws.ec2.Instance("web-server", {
ami: ubuntuAmi.then(ami => ami.id),
instanceType: instanceType,
vpcSecurityGroupIds: [webSecurityGroup.id],
tags: {
Name: "web-server",
},
});
// Outputs (replaces Terraform output blocks)
export const instancePublicIp = webServer.publicIp;
export const securityGroupId = webSecurityGroup.id;
Key Conversion Patterns
Several patterns emerge when translating Terraform HCL to Pulumi:
- Provider configuration moves to
pulumi.Configor provider constructors. AWS region typically comes fromaws:regionin stack config. - Resource references use real object attributes (
webSecurityGroup.id) instead of HCL interpolation (aws_security_group.web_sg.id). - Data sources become async getter functions. Use
.then()orpulumi.Output.apply()to unwrap values. - Variables become typed config values or constants. Multi-environment values live in stack config files (
Pulumi.dev.yaml). - Modules become either Component Resources (classes extending
pulumi.ComponentResource) or plain functions that return resource objects.
Step 4: Handle Terraform Modules as Pulumi Components
Terraform modules are one of the trickiest parts to migrate. A module encapsulates multiple resources and exposes inputs and outputs. In Pulumi, you achieve the same encapsulation using Component Resources—classes that group related resources and expose a clean interface.
Terraform VPC Module Usage
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "main-vpc"
cidr = "10.0.0.0/16"
azs = ["us-west-2a", "us-west-2b"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
}
Pulumi Component Resource Equivalent
// components/vpc.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
export interface VpcArgs {
name: string;
cidr: string;
availabilityZones: string[];
publicSubnetCidrs: string[];
}
export class Vpc extends pulumi.ComponentResource {
public readonly vpcId: pulumi.Output;
public readonly publicSubnetIds: pulumi.Output[];
constructor(name: string, args: VpcArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:networking:Vpc", name, {}, opts);
// Create the VPC
const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
cidrBlock: args.cidr,
tags: { Name: args.name },
}, { parent: this });
// Create public subnets
const publicSubnets = args.publicSubnetCidrs.map((cidr, index) => {
const az = args.availabilityZones[index % args.availabilityZones.length];
return new aws.ec2.Subnet(`${name}-public-${index}`, {
vpcId: vpc.id,
cidrBlock: cidr,
availabilityZone: az,
mapPublicIpOnLaunch: true,
tags: { Name: `${args.name}-public-${az}` },
}, { parent: this });
});
// Create Internet Gateway
const igw = new aws.ec2.InternetGateway(`${name}-igw`, {
vpcId: vpc.id,
}, { parent: this });
// Expose outputs
this.vpcId = vpc.id;
this.publicSubnetIds = publicSubnets.map(s => s.id);
this.registerOutputs({
vpcId: this.vpcId,
publicSubnetIds: this.publicSubnetIds,
});
}
}
// Usage in index.ts
const mainVpc = new Vpc("main-vpc", {
name: "main-vpc",
cidr: "10.0.0.0/16",
availabilityZones: ["us-west-2a", "us-west-2b"],
publicSubnetCidrs: ["10.0.1.0/24", "10.0.2.0/24"],
});
Component resources are the idiomatic Pulumi replacement for Terraform modules. They support inheritance, encapsulation, and lifecycle management identical to native resources. Every resource created inside a component should pass { parent: this } to maintain proper dependency tracking.
Step 5: Import Existing Terraform-Managed Resources
If you have live infrastructure managed by Terraform that you want to bring under Pulumi without destroying and recreating, use pulumi import. This command reads a resource's current state from the cloud provider and adopts it into a Pulumi stack.
Import Workflow
The import process requires three pieces of information: the Pulumi resource type, the resource's logical name in your Pulumi program, and the cloud provider's resource ID. You declare the resource in code first, then run the import command.
// Step 5a: Declare the resource in your Pulumi program
// Add this to index.ts (or keep it commented until import completes)
const importedSecurityGroup = new aws.ec2.SecurityGroup("web-sg", {
name: "web-server-sg",
description: "Allow HTTP and HTTPS traffic",
ingress: [
{
fromPort: 80,
toPort: 80,
protocol: "tcp",
cidrBlocks: ["0.0.0.0/0"],
},
],
egress: [{
fromPort: 0,
toPort: 0,
protocol: "-1",
cidrBlocks: ["0.0.0.0/0"],
}],
});
# Step 5b: Run the import command
# Syntax: pulumi import <resource-type> <logical-name> <cloud-resource-id>
pulumi import aws:ec2/securityGroup:SecurityGroup web-sg sg-0a7b8c9d4e5f67890
# For an EC2 instance:
pulumi import aws:ec2/instance:Instance web-server i-0123456789abcdef0
After import, Pulumi updates the stack state to reflect the imported resources. Your next pulumi up will show no changes if your code definitions match the live resource properties exactly. This approach allows zero-downtime migration: resources continue running while you incrementally convert the codebase.
Bulk Import with Automated Tooling
For large infrastructures, Pulumi offers pulumi converter (experimental) and the community tool terraformer-pulumi that can generate Pulumi code and perform bulk imports from existing Terraform state files. These tools handle dozens or hundreds of resources at once.
# Generate Pulumi TypeScript from Terraform state
# Using the built-in experimental converter
pulumi converter terraform --terraform-state-file=terraform.tfstate \
--language=typescript --out=pulumi-output/
# Review and manually adjust the generated code
# Then import resources in bulk using a script
Step 6: Manage Configuration and Secrets
Terraform manages variables through .tfvars files, environment variables, and the variable block. Pulumi uses a layered configuration system with stack-specific config files and a built-in secrets manager.
Stack Configuration Files
Each Pulumi stack has a corresponding YAML file (Pulumi.<stack-name>.yaml) that stores configuration values. These replace terraform.tfvars and variable defaults.
# Pulumi.dev.yaml (generated/managed by pulumi CLI)
config:
aws:region: us-west-2
app:instanceType: t3.medium
app:domainName: example.com
# Encrypted secret value
app:dbPassword:
secure: AAAB...encrypted-base64...XYZ==
Set configuration values using the CLI. Secrets are encrypted automatically when you pass the --secret flag.
# Set plaintext config
pulumi config set app:instanceType t3.medium
# Set encrypted secret
pulumi config set --secret app:dbPassword "super-secret-password"
# Read config in code
import * as pulumi from "@pulumi/pulumi";
const appConfig = new pulumi.Config("app");
const dbPassword = appConfig.requireSecret("dbPassword");
Step 7: Preview and Deploy the Migrated Stack
With resources converted and imported, it's time to verify the migration. Pulumi's preview (pulumi preview) shows planned changes without modifying infrastructure, directly analogous to terraform plan.
# Preview changes (equivalent to terraform plan)
pulumi preview
# Example output:
# Previewing update (dev)
# Type Name Plan
# pulumi:pulumi:Stack migration-project-dev
# + aws:ec2:SecurityGroup web-sg create
# + aws:ec2:Instance web-server create
# Resources:
# + 2 to create
# Do you want to perform this update?
If you imported resources correctly, the preview will show zero changes—confirmation that your Pulumi code matches the live state exactly.
# Deploy changes (equivalent to terraform apply)
pulumi up
# For automated pipelines, use non-interactive mode
pulumi up --yes --stack dev
# Refresh state to reconcile with real-world resources
pulumi refresh
Step 8: Decommission Terraform State Safely
Once Pulumi fully manages all resources and you've validated operations through multiple deployments, you can safely retire the Terraform state. This is a critical step that requires careful sequencing.
# 1. Verify Pulumi state contains all expected resources
pulumi stack --show-urns
# 2. Export Pulumi state as a backup
pulumi stack export --stack dev > pulumi-state-backup.json
# 3. Run a final Terraform plan to confirm zero resources
# (after removing resources from .tf files or using terraform state rm)
cd terraform/
terraform plan # Should show: No changes. Your infrastructure matches configuration.
terraform state list # Should return empty or only non-migrated resources
# 4. Archive the Terraform directory and state file
tar -czf terraform-archive-$(date +%Y%m%d).tar.gz terraform/
# 5. Remove the Terraform state backend if applicable
# (e.g., delete the S3 bucket or Terraform Cloud workspace)
Keep the Terraform codebase archived for at least 30 days as a reference. The state file should never be used for operations again to avoid split-brain scenarios where both tools attempt to manage the same resources.
Best Practices for Terraform-to-Pulumi Migration
Adopt Incremental Migration
Never attempt a big-bang migration of hundreds of resources in one go. Break the migration into logical units—by stack, by module, or by resource group—and migrate incrementally. Use pulumi import to adopt resources without downtime, then gradually convert the surrounding code. This approach lets you run Terraform and Pulumi concurrently on different resource sets during the transition period.
Use Stack References for Cross-Stack Dependencies
Terraform's remote state data source allows sharing outputs across configurations. Pulumi's equivalent is StackReference, which reads outputs from another Pulumi stack.
// Reference another Pulumi stack (replaces terraform_remote_state)
import * as pulumi from "@pulumi/pulumi";
const networkStack = new pulumi.StackReference("organization/network-stack/dev");
const vpcId = networkStack.getOutput("vpcId");
// Use the output in a resource
const subnet = new aws.ec2.Subnet("app-subnet", {
vpcId: vpcId,
cidrBlock: "10.0.3.0/24",
});
Convert Modules to Reusable Packages
Instead of one-off component resources, package common infrastructure patterns into reusable Pulumi packages published to your organization's registry. Pulumi's package model supports TypeScript, Python, Go, C#, and Java packages that can be versioned and distributed like any other library.
# Create a reusable Pulumi package from a component
pulumi package new mycorp-vpc --language=typescript
# Package structure:
# mycorp-vpc/
# index.ts # Exported Vpc component
# package.json
# tsconfig.json
Implement Automated Testing Early
One of Pulumi's key advantages is testability. Write unit tests for your infrastructure components using standard testing frameworks before the migration is complete.
// __tests__/vpc.test.ts (using Jest and Pulumi's testing helpers)
import * as pulumi from "@pulumi/pulumi";
import { Vpc } from "../components/vpc";
describe("Vpc Component", () => {
it("creates a VPC with correct CIDR", async () => {
const vpc = new Vpc("test-vpc", {
name: "test",
cidr: "10.0.0.0/16",
availabilityZones: ["us-west-2a"],
publicSubnetCidrs: ["10.0.1.0/24"],
});
// Use Pulumi's runtime helpers to inspect outputs
const vpcId = await pulumi.runtime.isDryRun
? Promise.resolve("vpc-12345")
: vpc.vpcId;
expect(typeof vpcId).toBe("string");
});
});
Standardize Stack Configuration Hierarchy
Establish a consistent configuration layering pattern: base config for common values, environment-specific config for overrides, and secrets always encrypted. Avoid scattering configuration across code files.
# Layered config example
# Pulumi.common.yaml (shared across all stacks via a custom script)
config:
app:retentionDays: "30"
# Pulumi.dev.yaml
config:
aws:region: us-west-2
app:instanceType: t3.micro
# Pulumi.prod.yaml
config:
aws:region: us-east-1
app:instanceType: c5.xlarge
app:dbPassword:
secure: AAAB...
Monitor Drift Proactively
Schedule regular pulumi refresh runs (daily or weekly) to detect and reconcile drift—changes made directly in the cloud console that diverge from your Pulumi state. Integrate this into your CI/CD pipeline with notifications for unexpected drift.
# Cron job or CI scheduled pipeline step
pulumi refresh --stack dev --yes
# If drift detected (non-empty diff), alert the team via Slack/PagerDuty
Maintain a Migration Runbook
Document every step of the migration process, including the mapping of Terraform resources to Pulumi URNs, import commands executed, and any manual interventions. This runbook becomes invaluable during troubleshooting and for onboarding team members who join mid-migration.
# Example runbook entry format
# Resource: aws_security_group.web_sg
# Terraform state key: aws_security_group.web_sg
# Cloud ID: sg-0a7b8c9d4e5f67890
# Pulumi URN: urn:pulumi:dev::migration-project::aws:ec2/securityGroup:SecurityGroup::web-sg
# Import command: pulumi import aws:ec2/securityGroup:SecurityGroup web-sg sg-0a7b8c9d4e5f67890
# Status: Imported and verified — 2024-01-15
# Notes: No drift detected on refresh
Common Pitfalls and Troubleshooting
Resource Replacement Due to Property Mismatch
If pulumi preview shows an unexpected update-replacement on an imported resource, your code definition differs from the live resource in an immutable property. Common culprits include: uppercase vs. lowercase tags, missing optional attributes that have defaults in the cloud, or computed properties that you're now hardcoding. Use pulumi stack export to inspect the exact state and align your code accordingly.
# Export stack state to inspect resource properties
pulumi stack export --stack dev | jq '.resources[] | select(.urn | contains("web-sg"))'
Handling Terraform's count and for_each
Terraform's count and for_each become standard loops in Pulumi. Use Array.map(), for...of, or pulumi.Output.all() for dependent iterations.
// Terraform: resource "aws_instance" "web" { count = 3 ... }
// Pulumi equivalent:
const instanceCount = 3;
const webInstances = Array.from({ length: instanceCount }, (_, i) => {
return new aws.ec2.Instance(`web-${i}`, {
ami: ubuntuAmi.then(ami => ami.id),
instanceType: instanceType,
tags: { Name: `web-server-${i}` },
});
});
// Terraform: for_each = var.subnet_config
// Pulumi equivalent:
const subnetConfig: Record = {
"public-a": { cidr: "10.0.1.0/24", az: "us-west-2a" },
"public-b": { cidr: "10.0.2.0/24", az: "us-west-2b" },
};
const subnets = Object.entries(subnetConfig).map(([name, config]) => {
return new aws.ec2.Subnet(`subnet-${name}`, {
vpcId: vpc.id,
cidrBlock: config.cidr,
availabilityZone: config.az,
});
});
Managing Dependencies Between Imported Resources
Imported resources may lack proper dependency edges in Pulumi's state. After importing, run pulumi up --yes to let the engine recalculate dependencies based on your code's resource references. If circular dependencies emerge, restructure your code to break them using intermediary values or pulumi.Output.apply().
CI/CD Integration for the Migrated Pulumi Project
After migration, adapt your deployment pipelines. Replace terraform plan and terraform apply with Pulumi equivalents. Below is a GitHub Actions workflow example that mirrors a typical Terraform pipeline.
# .github/workflows/pulumi-deploy.yml
name: Pulumi Deployment
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
preview:
name: Preview Infrastructure Changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Pulumi
uses: pulumi/actions@v5
- name: Install Node.js dependencies
run: npm ci
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
aws-region: us-west-2
- name: Pulumi Preview
uses: pulumi/actions@v5
with:
command: preview
stack-name: dev
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
deploy:
name: Deploy Infrastructure
if: github.ref == 'refs/heads/main'
needs: preview
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pulumi/actions@v5
- run: npm ci
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
aws-region: us-west-2
- name: Pulumi Deploy
uses: pulumi/actions@v5
with:
command: up
stack-name: dev
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
Conclusion
Migrating from Terraform to Pulumi is a strategic investment in your infrastructure engineering capabilities. The process transforms rigid HCL configurations into expressive, testable programs that live alongside your application code. By following this step-by-step guide—assessing your Terraform footprint, converting resources methodically, importing live infrastructure without downtime, and adopting best practices like incremental migration and automated testing—you can complete the transition with minimal risk and disruption. The result is a unified IaC platform where your team leverages the full power of modern programming languages, shares infrastructure components as versioned packages, and catches configuration errors before they reach production. Keep your Terraform archive accessible for reference, invest in a robust CI/CD pipeline around Pulumi, and schedule regular state refreshes to maintain a healthy, drift-free infrastructure estate.