The IaC week so far assumed greenfield. Reality is a subscription full of resources created by hand over five years, and the question every platform team eventually faces: how does this become code without a big bang rebuild? The answer is a sequenced import campaign, and the tooling has matured enough to make it a project rather than an ordeal.
The Mechanics: Import Blocks and Generated Config
Modern Terraform makes import declarative: an import block pairs a resource address with an Azure resource ID, and terraform plan -generate-config-out writes a starting configuration for resources you have not authored yet. The generated HCL is a draft, not a destination, it captures every attribute verbatim where your standards want variables, modules, and omitted defaults, so the workflow is: import block, generate, refactor the draft into your module conventions, plan until the diff is zero, commit. Zero diff is the acceptance test; a plan that wants to modify an imported resource means your configuration and reality disagree, and you resolve it deliberately, either adopting reality into code or scheduling the change that brings reality to standard.
import {
to = module.orders_db.azurerm_postgresql_flexible_server.this
id = "/subscriptions/xxxx/resourceGroups/rg-orders-prod/providers/Microsoft.DBforPostgreSQL/flexibleServers/psql-orders-prod"
}
For estates, do this wholesale with tooling: Azure Export for Terraform (aztfexport) walks a resource group or query scope and emits both configuration and import mappings, giving you the first eighty percent mechanically. Where the azurerm provider lacks coverage for a niche or preview resource, the AzAPI provider is the escape hatch, typed access to any ARM resource type and API version, importable the same way, and a legitimate permanent resident of your configuration rather than a temporary shame; wrap AzAPI resources in the same modules and nobody downstream knows the difference.
Sequencing the Campaign
Order by blast radius and dependency direction, the same logic as the landing zone series. First the governance layer: management groups, policy, RBAC, cheap to import, high leverage, and it establishes the state architecture from Monday’s post. Second, shared platform: networking, DNS zones, firewalls, the hub. Third, workloads, one application per state, prioritized by change frequency, the app that deploys weekly benefits from code far more than the file server nobody touches. Some things should not make the cut: resources scheduled for decommission, one off experiments, and personal dev debris get a decommission date instead of an import block, because codifying garbage preserves it. Within each wave the loop is identical: export, refactor into modules, zero diff plan, enable the drift detection nightly plan, then move the pipeline in front of it so future changes flow through code.
Keeping It Won
An imported estate regresses the day someone edits the portal, so the campaign’s final phase is closing the side doors: RBAC narrowed so humans read and pipelines write, the deny assignment pattern (via deployment stacks where you run Bicep, via policy and PIM discipline elsewhere), drift alerts triaged like incidents with the reconciling PR rule from the state post, and the cultural marker that matters most, the first time an emergency change goes through an expedited pipeline run instead of the portal and everyone sees it was fast enough. Measure progress honestly with a resource coverage metric from Resource Graph, resources under management versus total, and celebrate the boring milestone when the number stops moving because everything that should be code is code.
Cheers
Osama
Leave a Reply