10.1 IaC:状态、依赖图与安全变更
生产平台运行在 K8s 上,但你发现另一个问题:所有底层基础设施,虚拟机、负载均衡器、DNS 记录、数据库,都是手动创建的。
基础设施即代码(Infrastructure as Code,IaC)的价值,不是把控制台点击翻译成一大段 HCL。它真正建立的是一套可重复的变更协议:配置描述期望状态,工具读取真实状态,生成差异计划,经过评审后再执行。
本课以 Terraform 语义讲解,OpenTofu 的核心配置、状态与工作流与之相近。具体 provider 和版本仍应以项目锁文件及官方文档为准。
三种状态不能混为一谈
一次 IaC 变更至少涉及三种状态:
| 状态 | 来源 | 作用 |
|---|---|---|
| 配置状态 | Git 中的 HCL | 团队声明想要什么 |
| 记录状态 | terraform.tfstate 或远程后端 | 把资源地址绑定到真实对象 |
| 真实状态 | 云 API、Kubernetes API 等 | 此刻实际存在什么 |
Terraform 的 state 不是资源清单缓存。它保存 aws_instance.api[0] 与某个云实例 ID 的绑定,也可能包含资源属性和敏感值。删掉 state 不会自动删掉云资源,却会让工具失去对它们的管理关系。
因此不要把 state 提交到 Git,也不要手工编辑 state JSON。生产环境应使用支持访问控制、加密、版本恢复和锁定的远程后端,并把 state 当成敏感资产备份和审计。
配置先形成依赖图
下面是已配置 AWS provider、区域、版本约束和授权凭据的项目片段,不是可直接部署的完整项目。资源引用会产生依赖边,而不是依赖文件顺序:
resource "aws_vpc" "main" {
cidr_block = "10.20.0.0/16"
}
resource "aws_subnet" "app" {
vpc_id = aws_vpc.main.id
cidr_block = "10.20.10.0/24"
}aws_subnet.app 读取 VPC 的 ID,所以 Terraform 能推导创建顺序。只有工具无法从数据流看出约束时,才考虑 depends_on;滥用显式依赖会扩大未知值传播,使计划更保守、更难读。
Plan 是一次有上下文的快照
在对应项目目录中先生成并查看计划:
terraform init
terraform fmt -check
terraform validate
terraform plan -out=tfplan
terraform show tfplan评审通过后才执行 terraform apply tfplan。传入保存的计划就被视为批准,命令不会再次询问确认;应核对执行环境、计划来源和生成时间。
validate 只检查配置内部是否有效,不证明权限、配额和远端 API 一定允许执行。plan 通常会刷新已管理对象,然后比较配置、state 和远端结果;它也不是永不过期的承诺。计划生成后若凭据、配额、外部对象或 provider 行为发生变化,apply 仍可能失败。
在 CI 中可用 terraform plan -detailed-exitcode 区分结果:
0:成功且无差异;1:执行错误;2:成功且存在差异。
退出码 2 是有变更,不是执行失败;使用 set -e 的 CI 脚本要显式处理它。保存的计划会保留敏感值,即使终端输出已遮蔽,也不能把文件当公共 artifact。
远程状态与锁解决不同问题
远程后端让协作者读取同一份最新 state;状态锁阻止两个写操作同时修改同一 state。两者都需要,但锁不能替代代码评审,也不能阻止另一个人绕过 IaC 去控制台改资源。
下面的 S3 原生锁配置要求 Terraform 1.10 或更高版本。bucket 必须在 init 前已存在,不能指望同一份尚未初始化的配置先把它创建出来;示例名称和区域应替换成团队拥有的实际资源:
terraform {
backend "s3" {
bucket = "atlas-terraform-state"
key = "prod/network/terraform.tfstate"
region = "ap-east-1"
encrypt = true
use_lockfile = true
}
}S3 bucket 还应启用版本控制,并对 state 与 .tflock 分别授予最小权限。过去常见的 dynamodb_table 锁正在废弃,不应再作为新项目的默认答案。
锁异常时先调查持锁者,确认本机、CI 和其他执行端都没有仍在运行的 apply,再检查后端状态。force-unlock 是事故处置工具,不是“卡住就解锁”的日常操作。
模块抽象业务能力,而不是包装每个资源
一个可复用模块通常包含:
modules/service-network/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
└── README.md好的模块表达“一个带私有子网和必要路由的服务网络”,而不是机械地给 aws_subnet 再包一层。输入应有类型、描述和必要校验;输出只暴露调用方真正需要的契约;provider 版本要受约束,并提交记录所选版本和校验和的 .terraform.lock.hcl。这个锁文件不锁 module;远程模块要另行固定精确版本或源代码提交。
不要用一个巨型 state 管理公司所有资源。state 边界同时决定故障半径、权限边界和并发能力。常见划分依据是环境、生命周期和团队所有权,而不是单纯按资源类型切碎。
导入、移动与删除是三种不同操作
已存在资源接入 IaC 时,先写目标资源配置,再声明导入关系。以下片段假设 aws_s3_bucket.audit 已在当前项目声明,bucket 也已存在且归团队管理:
import {
to = aws_s3_bucket.audit
id = "atlas-prod-audit"
}重命名或迁移模块地址时,用 moved block 记录地址变化,避免 Terraform 把“改名”误判为删旧建新。下面假设新模块中的目标资源已声明,旧地址确实存在于 state:
moved {
from = aws_s3_bucket.logs
to = module.audit.aws_s3_bucket.this
}state rm 只让工具忘记对象,不删除远端资源;若配置仍在,下一次计划可能尝试重建,甚至撞上原对象的名称。destroy 或配置移除则可能真实删除对象。prevent_destroy 只在相应资源配置仍存在时生效:把整个资源块删掉,也就删掉了这道保护。数据库、密钥和状态桶还需要服务端删除保护、备份和相应审批。
漂移需要分类处理
当控制台热修或外部控制器改变资源,下一次计划会看到 drift。处理方式取决于所有权:
- 未授权漂移:让 IaC 恢复声明状态,并追查绕过流程的权限路径;
- 合理的事故热修:先恢复服务,再把真实变更回写配置;
- 外部控制器拥有的字段:重新划分所有权,必要时对精确属性使用
ignore_changes; - 只想更新记录:评审
plan -refresh-only并应用该计划。它更新 state,不改 HCL;若要长期接受变化,还要修改配置,否则下一次普通计划仍可能要求改回去。
宽泛的 ignore_changes = all 会跳过属性更新,但仍允许创建和删除,既不是持续管理,也不是防删开关。
一条可审计的生产变更链
需求与风险说明
→ 修改配置和测试
→ CI 格式/校验/策略检查
→ 生成计划并标注删除、替换和权限扩大
→ 代码所有者与环境所有者审批
→ 使用同一份已批准计划执行
→ 验证服务与成本信号
→ 保存审计记录IaC 管的是资源生命周期。下一课把应用部署交给持续协调器,让 Git 中的期望状态能够被反复收敛,而不是只执行一次脚本。
参考资料
HashiCorp Terraform, State
HashiCorp Terraform, S3 backend
HashiCorp Terraform,
terraform planHashiCorp Terraform, Standard module structure
OpenTofu, State storage and locking
HashiCorp Terraform, Saved-plan apply
HashiCorp Terraform, Dependency lock file
HashiCorp Terraform, Lifecycle rules