# Bring existing Azure infra under Terraform, fast

Bringing existing Azure infrastructure under Terraform management is one of those tasks that sounds simple until you actually do it. Terraform has supported `import {}` blocks since 1.5, but writing them by hand is tedious: every block needs the exact Azure resource ID, and a lot of those IDs are surprisingly hard to look up.

I built a small tool to do it for you — straight from a binary `.plan` file, with no `terraform init`, no provider plugins, and no `terraform` CLI required. It's on PyPI as [`generate-imports-from-plan`](https://pypi.org/project/generate-imports-from-plan/).

## The problem

Say you have a Terraform configuration that describes resources which already exist in Azure. You run a plan, and Terraform wants to *create* everything — because it doesn't know those resources are already there. The fix is to write an `import {}` block per resource:

```hcl
import {
  to = module.core.azurerm_resource_group.this
  id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-myapp-dev-we-01"
}
```

For a handful of resources, fine. For a real environment with dozens of resource groups, networks, role assignments and Entra ID apps, doing this by hand is a long afternoon of copy-pasting IDs out of the portal.

The obvious idea is to script it from `terraform show -json`. But there's a catch.

## Why `terraform show -json` isn't enough

`terraform show -json <planfile>` does not expose all planned attribute values. Computed fields — including things you need to build the resource ID, like the resource name or the resource group name — show up as "known after apply" and are simply absent from the JSON.

The values *are* in the plan, though. A `.plan` file is actually a ZIP containing:

- `tfplan` — the planned changes, as protobuf
- `tfstate` — the prior state
- `tfconfig/` — the raw HCL of your configuration

Inside `tfplan`, attribute values are msgpack-encoded, and they include the computed values that the JSON view drops. So instead of going through `terraform show`, this tool reads the binary plan directly: unzip, parse the protobuf, decode the msgpack attributes. No provider plugins needed, because nothing is being evaluated — the values are already sitting in the plan.

## How IDs get resolved

Azure resource IDs come in a few flavours, and the tool handles each differently:

1. **Formula-based IDs.** Most ARM resources have a deterministic ID you can assemble from their attributes — subscription, resource group, provider namespace, name. These are built from a formula and emitted immediately, no prompting.

2. **Cross-plan IDs.** Some resources have an ID that depends on *another* resource in the same plan — a subnet association needs the subnet ID, a DNS-servers resource needs the VNet ID. The tool reads the HCL references in `tfconfig/` and derives those IDs from the sibling resource, again without any external calls.

3. **Live IDs.** A few resource types only get their ID assigned at creation time — Entra ID resources, Azure role assignments. For those, the tool calls the Azure CLI (`az`) to look up the real ID, and it shows you the exact command before running it.

There's one more nice detail: the correct **subscription ID is determined per resource** by following the `azurerm` provider chain through the config. A plan that spans multiple subscriptions gets the right subscription on each resource — no single global assumption.

## Using it

The tool is published to PyPI, so the quickest path (nothing installed permanently) is `uvx`:

```bash
uvx generate-imports-from-plan terraform.plan > imports.tf
```

If you don't have [`uv`](https://docs.astral.sh/uv/), install it (`pip install uv`, or `winget install astral-sh.uv` on Windows), or install the tool directly with pip/pipx:

```bash
pipx install generate-imports-from-plan
generate-imports-from-plan terraform.plan > imports.tf
```

You'll need Python 3.11+ and — only for the live lookups — the Azure CLI.

## The workflow

```bash
# 1. Generate the plan
terraform plan -out terraform.plan

# 2. Generate import blocks interactively
generate-imports-from-plan terraform.plan > imports.tf

# 3. Fill in any remaining placeholder IDs, then re-plan and apply
terraform plan -out terraform.plan
terraform apply terraform.plan
```

The tool is interactive and decides what to do per resource:

| Situation | Behaviour |
|---|---|
| Complete formula-based ID | Emitted immediately, no prompt |
| Cross-plan ID (depends on another resource in the plan) | Resolved and emitted automatically |
| Entra ID resource or role assignment | Shows the `az` command and asks for confirmation |
| Unresolvable ID | Shows which attribute is computed and its HCL reference chain; asks whether to skip |
| Unsupported import (e.g. `azuread_application_password`) | Emits a comment block instead |

Use `--yes` to accept everything without prompting, or `--no` for a dry run.

Sample output:

```hcl
import {
  to = module.core.azurerm_resource_group.this
  id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-myapp-dev-we-01"
}

import {
  to = module.core.azuread_application.default["my-app"]
  id = "/applications/00000000-0000-0000-0000-000000000000"
}

# import not supported for azuread_application_password:
# module.core.azuread_application_password.default["my-app"]
```

## Extending it

Adding support for a new resource type is usually a one-line formula. For example, in `generate_imports/ids.py`:

```python
"azurerm_my_resource":
    lambda a, s: _arm(s, _str(a, "resource_group_name"),
                      "Microsoft.MyNamespace/myResources", _str(a, "name")),
```

For the trickier cases there are also cross-plan resolvers (derive an ID from a sibling resource) and live resolvers (`az` CLI). The repo's `docs/resolvers.md` walks through all three.

## Links

- 📦 PyPI: [generate-imports-from-plan](https://pypi.org/project/generate-imports-from-plan/)
- 💻 Source & docs: [github.com/cveld/terraform-importer](https://github.com/cveld/terraform-importer)

If you try it on your own environment, I'd love to hear which resource types you hit that aren't covered yet — issues and PRs are welcome.
