使用 Azure CLI 发现和 Azure Verified Modules (AVM) 将现有 Azure 资源导入 Terraform。当需要逆向工程实时 Azure 基础设施、从现有订阅/资源组/资源 ID 生成基础设施即代码、映射依赖关系、从下载的模块源推导精确导入地址、防止配置漂移,并生成基于 AVM 的 Terraform 文件(可用于跨任何 Azure 资源类型的验证和规划)时使用。
导入基础设施即代码(Azure -> 使用 AVM 的 Terraform)
使用发现数据和 Azure Verified Modules 将现有 Azure 基础设施转换为可维护的 Terraform 代码。
何时使用此技能
当用户要求以下操作时使用此技能:
- 将现有 Azure 资源导入 Terraform
- 从实时 Azure 环境生成 IaC
- 处理 AVM 支持的任何 Azure 资源类型(并记录合理的非 AVM 回退方案)
- 从订阅或资源组重建基础设施
- 映射已发现 Azure 资源之间的依赖关系
- 使用 AVM 模块而不是手写
azurerm_*资源
先决条件
- 已安装并认证 Azure CLI(
az login) - 可访问目标订阅或资源组
- 已安装 Terraform CLI
- 可访问 Terraform Registry 和 AVM 索引源
输入
| 参数 | 必需 | 默认值 | 描述 |
|---|---|---|---|
subscription-id |
否 | 活动 CLI 上下文 | 用于订阅范围发现和上下文设置的 Azure 订阅 |
resource-group-name |
否 | 无 | 用于资源组范围发现的 Azure 资源组 |
resource-id |
否 | 无 | 用于特定资源范围发现的一个或多个 Azure ARM 资源 ID |
至少需要 subscription-id、resource-group-name 或 resource-id 中的一个。
分步工作流
1) 收集所需范围(必需)
在运行发现命令之前,请求以下范围之一:
- 订阅范围:
<subscription-id> - 资源组范围:
<resource-group-name> - 特定资源范围:一个或多个
<resource-id>值
范围处理规则:
- 将 Azure ARM 资源 ID(例如
/subscriptions/.../providers/...)视为云资源标识符,而非本地文件系统路径。 - 仅将资源 ID 用于 Azure CLI
--ids参数(例如az resource show --ids <resource-id>)。 - 除非用户明确说明是本地文件路径,否则切勿将资源 ID 传递给文件读取命令(
cat、ls、read_file、glob 搜索)。 - 如果用户已提供一个有效范围,除非失败命令需要,否则不要请求额外的范围输入。
- 不要询问可以从已提供的范围值回答的后续问题。
如果缺少范围,明确要求并提供,然后停止。
2) 认证并设置上下文
仅运行所选范围所需的命令。
对于订阅范围:
az login
az account set --subscription <subscription-id>
az account show --query "{subscriptionId:id, name:name, tenantId:tenantId}" -o json
预期输出:包含 subscriptionId、name 和 tenantId 的 JSON 对象。
对于资源组或特定资源范围,仍需要 az login,但如果活动上下文已正确,则 az account set 可选。
使用特定资源范围时,优先直接使用基于 --ids 的命令,除非具体命令需要,否则避免额外的订阅或资源组发现提示。
3) 运行发现命令
使用所选范围发现资源。确保获取所有必要信息以准确生成 Terraform。
# 订阅范围
az resource list --subscription <subscription-id> -o json
# 资源组范围
az resource list --resource-group <resource-group-name> -o json
# 特定资源范围
az resource show --ids <resource-id-1> <resource-id-2> ... -o json
预期输出:包含 Azure 资源元数据(id、type、name、location、tags、properties)的 JSON 对象或数组。
4) 在代码生成前解析依赖关系
解析导出的 JSON 并映射:
- 父子关系(例如:NIC -> 子网 -> VNet)
properties中的跨资源引用- Terraform 创建的顺序
重要:生成以下文档并保存到项目根目录的 docs 文件夹中。
exported-resources.json:包含所有发现的资源及其元数据,包括依赖关系和引用。EXPORTED-ARCHITECTURE.MD:基于发现的资源及其关系的人类可读架构概述文件。
5) 选择 Azure Verified Modules(必需)
为每种资源类型使用最新的 AVM 版本。
Terraform Registry
- 搜索 "avm" + 资源名称
- 按 "Partner" 标签过滤以查找官方 AVM 模块
- 示例:搜索 "avm storage account" → 按 Partner 过滤
官方 AVM 索引
注意: 以下链接始终指向主分支上 CSV 文件的最新版本。这意味着文件可能会随时间变化。如果需要特定时间点的版本,请考虑在 URL 中使用特定发布标签。
- Terraform 资源模块:
https://raw.githubusercontent.com/Azure/Azure-Verified-Modules/refs/heads/main/docs/static/module-indexes/TerraformResourceModules.csv - Terraform 模式模块:
https://raw.githubusercontent.com/Azure/Azure-Verified-Modules/refs/heads/main/docs/static/module-indexes/TerraformPatternModules.csv - Terraform 实用模块:
https://raw.githubusercontent.com/Azure/Azure-Verified-Modules/refs/heads/main/docs/static/module-indexes/TerraformUtilityModules.csv
单个模块信息
如果 .terraform 文件夹中本地没有模块信息,请使用 web 工具或其他合适的 MCP 方法获取模块信息。
使用 AVM 源:
- Registry:
https://registry.terraform.io/modules/Azure/<module>/azurerm/latest - GitHub:
https://github.com/Azure/terraform-azurerm-avm-res-<service>-<resource>
当 AVM 模块存在时,优先使用 AVM 模块而非手写 azurerm_* 资源。
从 GitHub 仓库获取模块信息时,仓库根目录中的 README.md 文件通常包含模块的所有详细信息,例如:https://raw.githubusercontent.com/Azure/terraform-azurerm-avm-res-<service>-<resource>/refs/heads/main/README.md
5a) 在编写任何代码之前阅读模块 README(必需)
此步骤不可省略。 在为模块编写一行 HCL 之前,获取并阅读该模块的完整 README。不要依赖对原始 azurerm 提供者的了解或之前使用其他 AVM 模块的经验。
对于每个选定的 AVM 模块,获取其 README:
https://raw.githubusercontent.com/Azure/terraform-azurerm-avm-res-<service>-<resource>/refs/heads/main/README.md
或者如果模块在 terraform init 后已下载:
cat .terraform/modules/<module_key>/README.md
从 README 中提取并记录 在编写代码之前:
- 必需输入 — 模块所需的每个输入。此处列出的任何子资源(NIC、扩展、子网、公共 IP)都在模块 内部 管理。不要为这些资源创建独立的模块块。
- 可选输入 — 确切的 Terraform 变量名称及其声明的
type。不要假设它们与原始azurerm提供者的参数名称或块形状匹配。 - 使用示例 — 检查使用的资源组标识符(
parent_id与resource_group_name),子资源如何表示(内联映射与单独模块),以及每个输入期望的语法。
将模块规则作为模式应用,而非假设
使用以下经验教训作为导致导入失败的 类型 不匹配的示例。不要假设这些确切名称适用于每个 AVM 模块。始终验证每个选定模块的 README 和 variables.tf。
avm-res-compute-virtualmachine(任何版本)
network_interfaces是 必需输入。NIC 由 VM 模块拥有。切勿在 VM 模块旁边创建独立的avm-res-network-networkinterface模块 — 在network_interfaces下内联定义每个 NIC。- TrustedLaunch 通过顶层布尔值
secure_boot_enabled = true和vtpm_enabled = true表示。security_type参数仅存在于os_disk下,用于机密 VM 磁盘加密,不得用于 TrustedLaunch。 boot_diagnostics是bool类型,而非对象。使用boot_diagnostics = true;如果需要存储 URI,则使用单独的boot_diagnostics_storage_account_uri变量。- 扩展通过
extensions映射在模块内部管理。不要创建独立的扩展资源。
avm-res-network-virtualnetwork(任何版本)
- 此模块由 AzAPI 提供者支持,而非
azurerm。使用parent_id(完整的资源组资源 ID 字符串)指定资源组,而非resource_group_name。 - README 中的每个示例都显示
parent_id;没有示例显示resource_group_name。
所有 AVM 模块的通用要点:
- 在创建兄弟模块之前,从 必需输入 确定子资源所有权。
- 从 可选输入 和
variables.tf确定接受的变量名称和类型。 - 从 README 使用示例确定标识符样式和输入形状。
- 不要从原始
azurerm_*资源推断参数名称。
6) 生成 Terraform 文件
在编写导入块之前 — 检查模块源(必需)
在 terraform init 下载模块后,在编写任何 import {} 块之前,检查每个模块的源文件以确定确切的 Terraform 资源地址。切勿凭记忆编写导入地址。
步骤 A — 识别提供者和资源标签
grep "^resource" .terraform/modules/<module_key>/main*.tf
这将揭示模块使用的是 azurerm_* 还是 azapi_resource 标签。例如,avm-res-network-virtualnetwork 暴露的是 azapi_resource "vnet",而非 azurerm_virtual_network "this"。
步骤 B — 识别子模块和嵌套路径
grep "^module" .terraform/modules/<module_key>/main*.tf
如果子资源在子模块中管理(子网、扩展等),导入地址必须包含每个中间模块标签:
module.<root_module_key>.module.<child_module_key>["<map_key>"].<resource_type>.<label>[<index>]
步骤 C — 检查 count 与 for_each
grep -n "count\|for_each" .terraform/modules/<module_key>/main*.tf
任何使用 count 的资源都需要在导入地址中包含索引。当 count = 1 时(例如,条件性 Linux 与 Windows 选择),地址必须以 [0] 结尾。使用 for_each 的资源使用字符串键,而非数字索引。
已知导入地址模式(来自经验教训的示例)
这些仅是示例。将它们用作推理模板,然后从当前导入中模块的下载源代码推导出确切地址。
| 资源 | 正确的导入 to 地址模式 |
|---|---|
| 基于 AzAPI 的 VNet | module.<vnet_key>.azapi_resource.vnet |
| 子网(嵌套,基于 count) | module.<vnet_key>.module.subnet["<subnet_name>"].azapi_resource.subnet[0] |
| Linux VM(基于 count) | module.<vm_key>.azurerm_linux_virtual_machine.this[0] |
| VM NIC | module.<vm_key>.azurerm_network_interface.virtualmachine_network_interfaces["<nic_key>"] |
| VM 扩展(默认 deploy_sequence=5) | module.<vm_key>.module.extension["<ext_name>"].azurerm_virtual_machine_extension.this |
| VM 扩展(deploy_sequence=1–4) | module.<vm_key>.module.extension_<n>["<ext_name>"].azurerm_virtual_machine_extension.this |
| NSG-NIC 关联 | module.<vm_key>.azurerm_network_interface_security_group_association.this["<nic_key>-<nsg_key>"] |
生成:
providers.tf:包含azurerm提供者和所需版本约束main.tf:包含 AVM 模块块和显式依赖关系variables.tf:用于环境特定值outputs.tf:用于关键 ID 和端点terraform.tfvars.example:包含占位符值
将实时属性与模块默认值进行比较(必需)
编写初始配置后,将每个发现的实时资源的每个非零属性与相应 AVM 模块的 variables.tf 中声明的默认值进行比较。任何实时值与模块默认值不同的属性都必须在 Terraform 配置中显式设置。
特别注意以下属性类别,这些是静默配置漂移的常见来源:
- 超时值(例如,公共 IP
idle_timeout_in_minutes默认为4;实际部署通常使用30) - 网络策略标志(例如,子网
private_endpoint_network_policies默认为"Enabled";现有子网通常为"Disabled") - SKU 和分配(例如,公共 IP
sku、allocation_method) - 可用区(例如,VM 区域、公共 IP 区域)
- 存储和数据库资源的冗余和复制设置
使用显式 az 命令检索完整的实时属性,例如:
az network public-ip show --ids <resource_id> --query "{idleTimeout:idleTimeoutInMinutes, sku:sku.name, zones:zones}" -o json
az network vnet subnet show --ids <resource_id> --query "{privateEndpointPolicies:privateEndpointNetworkPolicies, delegation:delegations}" -o json
不要仅依赖 az resource list 输出,它可能省略嵌套或计算属性。
显式固定模块版本:
module "example" {
source = "Azure/<module>/azurerm"
version = "<latest-compatible-version>"
}
7) 验证生成的代码
运行:
terraform init
terraform fmt -recursive
terraform validate
terraform plan
预期输出:无语法错误、无验证错误,且计划与发现的基础设施意图匹配。
故障排除
| 问题 | 可能原因 | 操作 |
|---|---|---|
az 命令因授权错误失败 |
错误的租户/订阅或缺少 RBAC 角色 | 重新运行 az login,验证订阅上下文,确认所需权限 |
| 发现输出为空 | 范围不正确或范围内无资源 | 重新检查范围输入并再次运行范围列表/显示命令 |
| 未找到资源类型的 AVM 模块 | 资源类型尚未被 AVM 覆盖 | 对该类型使用原生 azurerm_* 资源并记录差距 |
terraform validate 失败 |
缺少变量或未解析的依赖关系 | 添加所需变量和显式依赖关系,然后重新运行验证 |
| 模块中找不到未知参数或变量 | AVM 变量名称与 azurerm 提供者参数名称不同 |
阅读模块 README 的 variables.tf 或可选输入部分以获取正确名称 |
| 导入块失败 — 在地址处未找到资源 | 错误的提供者标签(azurerm_ 与 azapi_)、缺少子模块路径或缺少 [0] 索引 |
运行 grep "^resource" .terraform/modules/<key>/main*.tf 和 grep "^module" 以找到确切地址 |
terraform plan 显示导入资源上意外的 ~ update |
实时值与 AVM 模块默认值不同 | 使用 az <resource> show 获取实时属性,与模块默认值比较,添加显式值 |
| 子资源模块给出 "provider configuration not present" | 子资源声明为独立模块,即使父模块拥有它们 | 检查 README 中的必需输入,删除不正确的独立模块,并使用父模块文档化的输入结构建模子资源 |
| 嵌套子资源导入失败,提示 "resource not found" | 缺少中间模块路径、错误的映射键或缺少索引 | 检查源中的模块块和 count/for_each;构建完整的嵌套导入地址,包括所有模块段和所需键/索引 |
| 工具尝试将 ARM 资源 ID 作为文件路径读取或重复询问范围问题 | 资源 ID 未被视为 --ids 输入,或代理未信任已提供的范围 |
严格将 ARM ID 视为云标识符,使用 az ... --ids ...,并在存在一个有效范围后停止重新提示 |
响应契约
返回结果时,提供:
- 使用的范围(订阅、资源组或资源 ID)
- 创建的文件
- 检测到的资源类型
- 选定的 AVM 模块及其版本
- 生成或更新的 Terraform 文件
- 验证命令结果
- 需要用户输入的未解决问题(如有)
代理执行规则
- 如果缺少范围,不要继续。
- 不要在没有列出发现文件和验证输出的情况下声称导入成功。
- 在生成 Terraform 之前不要跳过依赖关系映射。
- 优先使用 AVM 模块;明确证明每个非 AVM 回退的合理性。
- 在编写代码之前阅读每个 AVM 模块的 README。 必需输入标识模块拥有哪些子资源。可选输入记录确切的变量名称和类型。使用示例显示提供者特定的约定(
parent_id与resource_group_name)。跳过 README 是基于 AVM 的导入中代码错误的最常见原因。 - 切勿假设 NIC、扩展或公共 IP 资源是独立的。 对于任何 AVM 模块,除非 README 明确指示需要单独的模块,否则将子资源视为父模块拥有。在创建兄弟模块之前检查必需输入。
- 切勿凭记忆编写导入地址。 在
terraform init之后,使用 grep 检查下载的模块源以发现实际的提供者(azurerm与azapi)、资源标签、子模块嵌套以及count与for_each的使用,然后再编写任何import {}块。 - 切勿将 ARM 资源 ID 视为文件路径。 资源 ID 属于 Azure CLI
--ids参数和 API 查询,而非文件 IO 工具。仅在提供实际工作空间路径时读取本地文件。 - 当范围已知时最小化提示。 如果已提供订阅、资源组或特定资源 ID,直接继续执行命令,仅当命令因缺少所需上下文而失败时才询问后续问题。
- 在
terraform plan显示 0 个销毁和 0 个不需要的更改之前,不要声明导入完成。+ create资源是可接受的。任何~ update或- destroy对实际基础设施资源的更改都必须解决。






