1. 先有鸡问题
一句话总结: 用 Terraform 创建存放 state 的桶,而这个桶本身又需要 state——这就是引导(bootstrap)要解决的核心矛盾。
理想状态下 terraform apply 创建 S3 桶来存 state,但第一次 apply 时桶还不存在,无法读写 state。同样的矛盾还出现在:凭据(apply 需要云凭据,而 IAM Role 本身要用 Terraform 建)、模块仓库(配置引用私有模块,而仓库的存储与权限要用 Terraform 建)、锁表(DynamoDB 锁表也需要先存在)、后端加密密钥(KMS key 要在 state 加密之前存在)。
三条解决思路:
| 思路 | 做法 | 优缺点 |
|---|---|---|
| 本地 state 起步 | 引导层先用本地 state,建完桶后迁到远程 | 简单直接,但本地 state 易丢 |
| 独立引导栈 | 引导层永远用本地 state,不进主流程 | 最常用,职责清晰 |
| 手工创建 | 手工建桶与锁表,之后全交给 Terraform | 快但不可复现、易漂移 |
推荐「独立引导栈」:把「创建 state 存储」本身也写成 Terraform 配置,但它是一个独立的、极少变更的配置,其 state 放在本地或另一个更原始的存储里。
引导层应包含:state 桶 + 版本控制 + 加密 + 生命周期、锁表、
state 访问所需的 IAM 角色、组织级 OIDC/SSO 角色、模块仓库
引导层不应包含:业务资源(VPC、数据库、应用)、频繁变更的任何东西
一句话: 引导层的目标不是「消灭本地 state」,而是把本地 state 的范围限制到最小且稳定——通常只有几个桶、一张锁表、几个 IAM 角色。
2. 引导层设计
一句话总结: 引导层是一个独立的根模块,通常用本地 state,只在「初始化一个新账号/新环境」时运行一次,之后几乎不再改动。
2.1 目录结构
infra/
├── bootstrap/ # 引导层,本地 state(需备份)
│ ├── main.tf
│ └── terraform.tfstate
└── envs/prod/
├── backend.tf # 指向 bootstrap 建出来的桶
└── main.tf
2.2 引导层配置
# bootstrap/main.tf
terraform {
required_version = ">= 1.6"
# 刻意不配置远程后端:这一步就是要创建远程后端
}
resource "aws_s3_bucket" "state" {
bucket = "acme-tfstate"
}
resource "aws_s3_bucket_versioning" "state" {
bucket = aws_s3_bucket.state.id
versioning_configuration { status = "Enabled" } # state 版本历史是救命稻草
}
resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
bucket = aws_s3_bucket.state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.state.arn
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_public_access_block" "state" {
bucket = aws_s3_bucket.state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "lock" {
name = "acme-tfstate-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute { name = "LockID", type = "S" }
}
2.3 state 桶的额外保护
state 是基础设施的「唯一真相」,一旦损坏或泄露后果严重:
# 旧版本过期(有版本控制时必须配,否则存储无限增长)
resource "aws_s3_bucket_lifecycle_configuration" "state" {
bucket = aws_s3_bucket.state.id
rule {
id = "expire-old-versions"
status = "Enabled"
noncurrent_version_expiration { noncurrent_days = 90 }
}
}
# 拒绝非加密传输
resource "aws_s3_bucket_policy" "deny_insecure" {
bucket = aws_s3_bucket.state.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "DenyInsecureTransport", Effect = "Deny", Principal = "*"
Action = "s3:*"
Resource = ["${aws_s3_bucket.state.arn}", "${aws_s3_bucket.state.arn}/*"]
Condition = { Bool = { "aws:SecureTransport" = "false" } }
}]
})
}
2.4 引导层的 state 怎么管
三种做法:本地 state + 备份(加密后存进密码管理器或另一个账号的桶,简单但依赖纪律);用第二个更原始的存储(把引导层 state 存在已有账号的桶里,适合多账号);导入式重建(不保存 state,需要时用 terraform import 重新纳管)。多数团队选第一种,配合「引导层极少变更」的纪律。
3. 状态后端配置
一句话总结: 后端配置决定 state 存哪、怎么锁、怎么加密;生产上必须用远程后端 + 版本控制 + 加密,且后端配置要能通过变量注入以复用。
3.1 后端块与部分配置
不要把桶名硬编码进每个环境,用部分配置(partial configuration)注入:
# envs/prod/backend.tf
terraform {
backend "s3" {
encrypt = true # bucket / key / region / dynamodb_table 由 -backend-config 提供
}
}
terraform init -backend-config=prod.backend.tfvars
# prod.backend.tfvars: bucket / key / region / dynamodb_table
这样同一份代码可在不同环境用不同 state 路径初始化,多环境与工作区 里讨论的隔离策略正是靠这个机制落地。
3.2 各云的后端选型与锁
| 云 | 后端 | 锁机制 |
|---|---|---|
| AWS | s3 | DynamoDB 表,或 S3 条件写(1.10+) |
| GCP | gcs | 内置(对象世代号) |
| Azure | azurerm | Blob lease |
| 通用 | remote(Terraform Cloud) | 平台内置 |
Terraform 1.10 起 S3 后端支持 use_lockfile = true,用 S3 条件写实现锁,可以不再依赖 DynamoDB:
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "prod/app/terraform.tfstate"
region = "ap-northeast-1"
use_lockfile = true # 免去 DynamoDB 锁表
encrypt = true
}
}
对新项目,这能省掉一张表与其权限管理。老项目迁移时要确认所有执行者的 Terraform 版本都 ≥ 1.10,否则会出现「一部分人用锁表、一部分人用锁文件」的破锁窗口。
3.3 权限收敛与后端迁移
state 桶的访问权限应当只给需要它的流水线与角色:
data "aws_iam_policy_document" "state_access" {
statement {
sid = "StateBucketAccess"
actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"]
resources = [aws_s3_bucket.state.arn, "${aws_s3_bucket.state.arn}/*"]
}
statement {
sid = "LockAccess"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
resources = [aws_dynamodb_table.lock.arn]
}
}
state 里可能包含敏感值,所以读 state 的权限等同于读密钥的权限,这与 资源与状态 里讨论的 state 安全一致。
迁移后端(本地 → 远程,或换桶)时先备份再迁移:
cp terraform.tfstate terraform.tfstate.backup-$(date +%F)
terraform init -migrate-state # Terraform 会问是否复制 state
terraform plan # 应无意外差异
4. 账号与权限初始化
一句话总结: 引导层还要负责「让后续的 Terraform 能跑起来」——创建执行角色、配置 OIDC 联邦、建立权限护栏,这些都必须在主流程之前存在。
4.1 执行角色
# 供 CI 使用的执行角色,通过 OIDC 联邦免静态密钥
resource "aws_iam_role" "terraform_ci" {
name = "terraform-ci"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
StringLike = { "token.actions.githubusercontent.com:sub" = "repo:acme/infra:*" }
}
}]
})
}
4.2 权限护栏与多账号铺开
引导层应建立护栏,防止后续配置越权(例如禁止关闭 S3 的公共访问阻断,用 SCP 或权限边界实现)。护栏放在引导层的好处是:它比业务资源更难被绕过,且变更需要单独审批。
多账号场景下,引导层要按账号分别运行:
management 账号 ──► 组织、SCP、SSO、创建成员账号、配置跨账号 assume role
member 账号 ──► 各自的 bootstrap(由 management 用 assume role 触发)
provider "aws" {
alias = "member"
region = var.region
assume_role {
role_arn = "arn:aws:iam::${var.member_account_id}:role/OrganizationAccountAccessRole"
}
}
module "member_bootstrap" {
source = "../modules/state-backend"
providers = { aws = aws.member }
bucket = "acme-tfstate-${var.member_account_id}"
}
5. 模块仓库与版本
一句话总结: 引导层还要解决「模块从哪来」——私有模块仓库、版本控制与访问凭据都必须在主配置引用模块之前就绪。
5.1 模块来源的三种方式
| 方式 | 写法 | 适用 |
|---|---|---|
| Git 直接引用 | source = "git::https://.../modules//vpc?ref=v1.2.0" | 小团队,起步快 |
| 私有 Registry | source = "app.terraform.io/acme/vpc/aws" | 需要版本语义与文档 |
| S3 / GCS 归档 | source = "s3::https://.../vpc.zip" | 需要私有分发 |
关键纪律:永远用 ref= 锁定版本,不要指向分支——写成 ?ref=main 会让模块一改、所有环境同时受影响。
5.2 私有 Registry 与版本集中管理
若用 Terraform Cloud/Enterprise 或自建 Registry,注册表本身与其凭据要在引导层配置(tfe_team、tfe_registry_module 等资源)。模块的发布流程与语义化版本规范参见 模块注册表发布
。
版本用变量集中管理,便于批量升级且 diff 清晰:
locals {
module_versions = { vpc = "v1.2.0", eks = "v2.4.1", rds = "v1.0.3" }
}
module "vpc" {
source = "git::https://github.com/acme/terraform-modules.git//vpc?ref=${local.module_versions.vpc}"
}
6. 灾难恢复重建
一句话总结: 灾难恢复的前提是「能从零重建一切」,而重建的起点就是引导层——必须定期演练,否则出事时才发现某个环节依赖了没人记得的手工操作。
6.1 重建顺序
1. 恢复引导层:建 state 桶 + 锁表,恢复引导层本地 state(从备份)
2. 恢复权限:IAM 角色、OIDC 提供方、SCP、CI 执行角色
3. 恢复模块仓库:私有 Registry / Git 仓库
4. 恢复环境:terraform init -backend-config=... && terraform apply(逐环境)
5. 校验:用漂移检测比对期望与实际
6.2 state 丢失的应对
如果 state 桶被误删(未开版本控制),后果是「Terraform 不认识已有资源」,只能逐个导入:
terraform import aws_vpc.main vpc-0a1b... # 资源还在,state 没了
terraform import aws_subnet.private[0] subnet-0123...
这正是必须开桶版本控制的原因——有版本历史时,恢复只是找回某个版本的对象:
aws s3api list-object-versions --bucket acme-tfstate --prefix prod/app/terraform.tfstate
aws s3api get-object --bucket acme-tfstate --key prod/app/terraform.tfstate \
--version-id <vid> restored.tfstate
6.3 定期演练与防丢措施
演练频率:每季度一次
演练内容:在隔离账号从零跑 bootstrap → 用备份 state 恢复一个环境 → 验证 apply 后 plan 干净
产出:一份「实际步骤」文档,替代「想象中的步骤」
防丢措施:引导层 state 备份到至少两个位置(密码管理器 + 另一个账号的桶);代码与文档放在最显眼的位置;记录每个手工步骤的理由,能自动化的尽量自动化;定期用 plan 确认引导层资源未被手工改动。
一句话: 没有演练过的灾难恢复方案等于没有方案;引导层是演练的起点,也是最容易藏着「只有某个人知道的手工步骤」的地方。
7. 实战与排错
一句话总结: 引导与后端的故障集中在「init 失败、锁残留、权限不足、state 路径写错」,排查入口是
terraform init的输出与后端存储的实际内容。
| 现象 | 原因 | 处理 |
|---|---|---|
Failed to get existing workspaces | 桶不存在或权限不足 | 确认 bootstrap 已跑、角色有 ListBucket |
Error acquiring the state lock | 上次 apply 异常退出 | 确认无运行中进程后 force-unlock <ID> |
Backend configuration changed | 后端参数变了 | terraform init -migrate-state |
| 两个环境互相覆盖 | key 写成了同一个 | 检查各环境的 backend.tfvars |
No valid credential sources | 执行角色未配置 | 检查 OIDC 或 assume role |
7.1 锁残留处理
# 先确认没有正在运行的 apply
aws dynamodb get-item --table-name acme-tfstate-lock \
--key '{"LockID":{"S":"acme-tfstate/prod/app/terraform.tfstate"}}'
terraform force-unlock <LOCK_ID> # 最后手段
force-unlock 是最后手段——如果确实有另一个 apply 在跑,强制解锁会导致两个进程同时写 state,产生不可预测的损坏。
7.2 init 排错命令
terraform init -reconfigure # 后端参数变化后重新初始化
terraform init -backend-config=prod.backend.tfvars -upgrade
terraform state pull > /tmp/state.json # 拉取远程 state 检查内容
terraform state list # 确认资源确实在 state 里
7.3 生产清单
- 引导层独立于业务配置,用本地 state 并已备份到两处。
- state 桶开启版本控制、KMS 加密、公共访问阻断、非当前版本过期。
- 锁机制明确(DynamoDB 或
use_lockfile),全团队版本一致。 - state 访问权限收敛到具体角色,读 state 视为敏感权限。
- 执行角色用 OIDC 联邦,无长期 AK/SK。
- 模块引用全部用
ref=锁定版本,版本集中管理。 - 护栏(SCP / 权限边界)放在引导层,变更需单独审批。
- 每季度演练一次从零重建,并更新实际步骤文档。
- 引导层资源纳入 远程状态管理 的巡检范围。
一句话收尾: 引导层的价值在于「把不可复现的手工步骤压缩到最小且被代码化」。它是一次性投入、长期受益的工程——真正出事的那天,能不能在一小时内从零重建,取决于今天有没有认真写这个几乎不变更的目录。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。