Tackling a brownfield Terraform adoption at this scale is indeed a major undertaking, but it should be achievable with a systematic approach. Since you used aztfexport with --hcl-only, you have the configuration code but no state file yet. To refactor this volume of code, you can leverage a combination of architectural isolation and targeted AI automation using GitHub Copilot. Microsoft 365 Copilot excels at workspace documents, but GitHub Copilot is designed for codebase comprehension and IDE integration, making it more suitable in this case.
The core strategy is to limit the blast radius. Do not put all 25+ resource groups into a single Terraform state file. Doing so can increase plan execution times and make mistakes affect a much larger portion of your infrastructure. Instead, map one Terraform state or workspace to one Azure resource group, or to a small cluster of tightly coupled resource groups.
For an individual resource group, aztfexport may output everything into a large main.tf file within a folder. Keep the directory structure intact for that resource group and create separate files in the same folder, such as networking.tf, compute.tf, storage.tf, recovery.tf, and monitoring.tf. Terraform evaluates all .tf files in a directory together, so moving resource blocks between files in the same directory does not change how Terraform tracks those resources.
You can automate the refactoring process using GitHub Copilot inside Visual Studio Code instead of manually cutting and pasting thousands of lines across 25+ resource groups. Before scanning your code, verify that your organization's GitHub Copilot licensing and settings meet its security and compliance requirements. GitHub Copilot Business and Enterprise provide commercial data protections, but you should verify the applicable terms and settings rather than assume that all prompts and code are never logged or retained. With an individual license, review your GitHub account's data and privacy settings to control whether your interactions can be used for model training.
Open the folder for a single resource group in VS Code, then use Copilot Edits or Copilot Chat to identify and reorganize resources. For example, ask Copilot to review main.tf, identify Azure networking resources such as azurerm_virtual_network, azurerm_subnet, azurerm_network_interface, and azurerm_public_ip, and move their complete resource blocks into networking.tf. Repeat this process for other resource categories, placing virtual machines, availability sets, and managed disks in compute.tf; storage accounts and container configurations in storage.tf; and Recovery Services vaults, Log Analytics resources, and diagnostic settings in recovery.tf and monitoring.tf, respectively. Review the resulting changes to ensure that no resource blocks, references, dependencies, or required configuration have been lost or altered.
You can also use Copilot to help standardize the generated HCL. The output from aztfexport may contain repeated resource group names and location strings. Ask Copilot to review each file and identify opportunities to replace repeated values with references to standardized local values, such as local.resource_group_name and local.location. Ensure those local values are defined correctly and that the refactoring preserves the actual configuration of the existing Azure resources.
Once the HCL files are organized, the next goal is to import the existing resources into Terraform state and reconcile the configuration with the deployed infrastructure. Terraform's native import blocks can simplify this process compared with executing hundreds of manual CLI commands. Each block maps an existing Azure resource ID to its corresponding Terraform resource address. For example, an import block can map an Azure virtual network resource ID to azurerm_virtual_network.example. Ensure that the resource block exists and that the address and resource ID are correct before running the import.
Your first terraform plan may reveal configuration differences caused by provider defaults, computed attributes, dynamically generated IDs, or differences in how Azure and Terraform represent values, such as location casing. Review these differences. If Terraform proposes an unexpected modification or replacement, provide the relevant plan output to Copilot and ask it to identify the attribute causing the difference and suggest an appropriate HCL correction. Use lifecycle.ignore_changes only when the ignored attribute is intentionally managed outside Terraform and the resulting loss of drift detection is acceptable. Do not use it simply to suppress unexplained differences.
The objective is to achieve a zero-change plan, meaning Terraform proposes no changes to the existing infrastructure after the resources have been imported and the configuration has been reconciled. Once the plan confirms that the configuration accurately represents the deployed resources, run terraform apply to complete any pending imports and finalize the state. Store the state in a securely configured remote backend with state locking and appropriate access controls. Note that importing resources and applying a zero-change plan does not itself lock the state permanently - the backend provides state storage and locking during Terraform operations.
If the above response helps answer your question, remember to "Accept Answer" so that others in the community facing similar issues can easily find the solution. Your contribution is highly appreciated.
hth
Marcin