4. 使用terraform plan进行漂移预检

在上一节中,我们已经理解了配置漂移的成因与危害。真正进入修复流程时,第一步并不是急着改代码或改云资源,而是先做一次"体检"——用 terraform plan 把当前真实基础设施与 Terraform 状态、配置三者之间的差异完整地暴露出来。plan 的输出本质上是一份"变更提案",它既包含我们主动修改配置后产生的预期变更,也包含别人在控制台手动改动资源后产生的意外漂移。能否读懂这份提案,直接决定了后续修复策略是否正确。

plan 的三方对比模型

要理解 plan 的输出,必须先明确它比较的是哪三方:配置文件(.tf)描述期望状态,状态文件(terraform.tfstate)记录上一次 apply 后的已知状态,真实云资源是实际存在的对象。Terraform 在 plan 阶段会先通过 refresh 把真实资源的最新属性回写到内存中的状态视图,再拿这份"刷新后的状态"与配置文件对比,最终生成变更计划。因此,漂移检测的关键就在于:refresh 让真实资源与状态文件之间的差异浮出水面,而 plan 的 diff 又让这份差异与配置期望之间的差距变得可见。

terraform plan 的三方对比与漂移识别 配置文件 .tf 期望状态 desired instance_type = t3.small 状态文件 .tfstate 上次 apply 记录 instance_type = t3.small 真实云资源 控制台实际值 instance_type = t3.medium refresh 回写 刷新后状态视图 instance_type = t3.medium plan 输出:变更提案 预期变更 配置主动修改引起 ~ update / + create 意外漂移 控制台手动改动引起 ~ update 回退到配置值 refresh 暴露真实差异 → plan diff 区分预期与意外 → 决定修复方向

案例背景与初始状态

假设我们维护一套电商演示环境,其中一台 Web 服务器由如下配置管理:

resource "aws_instance" "web" {
  ami           = "ami-0abc123"
  instance_type = "t3.small"
  tags = {
    Name        = "web-prod"
    Environment = "production"
  }
}

resource "aws_security_group" "web_sg" {
  name = "web-sg"
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

上周 apply 之后一切正常。本周运维同事为了临时压测,在控制台把实例规格改成了 t3.medium,并给安全组额外开放了 8080 端口,事后忘记还原。同时,我们自己为了统一命名规范,把配置文件里的 Name 标签从 web-prod 改成了 web-prod-01。现在执行 plan,会同时看到两类变更。

解读 plan 输出

$ terraform plan

Terraform will perform the following actions:

  # aws_instance.web will be updated in-place
  ~ resource "aws_instance" "web" {
      ~ instance_type = "t3.medium" -> "t3.small"
      ~ tags = {
          ~ "Name" = "web-prod" -> "web-prod-01"
        }
        # (其他属性不变)
    }

  # aws_security_group.web_sg will be updated in-place
  ~ resource "aws_security_group" "web_sg" {
      ~ ingress = [
          - {
              from_port = 8080
              to_port   = 8080
              protocol  = "tcp"
              cidr_blocks = ["0.0.0.0/0"]
            },
          + {
              from_port = 80
              to_port   = 80
              protocol  = "tcp"
              cidr_blocks = ["0.0.0.0/0"]
            },
        ]
    }

Plan: 0 to add, 2 to change, 0 to destroy.

逐条分析这份输出:

  • instance_type 的 ~ 变更:当前真实值是 t3.medium,配置期望是 t3.small。配置从未改过规格,所以这是典型的意外漂移——有人手动改了资源。plan 给出的动作是把实例改回 t3.small。
  • Name 标签的 ~ 变更:真实值是 web-prod,配置期望是 web-prod-01。这是我们主动修改配置造成的预期变更,应当保留。
  • 安全组 ingress 的增删:真实规则里多了一条 8080 端口,配置里没有。plan 会删除这条多余规则并恢复 80 端口规则。8080 是手动添加的,属于意外漂移。

这里有一个关键判断点:同一个 ~ 符号既可能代表预期变更,也可能代表意外漂移,不能只看符号,必须结合"这个属性最近有没有被我们改过配置"来判断。如果团队有 Git 提交记录,可以对照最近一次 commit 的 .tf 变更;如果没有,就要靠对资源用途的理解来推断。

判断技巧:把 plan 中每个变更属性与最近一次 Git 提交的配置 diff 做对照。配置 diff 里出现过的属性 → 预期变更;配置 diff 里没出现、但 plan 里出现的属性 → 意外漂移。

制定初步修复策略

识别出两类变更后,修复策略要分开处理,不能一把梭 terraform apply 了事。

对于预期变更(Name 标签):直接保留,apply 时会正常写入。这类变更不需要额外操作,但要注意它可能和漂移变更混在同一个资源里,apply 时会一起执行。

对于意外漂移(instance_type、8080 端口):需要先判断"手动改动是否应该保留"。这里有两种情况:

  1. 改动是临时的、应该回退:比如压测结束后忘记改回规格,8080 端口是调试时临时开的。此时策略是让 Terraform 把资源改回配置期望值,即直接 apply。但要注意,如果实例规格从 t3.medium 改回 t3.small 需要停机,应安排在维护窗口执行。
  2. 改动是合理的、应该固化到配置:比如团队决定长期使用 t3.medium,或 8080 端口确实需要长期开放。此时不能直接 apply(否则会把合理改动冲掉),而应该先修改 .tf 文件,把新值写进配置,再 apply。这样配置与真实资源重新对齐,漂移被"吸收"为正式配置。

在本案例中,压测已结束、8080 是临时调试端口,因此选择回退策略。但回退前建议先做一次 terraform plan -out=tfplan 把计划保存下来,便于复核和审计:

terraform plan -out=drift-fix.tfplan
terraform show -json drift-fix.tfplan | jq '.resource_changes[] | {address, actions: .change.actions}'

用 terraform show -json 可以把 plan 转成结构化 JSON,配合 jq 快速筛出所有 actions 为 ["update"] 的资源,方便在 CI 中做自动化漂移告警。例如在流水线里加一条判断:如果 plan 输出中出现了非预期的 update,就阻断部署并通知负责人。

修复前的检查清单

检查项 本案例情况 处理动作
变更是否来自配置修改 Name 标签来自配置修改 保留,正常 apply
变更是否来自控制台手动操作 instance_type、8080 端口 确认后回退或固化
回退是否会造成停机 实例规格变更需重启 安排维护窗口
是否有依赖该漂移的上下游 8080 端口可能被监控引用 先确认再删除
plan 是否已保存备查 尚未保存 执行 plan -out 保存
注意:不要在看到 plan 有变更时直接 terraform apply -auto-approve。自动批准会跳过人工确认,一旦漂移中包含应该保留的合理改动,就会被静默覆盖,造成生产事故。漂移修复场景下,始终建议人工审阅 plan 输出后再执行。

完成上述判断后,我们就得到了一个明确的修复方向:保留 Name 标签变更,回退 instance_type 和 8080 端口。下一步才是真正执行 apply 并验证结果。plan 预检的价值就在于,它把"改什么、为什么改、改了会怎样"提前摊开在面前,让修复从盲目操作变成有据可依的决策。