2. Terraform核心工作流回顾

在本地终端里,Terraform 的四个核心命令 init、plan、apply、destroy 看起来只是顺序执行的四个动作。但一旦把它们放进 CI/CD 流水线,每个命令的语义、执行环境、权限边界和可观测性都会发生明显变化。理解这些差异,是设计可靠交付流水线的前提。

一、四个命令在本地与流水线中的行为差异

terraform init 在本地通常只做一次,负责下载 provider 插件、初始化后端、拉取模块。在 CI/CD 中,它几乎每次流水线运行都要执行,因为 Runner 是无状态的一次性容器。关键差异在于:init 必须能访问后端存储和模块源。如果后端是 S3 + DynamoDB 锁,Runner 就需要对应的 IAM 权限;如果模块来自私有 Git 仓库,就需要注入 SSH key 或访问令牌。init 失败往往不是代码问题,而是凭据或网络策略问题。

terraform plan 在本地是交互式确认前的预览。在流水线中,plan 的产物——执行计划文件——成为流水线内部的一等交付物。它把“将要做什么”固化成二进制文件,使后续 apply 不再重新计算。这一点在 CI/CD 中极其重要:plan 和 apply 可能运行在不同的 Runner、不同的时间点,甚至由不同的人审批。没有计划文件,apply 就会重新读取真实基础设施并重新计算,导致审批的内容与实际执行的内容不一致。

terraform apply 在本地默认会再次显示计划并等待输入 yes。在流水线中必须使用 -auto-approve,并且通常配合 -input=false 禁止任何交互。更关键的是,apply 应该消费 plan 阶段生成的计划文件,而不是重新 plan。这样做的直接收益是:审批人看到的 diff 与最终执行的变更严格一致。

terraform destroy 在本地是清理手段。在 CI/CD 中,destroy 通常被封装成独立的、受严格保护的流水线,例如仅允许在特定分支、特定标签或手动触发时运行。它不应该出现在常规的 apply 流水线中,否则一次误合并就可能删除生产环境。destroy 同样需要状态文件和后端锁,并且它的执行计划也应该被保存和审批。

二、状态文件:流水线中的共享事实源

状态文件记录了 Terraform 所管理的真实资源与配置之间的映射关系。在 CI/CD 中,状态文件必须存放在远程后端,而不是 Runner 的本地磁盘。原因有三:第一,多个流水线可能并发运行,本地状态无法共享;第二,Runner 销毁后本地状态丢失,下次运行会认为所有资源都不存在;第三,远程后端配合锁机制可以防止并发写冲突。

以 S3 后端为例,典型配置如下:

