小Cの已经记不起来的博客

用 Terraform 管理云资源时状态文件拆分与协作规范

Terraform 这玩意儿,一个人用的时候挺爽,apply 一敲,资源就起来了;一旦变成多人协作,最先出问题的往往不是模块写得烂,而是 state 文件没管好。我见过一个 state 里塞了网络、数据库、K8s、监控、IAM 的,plan 一次要等好几分钟,两个人同时改还得抢锁,最离谱的是有次误删了一个安全组,结果半个环境跟着遭殃。所以 state 拆分和协作规范这事儿,真不是“以后再说”的优化项,而是上生产前就得定下来的底线。

先说为什么别把鸡蛋放一个 state 里

Terraform 的 state 记录的是配置和真实资源的映射关系。state 越大,plan 和 refresh 越慢,锁竞争越明显,爆炸半径也越大。更麻烦的是权限边界:应用团队其实不需要碰网络账号里的 state,但你如果全塞一个文件里,最后只能给所有人一把大钥匙。

但也不能走另一个极端,按每个资源拆一个 state。拆太碎以后,资源之间的依赖、输出引用、执行顺序都会变成灾难,改一个 VPC 得跑五个目录。我的经验是:先按环境拆,再按业务域拆,最后看生命周期和权限边界。比如 prod 和 dev 肯定分开;prod 里面再把网络、数据、应用、监控拆开。数据库这种生命周期长、变更风险高的,单独一个 state;临时实验资源就没必要拆那么细。

backend 和目录结构长什么样

我一般用“模块目录 + 环境目录”的结构,模块放公共逻辑,环境目录是 root module,各自有独立的 backend key。大概长这样:

infra/
  modules/
    network/
    app/
    database/
  live/
    prod/
      network/
        backend.tf
        main.tf
        outputs.tf
      app/
        backend.tf
        main.tf
    dev/
      network/
      app/

backend.tf 里把 key 写清楚,别用默认的 terraform.tfstate。比如用 S3 + DynamoDB 锁:

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

key 的命名我习惯按 环境/业务域/组件/terraform.tfstate 来,一眼能看出来这个 state 管什么。还有几个小习惯:本地 state 不要进 Git,.gitignore 里加上 *.tfstate、*.tfstate.*、.terraform/;.terraform.lock.hcl 要提交,Terraform 版本用 required_version 或 .terraform-version 固定住。别小看这些,换个人电脑上跑出 provider 版本不一致,plan 能给你整出一堆莫名其妙的 diff。

workspace 不是不能用,但别拿它硬扛生产隔离

Terraform workspace 适合临时环境、测试环境,或者同一套配置下快速切几个相似环境。但它不太适合做生产隔离:同一个 backend、同一套配置,切 workspace 全靠人记,权限也很难按 workspace 切得很细。要是有人 terraform workspace select prod 完忘了切回来,后面就刺激了。

所以我的做法是:生产、预发、开发用不同目录和不同 backend key,强隔离;workspace 只留给个人临时环境和一次性测试。如果云账号能分,生产最好单独账号,至少也要单独的项目/订阅和权限策略。state 里可能包含不少敏感信息,别指望它“只是个状态文件”。

拆 state 时怎么搬资源

拆分不是新建一个目录就完事,关键是资源不能丢,也不能让旧目录下次 plan 时想重新创建一份。动手前先备份:

terraform state pull > prod-network-backup-$(date +%F).json

跨 backend 搬资源,我一般用比较笨但稳的办法:新目录先把配置写好,用 terraform import 把现有资源接管进来,确认 plan 没有变化;然后在旧目录把对应资源从 state 里移除,最后删掉旧目录里的资源块。顺序别反,反了旧目录可能计划新建重复资源。

# 在新目录里接管已有资源
terraform import aws_vpc.main vpc-xxxxxxxx

# 确认新目录 plan 没有 diff 后,在旧目录里移除 state 记录
terraform state rm aws_vpc.main

如果只是同一 backend 下改资源地址,可以用 terraform state mv,但跨 state 的搬迁别过度依赖花活。moved block 适合改资源地址,不负责跨 state 搬家。不管用哪种方式,搬完一定跑 plan,看到 No changes 再合并。别问我怎么知道的,早年一次拆分没验证,旧目录差点把 RDS 当新资源建出来。

协作规范:把“能跑”变成“别人也能安全地跑”

state 拆分只是第一步,协作规范才是长期不翻车的关键。我们现在的规矩大概这几条:

  1. 禁止手改 state 文件,也不要把 state 内容贴到 issue 里。需要改地址用 state mv,需要接管用 import,需要删记录用 state rm,全部走命令并留记录。

  2. 所有变更走 PR。PR 里至少包含 terraform fmt、validate 和 plan 结果。CI 里可以跑:

    terraform fmt -check -recursive
    terraform init -input=false
    terraform validate
    terraform plan -out=tfplan
    

    apply 只从审批后的 plan 执行,别在本地随手 apply 生产。

  3. 同一个 state 同时只允许一个变更。Terraform 默认会加锁,除非救火,不要 -lock=false。如果锁住了,先查是谁在跑,别上来就强拆。

  4. 状态文件权限最小化。backend 加密、访问审计、按环境分权限。state 里可能有密码、连接串、证书,别给所有人读权限。

  5. 定期 drift 检测。生产 state 最好定时跑只读 plan,发现有人控制台手改资源就及时处理。别等下次发布才发现配置和现实对不上。

  6. 每个 state 有负责人和文档。写清楚它管哪些资源、依赖哪个 state 的输出、怎么执行、出问题找谁。依赖尽量通过 outputs 或 data source,别互相读整个 state 然后乱引用,很容易绕成循环。

  7. 模块版本固定。公共模块用 Git tag 或版本号引用,别直接跟主干,不然别人合并个模块改动,可能把你没打算动的环境一起改了。

最后再说一句,state 拆分没有标准答案,粒度合适比“看起来优雅”重要。拆到团队能独立变更、权限能收住、plan 时间能接受,基本就够了。剩下的就是老老实实走流程,别嫌麻烦——真出一次事故,后面补流程更麻烦。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放