The goal
Every production change goes through a pull request and a pipeline, and the code describes what really exists. You get there by adopting what is already deployed, not by rebuilding it.
1. Pick a tool and a scope
| Choose | When |
|---|---|
| Bicep | Azure-only estate, team knows ARM, want day-zero support for new resource types |
| Terraform | Multi-cloud or existing Terraform skills, want plan output and a large module ecosystem |
Start with one landing zone or one application resource group, not the whole tenant.
2. Bicep path: export, decompile, clean
az group export -g rg-app-prod > rg-app-prod.json
az bicep decompile --file rg-app-prod.json
The output works but is noisy. Clean it:
- Replace hard-coded names, IDs and locations with parameters.
- Remove read-only properties and defaults.
- Split into modules (network, data, compute).
Check with what-if — the goal is no changes:
az deployment group what-if -g rg-app-prod -f main.bicep -p @prod.bicepparam
Iterate until what-if shows nothing to modify (or only harmless noise you understand).
3. Terraform path: import blocks
Terraform 1.5+ can generate configuration for existing resources:
import {
to = azurerm_resource_group.app
id = "/subscriptions/<sub>/resourceGroups/rg-app-prod"
}
import {
to = azurerm_storage_account.data
id = "/subscriptions/<sub>/resourceGroups/rg-app-prod/providers/Microsoft.Storage/storageAccounts/stappprod"
}
terraform plan -generate-config-out=generated.tf
terraform apply # imports into state; should report 0 to add, 0 to change, 0 to destroy
Refactor generated.tf into modules and variables, then re-run terraform plan until it is clean. Microsoft's aztfexport tool can also bulk-export a resource group to Terraform.
Caution
Never apply a plan that destroys or replaces production resources during adoption. A "forces replacement" line means your code differs from reality on an immutable property — fix the code.
4. Pipeline and state
- Store Terraform state in a locked-down storage account with versioning and soft delete.
- Run plan on pull request, apply on merge, with an approval gate for production.
- Use workload identity federation for the pipeline identity, not a client secret.
5. Stop the drift
- Scheduled pipeline runs
what-if/terraform plannightly and alerts on any difference. - Reduce portal write access: move people from Contributor to Reader on production, keep a break-glass group.
- Use Azure Policy to deny resources without required tags that only the pipeline sets.
Checklist
- One scope adopted end to end, with no-change plan
- Remote state secured and backed up
- PR-based pipeline with approvals
- Nightly drift detection
- Portal write access reduced