跳到内容

第 7 卷 / 第 10 章 / 第 1 课

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、区域、版本约束和授权凭据的项目片段,不是可直接部署的完整项目。资源引用会产生依赖边,而不是依赖文件顺序:

hcl
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 是一次有上下文的快照 ​

在对应项目目录中先生成并查看计划:

bash
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 前已存在,不能指望同一份尚未初始化的配置先把它创建出来;示例名称和区域应替换成团队拥有的实际资源:

hcl
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 是事故处置工具,不是“卡住就解锁”的日常操作。

模块抽象业务能力,而不是包装每个资源 ​

一个可复用模块通常包含:

text
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 也已存在且归团队管理:

hcl
import {
  to = aws_s3_bucket.audit
  id = "atlas-prod-audit"
}

重命名或迁移模块地址时,用 moved block 记录地址变化,避免 Terraform 把“改名”误判为删旧建新。下面假设新模块中的目标资源已声明,旧地址确实存在于 state:

hcl
moved {
  from = aws_s3_bucket.logs
  to   = module.audit.aws_s3_bucket.this
}

state rm 只让工具忘记对象,不删除远端资源;若配置仍在,下一次计划可能尝试重建,甚至撞上原对象的名称。destroy 或配置移除则可能真实删除对象。prevent_destroy 只在相应资源配置仍存在时生效:把整个资源块删掉,也就删掉了这道保护。数据库、密钥和状态桶还需要服务端删除保护、备份和相应审批。

漂移需要分类处理 ​

当控制台热修或外部控制器改变资源,下一次计划会看到 drift。处理方式取决于所有权:

  1. 未授权漂移:让 IaC 恢复声明状态,并追查绕过流程的权限路径;
  2. 合理的事故热修:先恢复服务,再把真实变更回写配置;
  3. 外部控制器拥有的字段:重新划分所有权,必要时对精确属性使用 ignore_changes;
  4. 只想更新记录:评审 plan -refresh-only 并应用该计划。它更新 state,不改 HCL;若要长期接受变化,还要修改配置,否则下一次普通计划仍可能要求改回去。

宽泛的 ignore_changes = all 会跳过属性更新,但仍允许创建和删除,既不是持续管理,也不是防删开关。

一条可审计的生产变更链 ​

text
需求与风险说明
  → 修改配置和测试
  → CI 格式/校验/策略检查
  → 生成计划并标注删除、替换和权限扩大
  → 代码所有者与环境所有者审批
  → 使用同一份已批准计划执行
  → 验证服务与成本信号
  → 保存审计记录

IaC 管的是资源生命周期。下一课把应用部署交给持续协调器,让 Git 中的期望状态能够被反复收敛,而不是只执行一次脚本。

参考资料 ​

Built with VitePress | Software Systems Atlas