skip to content

Your team wants to move an existing Terraform repository that manages live production infrastructure over to Pulumi. What is the migration path, and what does not carry over automatically?

level: seniorimportance: should knowfreq 38%

answer

  1. two jobs, not one
  2. code converts, state does not
  3. empty stack means recreate everything
  4. done when preview shows nothing
  5. coexistence beats a big-bang migration

basics

~20 s

Two separate jobs: convert the code with pulumi convert --from terraform, then adopt the live resources into the Pulumi stack with imports. Converting code alone is dangerous — without imports the first up recreates everything, because state does not convert.

solid answer

~50 s

Treat it as code migration plus state adoption, and never confuse the two. `pulumi convert --from terraform` translates HCL into a program in your chosen language — it replaces the older tf2pulumi tool — but it produces only source, so the new stack believes it manages nothing. You then have to import the live resources so Pulumi adopts rather than recreates them, resource by resource or from a bulk import file, and you are done only when a preview against production shows zero changes. Expect a real human pass over the generated program: module structure, `count` and `for_each` idioms, dynamic blocks, provider aliases and variable files all come across mechanically and read badly. The safer alternative for a large estate is coexistence — leave Terraform managing what it manages, read its outputs from Pulumi, and write only new stacks in Pulumi.

code

bash · 5 lines
bash
pulumi convert --from terraform --language typescript --out ./infra
cd infra
pulumi stack init prod
pulumi import aws:s3/bucketV2:BucketV2 logs-prod my-prod-logs-bucket
pulumi preview --refresh --diff

go deeper

for a junior

Know that moving to another IaC tool means two things: translating the source code, and telling the new tool about the resources that already exist so it does not try to create them again.

for a middle

Explain the tooling and its limits — pulumi convert --from terraform produces a program but no state, so imports are mandatory, and the converted code needs a human pass over modules, loops and provider wiring.

for a senior

Demonstrate the safe sequence: one state boundary at a time, freeze the old tool, import, prove with a clean preview, apply once, then release from the old tool without destroying anything.

for a principal

Own the decision rather than the mechanics: weigh migration risk against the actual benefit, propose coexistence for a stable estate, and set the policy for which new systems go where.

## Frame it as two jobs, not one Every bad version of this migration comes from treating it as a single translation step. There are two independent things to move: 1. **The code** — HCL becomes a program in TypeScript, Python, Go or C#. 2. **The management relationship** — the record that says "this live resource is mine, here is its ID, here is what I last set on it". The first is automatable and low risk. The second is manual, is where production gets destroyed, and does not happen as a side effect of the first. ## Step 1: convert the code `pulumi convert --from terraform` reads the HCL in a directory and emits a Pulumi program in the language you ask for. It supersedes the older `tf2pulumi` tool. It handles the mechanical mapping well: resource types, arguments, most expressions, and Terraform variables become stack configuration. What comes out is a *starting point*, not a finished codebase. Expect to rewrite: - **Module structure.** A tree of Terraform modules converts into something functional but flat-feeling; if you wanted component abstractions, that is a design pass you do by hand. - **`count` and `for_each` idioms.** These become loops or maps that are correct but not idiomatic, and the resource naming they produce deserves scrutiny because names determine identity. - **Dynamic blocks and heavy expression code.** Conversion is literal; the result is often clearer rewritten. - **Provider aliases and multi-provider setups.** Verify each resource ends up bound to the provider you intended. - **Variable files and workspaces.** Values move into stack configuration, and a per-environment layout has to be reconstructed deliberately. - **Comments, formatting, and your repository conventions.** Largely lost. Budget review time proportional to the estate, and do not merge converted code that nobody has read. ## Step 2: adopt the live resources This is the step with the risk. A fresh Pulumi stack starts empty. Run `pulumi up` against converted code with an empty stack and it will happily try to create a second copy of everything — or fail on name collisions, which is the lucky outcome. For stateful resources the unlucky outcome is a duplicate database, a duplicate DNS record, or a conflict that leaves you half-migrated. So before any apply, every live resource must be imported into the stack. `pulumi import` adopts an existing resource by its provider ID and can generate matching code; for anything beyond a handful of resources you drive it from a bulk import file rather than by hand. Pulumi also ships tooling to seed this from an existing Terraform state file, which is far less error-prone than transcribing IDs. The acceptance test is unambiguous and you should state it in the interview: **the migration of a stack is done when `pulumi preview` against production reports no changes.** Not "a few harmless diffs" — every remaining diff is either a real behavioural difference between the two providers' defaults or a mistake in your converted code, and both need resolving before you cut over. ## Step 3: cut over safely - Migrate **one state boundary at a time**. Whatever the Terraform repository split into, keep those seams; do not merge them during the migration. - **Freeze** the Terraform side for that boundary while you import — two tools that both believe they manage a resource is the worst state to be in, and neither will warn you. - Once Pulumi previews clean and has applied once, remove those resources from Terraform's management **without destroying them** and delete the corresponding HCL. - Keep the old state around, read-only, until you are confident. - Do the least-important boundary first as a rehearsal. ## The option people forget: don't migrate For a large, stable estate the honest recommendation is often coexistence. Pulumi can read a Terraform stack's outputs — the `pulumi-terraform` provider exposes a remote state reference for exactly this — so a new Pulumi stack can consume the VPC ID or subnet IDs that Terraform still owns. New systems get built in Pulumi; the existing estate stays where it is and is migrated opportunistically or never. This is usually the better trade because the migration's benefit is the *language*, and the language only pays where there is logic to express. Re-importing three hundred stable resources to write the same declarations in TypeScript is risk without return. A senior answer says this out loud: propose the migration path, then say when you would not take it. ## The one-line summary Convert the code, import the state, prove it with an empty preview, cut over one boundary at a time — and consider coexistence instead, because code converts automatically and management does not.

  • What is your acceptance criterion that a migrated stack is safe to cut over?
    A `pulumi preview` run against production, with refresh enabled, that reports no changes at all. Every remaining diff is either a genuine default difference between the two providers or an error in the converted code, and both must be resolved rather than waved through. Only after that do you apply once, then remove the resources from Terraform's management without destroying them.
  • How do you avoid two tools believing they manage the same resource during the cutover?
    Freeze the Terraform side for the boundary being migrated — stop applies, ideally via pipeline and IAM controls, not just an agreement — do the imports, prove the clean preview, apply once from Pulumi, and only then drop the resources from Terraform's management without destroying them. Migrate one state boundary at a time so the frozen window is small and reversible.
  • When would you recommend not migrating at all?
    When the estate is large, stable and genuinely declarative, the team relies on community modules and HCL-oriented scanners, or the reviewers are ops-leaning. Migration costs a full re-import with real production risk and pays only where there is logic to express. Coexistence is usually stronger: Terraform keeps what it owns, Pulumi reads its outputs through the remote state reference, and new logic-heavy systems are built in Pulumi.

saying these in an interview costs you the question

  • Believes converting the code also brings the recorded state across
  • Runs the first up against production without importing anything
  • Suggests deleting the Terraform state so the tools do not conflict
  • Treats leftover diffs after conversion as cosmetic and applies anyway
  • Proposes a single big-bang migration of the whole estate at once

context