工作区不是环境隔离工具
一句话总结
workspace 是在同一个后端、同一套凭据之内,只保留多份状态的机制。如果环境的分界点在于权限,就不能用 workspace 来划分。
为什么需要它,又为什么被误解
划分环境的工作几乎总是这样开始的:在 dev 上创建的栈也要部署到 prod,但又不想把代码整个复制一份。这时 workspace new prod 看起来是个完美的答案。代码只有一份,一条命令就能切换,provider 缓存也不需要重新下载。
问题在于这个工具到底解决了什么。workspace 所隔开的只有状态(state)。后端仍然只有一个,打开这个后端的凭据也只有一套,代码也只有一份。所以如果让只应被授予 dev 权限的人使用 workspace,这个人其实已经拿着能打开存放 prod 状态的同一个仓库的钥匙了。
官方文档也是这么说的。OpenTofu 的 Workspaces 文档写明,workspace 并不适合用于系统分解,或需要独立凭据和访问控制的部署,并建议对更大的系统,应按架构边界拆分配置本身。也就是说,workspace 不是环境隔离工具,而是用来创建同一环境的临时副本的工具。文档举的典型例子,也是为功能分支创建的临时副本。
工作原理
在 local 后端上究竟发生了什么,文件布局已经说明了一切。
tfa-ws/
├── terraform.tfstate ← default workspace
├── terraform.tfstate.d/
│ ├── dev/terraform.tfstate
│ └── prod/terraform.tfstate
└── .terraform/
├── providers/ ← 캐시는 한 벌뿐이다
└── environment ← 지금 선택된 이름이 여기에만 있다
需要读懂三点。
- 只有默认 workspace 的路径不同。
default的状态仍然和以前一样位于工作目录中,其余的则放在terraform.tfstate.d/<이름>/(占位符为 workspace 名称)之下。远程后端各自按不同的规则添加前缀。 - 选择是本地状态。当前处于哪个 workspace,只写在
.terraform/environment这个文件的一行里。这个文件不会被提交,也不会与其他人的工作目录共享。也就是说,“正在对哪个环境下命令”不会留在代码或评审中。 - 在配置中只能看到名称。可以用
terraform.workspace读取名称来替换值。常见的做法是把规格写成映射,再用lookup取值。虽然方便,但如果选择了映射中不存在的名称,就会悄悄回落到默认值。
目录划分则具有恰好相反的性质。每个环境各有一个目录,所以状态、init、provider 缓存也都各自独立。正如文档所承认的,它会多占用磁盘和带宽,配置的更新也要逐个进行。换来的是,后端配置可以因环境而异,并且可以通过仓库权限,把“谁能修改 prod 目录”变成代码评审规则。
在现场相遇的样子
最常见的事故不在语法,而在选择。终端里没有任何地方写着 prod,提示符里不显示,命令里也不包含。昨天查看完 prod 就原样下班的目录,今天早上执行 destroy,一切就结束了。实验的第 5 步会故意制造这个事故。
第二种是权限本来就已经分开,却用 workspace 来划分的团队。审计时问“dev 负责人能否读取 prod 的状态”,答案永远是能。因为后端只有一个。这个问题无法靠修改代码来堵住,只能变成迁移到目录(或单独的配置)的搬迁工作。
第三种是workspace 名称渗入代码中的条件判断。terraform.workspace == "prod" ? ... : ... 一旦超过三四处,这份代码实际上就已经是每个环境各一份的两份不同代码了。这时就该改用目录来划分。
防范的方法很简单。在写入命令之前,放一个守卫,如果不是预期的 workspace 就停下来。在 CI 中,则从流水线变量接收环境名称并做同样的检查。让退出码代替人的记忆来做判断。把退出码分成三个而不是两个,在实际工作中也很有价值——流水线需要区分“与预期不符”和“脚本调用有误”,才能决定是重试还是叫人。
总结起来,选择标准是这样的:只需要划分状态,还是连凭据、后端、评审权限都要划分。前者的话,workspace 是最便宜的答案;后者的话,workspace 就不是答案。两者混用也很常见——把 prod 与非生产环境用目录大致分开,在非生产环境内部,再用 workspace 为每位开发者提供临时副本。
下一项实验要做什么
在 /root/tfa-ws 中创建三个 workspace,亲自打开看状态文件生成在哪里,用映射为每个环境提供不同的值,在选中 prod 的状态下运行 destroy 制造事故,然后进行恢复。接着用目录划分重新做出同样的结果,数一数状态文件数量和 provider 缓存数量,用表格比较两种方式。