Terraform on Azure 实战指南:从零搭建生产级基础设施
1. 项目概述为什么一个老运维在 Azure 上写第一行 Terraform 时手是抖的我第一次在 Azure 上敲下terraform init的时候手是真的抖。不是因为紧张而是因为——太熟悉那种“点点点”之后资源没起来、报错信息像天书、回滚全靠手动删资源的窒息感了。那会儿我刚接手一个客户环境三套相似的测试环境每套都要配 VNet、NSG、两个子网、一个跳板机、一个应用 VM、一个 SQL Server外加一堆标签和诊断设置。我花了整整两天半用 Azure 门户点到手指发麻中间还误删过一次生产 NSG 规则差点被拉进会议室喝茶。那天晚上回家路上我在地铁上翻 HashiCorp 官网看到那句 “Write, Plan, Apply” 的标语心里就一个念头这玩意儿要是真能管住 Azure我愿意给它倒杯咖啡。这就是 Terraform on Azure 的真实起点它不是什么高大上的新概念而是一把给一线工程师用的、能砍掉重复劳动、堵住人为失误、让“环境一致性”从口号变成默认行为的工具。它解决的不是“能不能做”而是“能不能不犯错地、不加班地、不被半夜叫醒地做”。你不需要是 DevOps 专家也不需要懂 Go 语言只要你每天要跟 Azure 打交道——不管是开发要搭测试环境、SRE 要管生产集群、还是云架构师要设计灾备方案——Terraform 就是你该立刻装进工具箱里的那把瑞士军刀。它核心就干三件事用代码定义你想要的 Azure 状态比如“我要一个带公网 IP 的 Ubuntu VM装好 Nginx只开放 80 和 22 端口”用plan告诉你它打算怎么一步步实现精确到创建顺序、修改字段、删除旧资源最后用apply把这个计划稳稳落地。整个过程不依赖你的鼠标点击习惯不依赖你今天心情好不好更不依赖你有没有记清上次在哪个资源组里建了那个 Key Vault。它把“人脑记忆”换成了“机器校验”把“口头约定”换成了“版本控制”。所以这篇指南不是给理论家写的是给那些明天就要在 Azure 上开干、后天就要交付环境、下周就要应对审计的实战派准备的。它不会堆砌术语但会告诉你为什么azurerm_linux_virtual_machine比azurerm_virtual_machine更值得用它不会回避坑反而会把我在客户现场踩过的、导致terraform apply卡死在“Waiting for SSH”长达 47 分钟的网络配置陷阱原原本本拆给你看它更不会假装 CI/CD 很简单而是直接给你贴出 GitHub Actions 里那个必须加的if: github.ref refs/heads/main github.event_name push判断逻辑——少这一行你的测试分支一推生产环境就可能被悄悄改写。如果你现在正对着 Azure 门户发呆或者刚被同事问“那个 VNet 的地址段是多少我记得上周改过”又或者你的 Terraformplan输出里突然冒出一堆/-符号让你头皮发紧……那么接下来的内容就是为你写的。2. 核心设计思路为什么 Terraform 是 Azure 自动化的“最优解”而不是“又一个选择”在 Azure 生态里自动化工具从来不少ARM 模板、Bicep、PowerShell、Azure CLI甚至还有人用 Python SDK 硬撸。那为什么 Terraform 是我给所有新团队推荐的第一选择这不是因为它“最先进”而是因为它在可读性、跨云能力、状态管理、社区成熟度这四个维度上给出了一个极其务实的平衡点。我们来一层层剥开这个选择背后的硬逻辑。2.1 可读性HCL 不是代码是“基础设施说明书”很多人第一次看.tf文件会觉得它像代码。其实不然。HCLHashiCorp Configuration Language的设计哲学是让基础设施描述像一份清晰的说明书。对比一下 ARM 模板的 JSON{ type: Microsoft.Network/virtualNetworks, apiVersion: 2023-06-01, name: [parameters(vnetName)], location: [parameters(location)], properties: { addressSpace: { addressPrefixes: [[parameters(vnetAddressPrefix)]] } } }再看 Terraform 的 HCLresource azurerm_virtual_network main { name var.vnet_name location var.location resource_group_name azurerm_resource_group.rg.name address_space [var.vnet_address_prefix] }区别在哪ARM 模板里你得在嵌套的properties里找addressSpace再在addressSpace里找addressPrefixes全是键值对没有结构感。而 HCL 用缩进和块结构天然表达了“虚拟网络”这个资源的整体性它的名字、位置、所属资源组、地址空间都是这个资源的属性平级并列一目了然。变量var.vnet_name直接出现在赋值位置而不是藏在parameters()函数里。这种“所见即所得”的表达让一个没写过代码的网络工程师也能在十分钟内看懂一个 VNet 的定义并且能准确指出哪一行控制着子网的 CIDR。提示HCL 的赋值符是强制的不能省略。这是它和 YAML 的关键区别——YAML 靠缩进HCL 靠语法杜绝了因空格数不对导致的解析失败。我见过太多次因为复制粘贴时多了一个空格terraform validate直接报错而错误信息指向的是下一行让人排查半小时才发现是上一行的后面多了个空格。记住HCL 的是铁律不是装饰。2.2 跨云能力不是“为 Azure 而生”而是“为一切而生”Terraform 的azurermprovider 只是它庞大生态中的一个插件。它的核心引擎terraform本身是完全中立的。这意味着你今天用 Terraform 写的模块明天可以无缝迁移到 AWS 或 GCP只需要更换 provider 和微调几个参数。这听起来像画饼但在真实企业场景里它解决了两个致命痛点。第一个是技术债隔离。很多客户有混合云架构核心数据库在 AzureAI 训练在 AWS边缘计算在 GCP。如果每个云都用自己原生的工具ARM/Bicep、CloudFormation、Deployment Manager那你的 IaC 仓库就会变成三个互不相通的孤岛。而 Terraform 统一了语法、统一了工作流、统一了状态管理。你可以在同一个main.tf里同时声明一个 Azure SQL 数据库和一个 AWS S3 存储桶并用data源让它们互通——比如把 Azure Key Vault 里的密钥注入到 AWS Lambda 的环境变量里。这种“跨云编排”是任何单一云厂商的工具都无法提供的。第二个是人才复用。一个熟练的 Terraform 工程师上手 AWS 或 GCP 的成本极低。他不需要重新学习一套全新的模板语法、全新的 CLI 命令、全新的状态管理逻辑。他只需要查一下aws_instance的文档就知道怎么写 EC2 实例。这大大降低了团队的技术栈复杂度和招聘门槛。我服务过一家金融客户他们要求所有云资源必须通过 IaC 管控但内部团队对 Azure 最熟对 AWS 了解甚少。我们用 Terraform 统一了所有云的交付流程AWS 部分由外包团队负责但整个 CI/CD 流水线、审批流程、安全扫描规则全部复用 Azure 的那一套。上线后他们惊讶地发现AWS 环境的交付速度居然比以前纯手工还快。2.3 状态管理.tfstate是你的“基础设施账本”不是可有可无的文件这是 Terraform 和其他声明式工具如 Bicep最根本的区别。Bicep 编译成 ARM 模板后是“一次性”的你az deployment group create一次Azure 就按模板执行执行完模板就完成了它的使命。而 Terraform 的.tfstate文件是一个持续演进的“账本”。它记录了每一个资源在 Azure 中的真实 ID、当前配置、以及它和其他资源的依赖关系。举个例子你用 Terraform 创建了一个azurerm_public_ip.tfstate里会存下它的完整 Azure Resource ID比如/subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.Network/publicIPAddresses/my-pip。当你后续想修改这个公网 IP 的分配方式从Static改成DynamicTerraform 不是去猜、去查而是直接打开.tfstate找到这个 ID然后调用 Azure API精准地更新这个字段。它知道“这个资源就是它”而不是“名字叫 my-pip 的资源可能是它”。这个“账本”机制带来了两个不可替代的价值精准变更terraform plan能告诉你这次修改只会改动publicIpAllocationMethod这一个字段不会误删、误建其他资源。跨工具协作.tfstate是 Terraform 的“唯一真相源”。你可以用terraform import把一个已经存在的、手工创建的 Azure 资源导入到这个账本里让它从此被 Terraform 管理。这解决了“历史遗留资源如何纳入 IaC”的老大难问题。注意.tfstate文件绝对不能提交到 Git它里面可能包含敏感信息如资源 ID、部分配置元数据更重要的是它是二进制 JSONGit 无法做有意义的 diff。我见过最惨的一次事故一个团队把.tfstate提交到了公共仓库被爬虫抓取攻击者根据里面的资源 ID反向定位到客户的订阅 ID 和资源组名虽然没造成直接损失但触发了全公司级别的安全审计。正确做法是永远使用远程后端如 Azure Blob Storage并通过backend配置启用状态锁定state locking确保同一时间只有一个apply在执行。2.4 社区与生态不是“我能做什么”而是“别人已经帮你做好了什么”Terraform 的最大护城河是它背后那个由数百万开发者共同维护的模块Module宇宙。HashiCorp Registry 上有超过 20,000 个经过验证的、可用于生产的模块。这意味着你几乎不需要从零开始写一个 AKS 集群、一个 Cosmos DB、甚至一个完整的 Azure Landing Zone。比如你要部署一个高可用的 Azure Kubernetes Service (AKS) 集群你可以直接使用官方的azure/aks/azurerm模块module aks { source azure/aks/azurerm version 5.2.0 resource_group_name azurerm_resource_group.rg.name cluster_name my-prod-aks location azurerm_resource_group.rg.location dns_prefix my-prod-aks kubernetes_version 1.28.3 enable_rbac true enable_managed_identity true # 定义节点池 agent_pool_profiles [{ name systempool count 3 vm_size Standard_D4s_v3 os_disk_size 128 os_type Linux }] }这个模块内部已经帮你处理了VNet 和子网的创建与关联Log Analytics 工作区的集成Azure Monitor 的指标收集RBAC 权限的自动绑定甚至包括了节点池的自动伸缩Auto Scaling配置。你只需要关心“我要什么”而不用操心“怎么实现”。这极大地提升了交付效率和可靠性。我自己在为客户搭建灾备环境时就直接复用了azure/avset/azurerm模块来创建可用性集省去了手动计算容错域、更新域的繁琐步骤也避免了因计算错误导致的单点故障风险。3. 实操细节解析从零开始搭建一个“生产就绪”的 Terraform-Azure 环境纸上谈兵终觉浅。现在我们把键盘敲响从一个干净的 Windows 或 macOS 机器开始一步步搭建起一个真正能用于生产环境的 Terraform-Azure 工作流。这个过程我会严格遵循“最小可行、最大安全”的原则不跳过任何一个关键步骤也不引入任何花哨但不稳定的“黑科技”。3.1 环境准备安装、认证、权限三步定生死第一步安装 Terraform CLI。别去官网下.zip包手动解压那是上个时代的做法。现代运维用包管理器。macOS (Homebrew)brew tap hashicorp/tap brew install hashicorp/tap/terraformWindows (Chocolatey)choco install terraform安装完成后验证terraform version # 输出应为类似Terraform v1.6.6 # (后面跟着 provider 版本先忽略)第二步安装 Azure CLI。这是 Terraform 与 Azure 通信的“信使”。同样用包管理器macOSbrew update brew install azure-cliWindows (PowerShell as Admin)winget install -e --id Microsoft.AzureCLI安装后登录 Azureaz login这会打开浏览器让你选择账号并授权。授权成功后终端会显示你当前登录的订阅列表。关键一步来了确认你登录的是正确的订阅很多人在这里栽跟头。运行az account show检查输出中的id字段是否是你打算用来部署资源的订阅 ID。如果不是切换az account set --subscription your-subscription-id-here第三步也是最关键的一步创建服务主体Service Principal并授予最小权限。你绝不能用个人账号的 token 去跑terraform apply这等于把你的 Azure 账号密码明文写在了脚本里。服务主体是 Azure AD 中的一个“机器人账号”它有独立的凭据和权限。在终端中运行以下命令将your-subscription-id替换为你的实际 IDaz ad sp create-for-rbac \ --name terraform-sp-prod \ --role Contributor \ --scopes /subscriptions/your-subscription-id \ --sdk-auth注意--sdk-auth参数至关重要。它会让输出直接以 JSON 格式打印包含了clientId,clientSecret,tenantId,subscriptionId四个字段这正是 Terraform 所需的环境变量格式。你会看到一大段 JSON 输出形如{ clientId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, clientSecret: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy, subscriptionId: zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz, tenantId: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, activeDirectoryEndpointUrl: https://login.microsoftonline.com/, resourceManagerEndpointUrl: https://management.azure.com/, activeDirectoryGraphResourceId: https://graph.windows.net/, sqlManagementEndpointUrl: https://management.core.windows.net:8443/, galleryEndpointUrl: https://gallery.azure.com/, managementEndpointUrl: https://management.core.windows.net/ }立刻马上把这个 JSON 复制下来保存在一个安全的地方比如你的密码管理器。这是服务主体的“出生证明”丢了就只能重来。然后将其中的四个关键字段设置为环境变量export ARM_CLIENT_IDxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx export ARM_CLIENT_SECRETyyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy export ARM_TENANT_IDaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa export ARM_SUBSCRIPTION_IDzzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz提示在生产环境中这些变量绝不应该硬编码在你的 shell 配置文件如.zshrc里。你应该在每次运行 Terraform 前临时设置它们或者更推荐在 CI/CD 流水线中通过密钥管理服务如 GitHub Secrets, Azure Key Vault来注入。我曾经在一个客户项目中因为一个实习生把ARM_CLIENT_SECRET写在了~/.bash_profile里又被 IDE 的“同步配置”功能上传到了 GitHub Gist导致整个订阅的 Contributor 权限被泄露。那次事件后我们强制所有环境变量都通过流水线 Secret 注入。3.2 项目结构一个目录五种文件十年不重构一个健康的 Terraform 项目其目录结构本身就是一种文档。它清晰地告诉任何人“这里管什么那里管什么哪些是输入哪些是输出”。我坚持使用以下结构它经受住了多个大型项目的考验my-azure-project/ ├── main.tf # 主资源定义VNet, VM, AKS... 核心逻辑 ├── variables.tf # 所有变量声明类型、默认值、描述 ├── terraform.tfvars # 当前环境的具体值dev/prod 的 region, size... ├── outputs.tf # 对外暴露的输出VM 的公网 IP, AKS 的 FQDN... ├── backend.tf # 远程后端配置Azure Blob Storage 的连接信息 └── modules/ # 可选自定义模块存放目录 └── web-app/ ├── main.tf ├── variables.tf └── outputs.tf让我们逐个创建这些文件。首先初始化项目目录mkdir -p my-azure-project/modules/web-app cd my-azure-projectvariables.tf定义所有可配置项。这是你和“外部世界”沟通的接口。# variables.tf variable location { description The Azure region where resources will be deployed. type string default East US } variable environment { description The environment name (e.g., dev, prod). type string default dev } variable vm_size { description The size of the virtual machine. type string default Standard_B2s } variable admin_password { description The admin password for the Linux VM. For production, use Azure Key Vault. type string sensitive true # 这个标记很重要它会让 terraform plan 的输出隐藏此变量的值 }terraform.tfvars为当前环境填充具体值。这是一个“实例化”的过程。# terraform.tfvars location East US environment prod vm_size Standard_D4s_v3 admin_password MySuperSecurePassword!2024backend.tf配置远程状态存储。这是生产环境的基石。# backend.tf terraform { backend azurerm { resource_group_name my-terraform-state-rg storage_account_name mystorageterraformstate container_name tfstate key prod/terraform.tfstate } }注意backend配置中的resource_group_name,storage_account_name,container_name必须提前手动创建好。Terraform 的init命令不会帮你创建它们因为这涉及到初始的“鸡生蛋”问题。你需要先用 Azure CLI 或 Portal 创建好这个 Blob Storage 容器然后再运行terraform init。否则你会得到一个关于“存储账户不存在”的错误。我建议你用 CLI 创建az group create --name my-terraform-state-rg --location East US az storage account create --resource-group my-terraform-state-rg --name mystorageterraformstate --sku Standard_LRS --encryption-services blob az storage container create --name tfstate --account-name mystorageterraformstatemain.tf开始定义你的第一个资源——一个资源组。这是所有 Azure 资源的“家”。# main.tf provider azurerm { features {} } resource azurerm_resource_group main { name rg-${var.environment}-${var.location} location var.location tags { Environment var.environment ManagedBy Terraform } }outputs.tf定义这个项目对外“说”什么。# outputs.tf output resource_group_name { value azurerm_resource_group.main.name description The name of the created resource group. }现在你的项目骨架已经完成。运行terraform init它会下载azurermprovider 插件连接到你配置的 Azure Blob Storage 后端初始化远程状态文件首次为空。如果一切顺利你会看到Initializing the backend...和Successfully configured the backend!的绿色提示。恭喜你的 Terraform-Azure 环境已经活了。3.3 核心资源实战从 VNet 到 Web 应用一条链路走通现在我们来构建一个真实的、可访问的 Web 应用环境。它包含一个虚拟网络VNet、一个子网、一个公共 IP、一个网络接口NIC、一台 Linux 虚拟机VM以及一个预装 Nginx 的cloud-init脚本。这条链路覆盖了网络、计算、自动化配置三大核心领域。3.3.1 网络层VNet 与子网的“黄金比例”在main.tf的末尾添加 VNet 和子网的定义# main.tf (续) resource azurerm_virtual_network main { name vnet-${var.environment}-${var.location} address_space [10.0.0.0/16] location azurerm_resource_group.main.location resource_group_name azurerm_resource_group.main.name tags azurerm_resource_group.main.tags } resource azurerm_subnet web { name snet-web-${var.environment} resource_group_name azurerm_resource_group.main.name virtual_network_name azurerm_virtual_network.main.name address_prefixes [10.0.1.0/24] tags azurerm_resource_group.main.tags }这里的关键点是address_prefixes。10.0.1.0/24提供了 256 个 IP 地址10.0.1.0到10.0.1.255。其中.0是网络地址.255是广播地址.1通常被 Azure 保留为网关地址。所以你实际可用的 IP 是10.0.1.4到10.0.1.254共 251 个。对于一个小型 Web 应用这绰绰有余。如果你预计未来要部署几十台 VM那就需要更大的 CIDR比如/221024 个地址。3.3.2 计算层VM 的“安全启动”与cloud-init接着定义公共 IP、NIC 和 VM# main.tf (续) resource azurerm_public_ip vm { name pip-vm-${var.environment} location azurerm_resource_group.main.location resource_group_name azurerm_resource_group.main.name allocation_method Static # 生产环境务必用 Static否则 IP 会变 sku Standard # Standard SKU 支持区域冗余和更高的 SLA tags azurerm_resource_group.main.tags } resource azurerm_network_interface vm { name nic-vm-${var.environment} location azurerm_resource_group.main.location resource_group_name azurerm_resource_group.main.name ip_configuration { name internal subnet_id azurerm_subnet.web.id private_ip_address_allocation Dynamic public_ip_address_id azurerm_public_ip.vm.id } tags azurerm_resource_group.main.tags } resource azurerm_linux_virtual_machine web { name vm-web-${var.environment} location azurerm_resource_group.main.location resource_group_name azurerm_resource_group.main.name size var.vm_size admin_username azureuser admin_password var.admin_password disable_password_authentication false # 开发测试阶段先用密码生产务必设为 true 并用 SSH Key network_interface_ids [ azurerm_network_interface.vm.id ] os_disk { caching ReadWrite storage_account_type Standard_LRS } source_image_reference { publisher Canonical offer UbuntuServer sku 22.04-LTS version latest } # cloud-init 脚本在 VM 首次启动时执行 custom_data base64encode(EOF #cloud-config package_upgrade: true packages: - nginx runcmd: - systemctl start nginx - systemctl enable nginx - echo h1Hello from Terraform on Azure!/h1 /var/www/html/index.nginx-debian.html EOF ) }这段代码里有几个“血泪经验”disable_password_authentication false这是为了方便你快速验证。但在生产环境中必须将其设为true并配合ssh_keys块使用 SSH 密钥对。密码认证是最大的安全漏洞之一。custom_data使用base64encodeAzure 要求custom_data是 Base64 编码的字符串。EOF ... EOF是 HCL 的“Here Document”语法让你可以写多行脚本非常直观。package_upgrade: truecloud-init默认不会升级系统包。加上这一行可以确保在安装 Nginx 之前系统是最新的避免了因内核或库版本不匹配导致的安装失败。3.3.3 输出与验证让世界看到你的成果最后在outputs.tf中添加 VM 的公网 IP 和 URL# outputs.tf (续) output vm_public_ip { value azurerm_public_ip.vm.ip_address description The public IP address of the web VM. } output web_app_url { value http://${azurerm_public_ip.vm.ip_address} description The URL to access the Nginx web server. }现在执行terraform plan。你会看到一个长长的预览列出所有将要创建的资源。仔细检查确认没有destroy或update in-place的意外操作。然后执行terraform apply -auto-approve开发环境可加-auto-approve跳过确认生产环境务必去掉手动确认。等待几分钟当命令返回Apply complete! Resources: X added, 0 changed, 0 destroyed.时打开浏览器访问http://your-vm-public-ip。如果看到Hello from Terraform on Azure!恭喜你你已经用 Terraform 在 Azure 上成功部署了一个完整的、可工作的 Web 应用4. 实操过程详解从plan到apply每一步都在做什么terraform plan和terraform apply是 Terraform 工作流的两个心脏。理解它们内部发生了什么是避免灾难、提升效率的关键。这不仅仅是“看看要做什么”和“执行它”而是一个精密的状态同步与变更协调过程。4.1terraform plan一场严谨的“沙盘推演”当你运行terraform plan时Terraform 并不是在“猜测”它要做什么。它是在进行一场三重比对比对 A你的代码 (*.tf文件)—— 这是你声明的“理想国”。它定义了你希望 Azure 拥有的最终状态一个名为vm-web-prod的 VM位于East US大小为Standard_D4s_v3等等。比对 B当前状态 (terraform.tfstate)—— 这是你基础设施的“现状报告”。它记录了 Azure 中目前实际存在什么一个名为vm-web-dev的 VM位于West US大小为Standard_B2s。如果这是你第一次运行这个文件是空的意味着“现状是什么都没有”。比对 CAzure 的实时 API—— 这是 Terraform 的“侦察兵”。在plan阶段Terraform 会调用 Azure 的 Read API去查询那些在.tfstate中记录的资源确认它们是否真的还存在、配置是否被手动修改过。例如如果.tfstate里说有一个Standard_B2s的 VM但 Azure API 返回说这个 VM 已经被手动升级成了Standard_D4s_v3那么plan就会检测到这个“漂移Drift”并在输出中明确标出~符号表示“将要更新”。plan的输出就是这三重比对后的“作战地图”。它用三种符号来标记动作将要创建Create—— 代码里有状态里没有。-将要销毁Destroy—— 状态里有代码里没有。~将要更新Update in-place—— 状态里有代码里也有但配置不同。一个典型的plan输出片段如下Terraform will perform the following actions: # azurerm_linux_virtual_machine.web will be updated in-place ~ resource azurerm_linux_virtual_machine web { id /subscriptions/xxx/resourceGroups/rg-prod-East-US/providers/Microsoft.Compute/virtualMachines/vm-web-prod name vm-web-prod ~ size Standard_B2s - Standard_D4s_v3 # 这里会高亮显示变化 # (other attributes unchanged) } # azurerm_public_ip.vm will be created resource azurerm_public_ip vm { name pip-vm-prod location East US resource_group_name rg-prod-East-US allocation_method Static sku Standard }提示plan的输出默认是彩色的但如果你在 CI/CD 流水线中运行颜色会被禁用。此时-no-color参数就变得非常重要它能保证输出是纯文本便于日志分析和解析。我建议在所有自动化脚本中都显式加上terraform plan -no-color -outtfplan。4.2terraform apply一次原子性的“手术执行”apply不是简单地执行plan的指令。它是一个带有严格事务性和依赖感知的执行引擎。事务性AtomicityTerraform 的apply是一个“全有或全无”的操作。如果在创建 10 个资源的过程中第 7 个失败了比如因为配额不足Terraform 会尝试回滚Rollback前面已经成功创建的 6 个资源。当然回滚本身也可能失败比如VNet 创建成功了但删除它时遇到依赖这时你就需要手动清理。因此apply前的plan审查是防止这种“半成品”状态的最重要防线。依赖感知Dependency GraphTerraform 会自动分析你的代码构建一个资源依赖图。它知道azurerm_network_interface.vm依赖于azurerm_subnet.web而azurerm_linux_virtual_machine.web又依赖于azurerm_network_interface.vm。因此它会严格按照VNet - Subnet - NIC - VM的顺序执行创建。你不需要也不应该手动指定depends_on除非是极少数的、Terraform 无法自动推断的隐式依赖比如一个资源需要另一个资源的输出作为输入但这个输入不是通过resource.id这种标准方式引用的。apply的执行过程会在终端中实时打印进度azurerm_resource_group.main: Creating... azurerm_resource_group.main: Creation complete after 2s [id/subscriptions/xxx/...] azurerm_virtual_network.main: Creating... azurerm_virtual_network.main: Creation complete after 8s [id/subscriptions/xxx/...] ...每一行都代表一个资源的生命周期。Creation complete后面的[id...]就是该资源在 Azure 中的唯一标识符它会被立即写入.tfstate文件。4.3 状态文件的“心跳”.tfstate如何被实时更新.tfstate文件是apply过程的“副产品”也是其最重要的成果。它不是一个静态快照而是一个动态的、增量更新的数据库。当apply成功创建一个资源后Terraform 会立即将该资源的最新状态ID、所有可读取的属性序列化为 JSON并追加或更新到.tfstate文件中。这个过程是原子的要么整个更新成功要么整个失败不会出现.tfstate文件损坏或不一致的情况。你可以用terraform state list查看当前状态中所有资源的路径$