terraform {
  backend "s3" {
    bucket         = "my-terraform-state-prod"
    key            = "network/terraform.tfstate"
    region         = "ap-southeast-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

其中 dynamodb_table 提供状态锁。当一条流水线正在 apply 时,另一条流水线的 plan 会因无法获取锁而等待或失败。这个行为在 CI/CD 中既是保护,也是需要处理的异常路径:流水线应该设置合理的重试或超时,而不是直接报错退出。

状态文件还承担着敏感信息载体的角色。很多 provider 会把密码、密钥、连接字符串写入状态。因此远程后端必须开启加密,并且访问权限要严格限制。在流水线中,这意味着 Runner 的角色只能访问它负责的那一个 state key,而不是整个 bucket。

三、执行计划:审批与执行之间的契约

执行计划文件是 plan 命令的输出,通常以 -out 参数生成:

terraform plan -input=false -out=tfplan
terraform show -json tfplan > tfplan.json
terraform apply -input=false tfplan

在 CI/CD 中,tfplan 作为流水线工件被上传保存,tfplan.json 则可以被策略引擎或人工审批界面解析。审批人看到的不是一段文字描述,而是结构化的资源变更列表。apply 阶段直接消费 tfplan,不再重新计算。这样就形成了“plan 一次、审批一次、apply 一次”的契约。

需要特别注意的是,计划文件具有时效性。如果 plan 和 apply 之间真实基础设施发生了变化,apply 可能会失败或产生非预期结果。因此流水线应该尽量缩短 plan 与 apply 之间的间隔,或者在 apply 前重新校验状态锁和资源版本。

不要在没有计划文件的情况下直接运行 terraform apply -auto-approve。这会让 apply 重新计算变更,审批环节看到的 diff 与实际执行的 diff 可能完全不同,是生产事故的常见来源。

四、CI/CD 语境下的命令职责划分

把四个命令映射到流水线阶段,可以得到一个清晰的职责模型:init 属于准备阶段,负责环境就绪;plan 属于验证与审批阶段,负责生成可审阅的变更契约;apply 属于执行阶段,负责消费契约并落地;destroy 属于受控清理阶段,独立于常规交付路径。状态文件贯穿始终,是四个命令共享的事实源;执行计划则是 plan 与 apply 之间的契约载体。

Terraform 核心命令在 CI/CD 中的职责与数据流 准备阶段 验证与审批阶段 执行阶段 受控清理阶段 terraform init 下载 provider / 初始化后端 拉取模块 / 配置锁表 terraform plan 读取状态 + 真实资源 生成执行计划文件 terraform apply 消费计划文件 -auto-approve -input=false terraform destroy 独立流水线 / 手动触发 同样需要计划与审批 远程状态文件(Remote State) S3 / GCS / Terraform Cloud + DynamoDB 锁 资源映射 · 敏感信息 · 并发锁 初始化后端 读取状态 写入状态 读取/更新状态 执行计划文件(tfplan) plan 生成 → 审批审阅 → apply 消费,保证 diff 一致 实线:命令执行路径 虚线:状态/计划数据流 橙色虚线:计划契约传递

五、一个可参考的流水线命令序列

下面给出一个适用于生产环境的命令序列示例,假设使用 S3 后端和 DynamoDB 锁,Runner 已通过 OIDC 获取临时凭据:

# 准备阶段
terraform init -input=false -backend-config=backend-prod.hcl

# 验证与审批阶段
terraform validate
terraform plan -input=false -lock-timeout=5m -out=tfplan
terraform show -json tfplan > tfplan.json

# 审批通过后,执行阶段
terraform apply -input=false -lock-timeout=5m tfplan

其中 -lock-timeout=5m 表示如果状态锁被占用,最多等待 5 分钟再失败。这在多条流水线可能并发的环境中比立即失败更实用。terraform validate 不访问远程状态,适合在 plan 之前快速拦截语法和类型错误,减少无效的 plan 执行。

对于 destroy,建议单独建立一条流水线,触发条件设置为手动或特定标签,并且同样执行 plan 和审批:

terraform plan -destroy -input=false -out=destroy.tfplan
terraform apply -input=false destroy.tfplan

注意 -destroy 标志让 plan 生成的是销毁计划,而不是常规变更计划。审批人看到的将是资源删除列表,而不是创建或修改列表。

在 CI/CD 中,把 plan 的输出保存为工件(artifact),并保留至少一个发布周期。当 apply 出现异常时,可以回溯当时的计划内容,判断是配置问题、状态漂移还是权限问题。

六、常见误区与判断依据

第一个误区是认为 init 只需要跑一次。在无状态 Runner 中,每次流水线都必须重新 init,否则 provider 插件和后端配置都不存在。判断依据很简单:如果流水线日志里出现 “Backend initialization required” 或 “provider plugin not found”,就是 init 缺失或失败。

第二个误区是 plan 和 apply 之间没有传递计划文件。这种情况下,apply 会重新计算,审批看到的 diff 可能已经过期。判断依据是 apply 日志中是否出现 “Refreshing state” 以及是否重新显示了 plan 摘要。如果 apply 重新 plan,说明没有消费计划文件。

第三个误区是状态文件放在本地或 Runner 临时目录。判断依据是第二次流水线运行时是否出现大量资源“将被创建”,而实际上资源已经存在。这通常意味着状态文件丢失,Terraform 无法建立映射。

把 init、plan、apply、destroy 在 CI/CD 中的行为差异、状态文件的共享事实源角色、执行计划的契约作用这三件事串起来,就能理解为什么流水线不能简单地把本地命令按顺序搬上去。每一个命令在流水线中都需要明确的输入、输出、权限和异常处理策略,而状态文件和执行计划正是连接这些策略的两个关键载体。