1. 为什么需要远程状态与协作
一句话总结: 本地 state 只适合单人玩具项目,一旦多人协作就必须把状态放到共享后端,否则两个人的 apply 会互相覆盖,产生无法解释的资源漂移。
terraform.tfstate 是 Terraform 的全部记忆:资源 ID、属性、依赖关系。本地存储有三个致命问题:
# 1. 状态文件在个人电脑上,别人拿不到
ls -la terraform.tfstate*
# 2. 误提交到 git(含明文密钥)
git log --all -- terraform.tfstate
# 3. 两人同时 apply,后者的状态覆盖前者
远程后端解决三件事:共享(所有人读写同一份状态)、加锁(同一时刻只有一个人能改)、审计(状态变更历史可追溯)。
| 能力 | 本地状态 | 远程后端 |
|---|---|---|
| 多人协作 | 不可能 | 原生支持 |
| 状态锁 | 无 | 有 |
| 版本历史 | 靠 git(危险) | 后端原生版本化 |
| 加密 | 无 | KMS / SSE |
| CI 可用 | 需拷贝文件 | 天然适配 |
一句话:把 state 从「一个人的文件」变成「团队的服务」,是 Terraform 从个人工具走向团队基础设施的第一步。
2. 后端选型
一句话总结: S3 + DynamoDB 是自托管场景的默认答案,Terraform Cloud 提供托管状态与协作 UI,选择取决于团队是否愿意自己运维存储与权限。
2.1 S3 + DynamoDB
S3 存状态文件,DynamoDB 提供锁。这是最经典的自托管方案,成本极低、可控性强。
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abcd-1234"
}
}
# 创建锁表(一次性)
aws dynamodb create-table \
--table-name terraform-locks \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
2.2 Terraform Cloud / HCP
托管方案把状态、锁、变量、审批、策略门禁都收进一个平台,代价是绑定供应商与按用量计费。
terraform {
cloud {
organization = "example-org"
workspaces {
name = "prod-network"
}
}
}
terraform login
terraform init
# 状态由平台托管,本地不再出现 tfstate 文件
| 维度 | S3 + DynamoDB | Terraform Cloud |
|---|---|---|
| 运维成本 | 自建自管 | 零运维 |
| 成本模型 | 按存储与请求 | 按用户与并发 |
| 状态锁 | DynamoDB | 内置 |
| 变量与密钥 | 自建(SSM/Vault) | 内置变量集 |
| 策略门禁 | 自行接入 OPA | 内置 Sentinel |
| 远程执行 | 需自建 runner | 内置 |
一句话:选型的核心问题是「你愿意自己运维状态存储吗」——愿意就用 S3,不愿意就用托管,二者没有技术优劣只有组织适配。
3. 状态锁与并发控制
一句话总结: 状态锁保证同一时刻只有一次 apply 在改状态,锁超时与强制解锁是必须掌握的两个操作,但强制解锁前一定要确认没有僵尸进程。
apply 时 Terraform 会先获取锁,失败则报错退出:
terraform apply
# Error: Error acquiring the state lock
# Lock Info:
# ID: 8f3c2a1e-...
# Path: prod/network/terraform.tfstate
# Operation: OperationTypeApply
# Who: ci-runner@build-1234
# 确认没有正在运行的 apply 之后,才强制解锁
terraform force-unlock 8f3c2a1e-...
# 也可以设置锁等待时间,避免 CI 因瞬时冲突失败
terraform apply -lock-timeout=5m
3.1 锁的常见陷阱
| 场景 | 表现 | 处理 |
|---|---|---|
| CI 任务被 kill | 锁残留 | 确认后 force-unlock |
| 本地 Ctrl+C | 锁未释放 | 重试或 force-unlock |
| 两人同时 plan | plan 可以并行 | plan 不加锁,apply 才加 |
| 状态被手工编辑 | 版本不一致 | 立即停止并用备份恢复 |
# 在 CI 中统一设置锁超时
# terraform plan/apply 都加 -lock-timeout
一句话:锁报错是「保护」不是「故障」——绝大多数情况下正确做法是等待或查明原因,而不是立刻 force-unlock。
4. 工作区与变量集
一句话总结: 工作区解决「同一套配置多环境实例」,变量集解决「多工作区共享同一批变量」,二者配合能把环境差异从代码里剥离到平台配置。
Terraform Cloud 的 workspace 与 CLI workspace 语义不同:前者是独立的状态 + 变量 + 运行历史容器。
# 同一份配置,通过 workspace 区分环境
locals {
env = terraform.workspace
instance_count = {
dev = 1
stg = 2
prod = 4
}[local.env]
}
terraform workspace list
terraform workspace new stg
terraform workspace select prod
terraform workspace show
变量集(Variable Set)把「所有工作区都要用的变量」集中管理,避免在每个工作区重复配置:
# 通过 API 创建变量集并关联多个工作区
curl -s -X POST \
-H "Authorization: Bearer $TFC_TOKEN" \
-H "Content-Type: application/vnd.api+json" \
https://app.terraform.io/api/v2/varsets \
-d '{
"data": {
"type": "varsets",
"attributes": {
"name": "global-aws-creds",
"global": false
}
}
}'
| 机制 | 作用域 | 适用 |
|---|---|---|
| workspace 变量 | 单个工作区 | 环境特有(实例数、规格) |
| 变量集 | 多工作区 | 共享凭证、公共标签 |
| 全局变量集 | 全部工作区 | 组织级默认值 |
一句话:workspace 是「隔离」,变量集是「复用」——把该隔离的隔离、该共享的共享,配置才不会退化成复制粘贴。
5. 状态拆分与数据源共享
一句话总结: 单一大状态会让每次 plan 都刷新所有资源、爆炸半径过大,按生命周期与变更频率拆分状态,再用 data source 或 remote state 在栈之间传递引用。
拆分原则:变更频率相近、生命周期一致、爆炸半径可接受的资源放同一个状态。
# 拆分示例
# prod/network/ —— VPC、子网、路由(极少变更)
# prod/data/ —— RDS、ElastiCache(季度变更)
# prod/app/ —— ECS、ALB、AutoScaling(频繁变更)
跨栈引用有两种方式。其一是 terraform_remote_state 数据源:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "company-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "web" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
}
其二是 Provider 原生数据源,耦合更松、推荐优先:
data "aws_vpc" "main" {
filter {
name = "tag:Name"
values = ["prod-vpc"]
}
}
resource "aws_instance" "web" {
subnet_id = data.aws_subnets.private.ids[0]
}
| 方式 | 耦合度 | 需要 state 读取权限 | 推荐度 |
|---|---|---|---|
| terraform_remote_state | 高(依赖对方 output) | 是 | 仅当无原生数据源 |
| Provider 数据源 | 低(按属性查询) | 否 | 首选 |
一句话:拆状态的判断标准是「这组资源会不会一起变」——一起变的一起拆,不一起变的坚决分开。
6. 敏感信息与加密
一句话总结: state 里存着数据库密码、私钥、令牌的明文,必须启用静态加密、限制访问权限,并把真正的密钥来源放在 Vault 或 SSM 而非 tfvars。
# 敏感值在 state 中仍是明文,标记 sensitive 只影响输出显示
resource "aws_db_instance" "main" {
password = var.db_password # state 里是明文!
}
# 检查 state 中是否有明文密钥
terraform state pull | jq -r '.. | strings' | grep -iE 'password|secret|token' | head
三重防护:
# 1. 后端加密(S3 SSE-KMS)
terraform {
backend "s3" {
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abcd-1234"
}
}
# 2. 密钥来自动态数据源,不落 tfvars
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db.secret_string
}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::company-terraform-state/*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::123456789012:role/terraform-ci"
}
}
}
]
}
| 措施 | 拦截的风险 |
|---|---|
| SSE-KMS 加密 | 存储介质泄漏 |
| 存储桶策略 | 越权读取状态 |
| 动态密钥源 | 密钥落入 tfvars 与 git |
| 状态访问审计 | 事后追查谁读了状态 |
一句话:把 state 当作「密钥库」来对待——它确实就是,只是大多数人忘了这一点。
7. 迁移、备份与故障恢复
一句话总结: 后端迁移用
terraform init -migrate-state,日常靠版本化与定期备份,状态损坏时用备份恢复并用 import 补齐缺口。
# 从本地迁移到 S3
terraform init -migrate-state
# 会提示:Do you want to copy existing state to the new backend?
# 从一个 S3 路径迁到另一个
terraform init -migrate-state -force-copy
# 开启存储桶版本化,保留历史状态
aws s3api put-bucket-versioning \
--bucket company-terraform-state \
--versioning-configuration Status=Enabled
# 列出某状态文件的历史版本
aws s3api list-object-versions \
--bucket company-terraform-state \
--prefix prod/network/terraform.tfstate \
--query 'Versions[].{VersionId:VersionId,LastModified:LastModified}'
# 定期备份到独立位置
terraform state pull > backup/network-$(date +%Y%m%d).tfstate
# 恢复:先备份现状,再推送历史版本
terraform state push backup/network-20261001.tfstate
| 场景 | 恢复手段 | 前置条件 |
|---|---|---|
| 状态被误改 | state push 历史版本 | 有备份或版本化 |
| 状态丢失 | 从备份恢复 + import 补齐 | 备份 + 资源清单 |
| 后端迁移失败 | 保留原后端配置回退 | 迁移前备份 |
| 锁残留 | force-unlock | 确认无运行中任务 |
一句话:备份不是「以防万一」,而是「一定会用到」——状态损坏只是时间问题,区别在于你有没有备份。
8. 总结
远程状态与协作是把 Terraform 用于团队的门槛,也是收益最大的基础设施投资:
| 环节 | 关键做法 | 常见坑 |
|---|---|---|
| 后端选型 | S3+DynamoDB 或 Terraform Cloud | 用本地状态做团队项目 |
| 状态锁 | 保留锁 + 谨慎 force-unlock | 见锁就强解导致状态损坏 |
| 工作区 | workspace 隔离环境 | 把 CLI workspace 当隔离边界 |
| 变量集 | 共享凭证集中管理 | 每个工作区重复配置 |
| 状态拆分 | 按变更频率与爆炸半径 | 单一巨型状态 |
| 跨栈引用 | 优先 Provider 数据源 | 过度依赖 remote_state |
| 敏感信息 | SSE-KMS + 动态密钥源 | state 明文密钥进 git |
| 备份恢复 | 版本化 + 定期 state pull | 出事时无备份可用 |
一句话收尾:远程状态是团队协作的地基——它决定了「谁能改、何时能改、改了能不能查」。当后端、锁、工作区、拆分与加密各就各位,Terraform 才真正从「我一个人的脚本」变成「整个团队的基础设施控制面」。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。