1. 有状态服务在声明式编排中的特殊性
一句话总结: 数据库是 Terraform 里唯一「删了就回不来」的资源,因此编排重点从「如何创建」转向「如何防止被误删、如何安全变更」。
无状态资源可以随时销毁重建,数据库不行。它的数据不在 Terraform 的 state 里,而在引擎自己的存储里——Terraform 只知道「这个实例应该存在」,不知道「里面有多少数据」。这带来两个直接后果:
| 后果 | 表现 | 应对 |
|---|---|---|
| 变更即风险 | 改一个参数可能触发重启 | 参数组与应用分离 |
| 误删不可逆 | destroy 无确认 | 删除保护 + 生命周期防护 |
resource "aws_db_instance" "primary" {
identifier = "${local.prefix}-primary"
engine = "postgres"
engine_version = "16.3"
instance_class = var.instance_class
allocated_storage = 100
max_allocated_storage = 500
storage_type = "gp3"
storage_encrypted = true
db_name = "appdb"
username = "appadmin"
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${local.prefix}-primary-final"
}
一句话:给数据库写 Terraform,第一行不是
resource,而是「哪些操作绝对不能发生」。
2. 实例选型与参数组
一句话总结: 参数组是数据库的「配置即代码」入口,它与实例解耦——改参数组不重建实例,但改参数组所属的族会。
参数组需要单独建模,并在实例上引用:
resource "aws_db_parameter_group" "postgres16" {
name = "${local.prefix}-pg16"
family = "postgres16"
parameter {
name = "log_min_duration_statement"
value = "500"
}
parameter {
name = "max_connections"
value = "400"
}
parameter {
name = "shared_preload_libraries"
value = "pg_stat_statements"
apply_method = "pending-reboot"
}
lifecycle {
create_before_destroy = true
}
}
注意 apply_method:多数参数可以动态生效,少数(如 shared_preload_libraries)需要重启。Terraform 不会替你判断,参数写错了会在 apply 阶段才暴露。
存储自动扩展是成本与可用性的平衡点:
resource "aws_db_instance" "primary" {
allocated_storage = 100
max_allocated_storage = 500
storage_type = "gp3"
iops = 3000
storage_throughput = 125
}
| 参数类型 | 生效方式 | 风险 |
|---|---|---|
| 动态参数 | 立即生效 | 可能瞬时抖动 |
| 需重启参数 | 维护窗口生效 | 连接中断 |
| 静态族属性 | 需新参数组 | 需重建或迁移 |
一句话:参数组的
family与引擎大版本绑定,升级大版本时参数组必须同步重建。
3. 网络位置:子网组与安全组
一句话总结: 数据库实例本身不选子网,而是选子网组;可用性由子网组覆盖的 AZ 数量决定,多可用区部署必须让子网组至少跨两个可用区。
resource "aws_db_subnet_group" "main" {
name = "${local.prefix}-db-subnets"
subnet_ids = aws_subnet.private[*].id
}
resource "aws_db_instance" "primary" {
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
publicly_accessible = false
multi_az = var.environment == "prod"
}
安全组只允许来自应用安全组的流量,而不是写 CIDR:
resource "aws_security_group" "db" {
name = "${local.prefix}-db"
vpc_id = aws_vpc.main.id
}
resource "aws_security_group_rule" "db_from_app" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_id = aws_security_group.db.id
source_security_group_id = aws_security_group.app.id
}
| 部署形态 | 子网组要求 | 故障切换 |
|---|---|---|
| 单可用区 | 1 个 AZ 即可 | 无,需重建 |
| 多可用区 | 至少 2 个 AZ | 自动切换,分钟级 |
| Aurora 集群 | 至少 2 个 AZ(3 个更佳) | 读副本提升 |
一句话:
publicly_accessible = true几乎总是错的,数据库应只在内网安全组之间可达。
4. 备份、快照与时间点恢复
一句话总结: 自动备份保留期决定可回溯的时间窗口,手动快照决定长期留存的基线,两者结合才是完整的恢复能力。
resource "aws_db_instance" "primary" {
backup_retention_period = 14
backup_window = "17:00-18:00"
maintenance_window = "sun:18:00-sun:19:00"
copy_tags_to_snapshot = true
}
backup_window 与 maintenance_window 不能重叠,否则 apply 会报错——这是很常见的初次踩坑。
长期留存靠手动快照,但要注意:手动快照不会被 Terraform 自动清理,需要显式管理。
resource "aws_db_snapshot" "baseline" {
db_instance_identifier = aws_db_instance.primary.id
db_snapshot_identifier = "${local.prefix}-baseline-${formatdate("YYYYMMDD", timestamp())}"
}
用 timestamp() 会导致每次 plan 都变化,因此生产实践通常是:日常备份交给自动备份,里程碑快照由流水线在发布前创建。
| 机制 | 保留期 | 恢复粒度 | 恢复速度 |
|---|---|---|---|
| 自动备份 | 1~35 天 | 秒级 PITR | 分钟级 |
| 手动快照 | 手动删除 | 快照点 | 分钟级 |
| 跨区域副本 | 跟随源 | 秒级 | 需提升 |
一句话:备份策略的验收标准不是「有备份」,而是「演练过恢复」。
5. 只读副本与读写分离
一句话总结: 只读副本既是读扩展手段,也是跨区域容灾的基础;但 Terraform 中的副本是独立资源,销毁主库时的级联行为必须显式声明。
resource "aws_db_instance" "replica" {
count = var.replica_count
identifier = "${local.prefix}-replica-${count.index}"
replicate_source_db = aws_db_instance.primary.identifier
instance_class = var.replica_instance_class
publicly_accessible = false
skip_final_snapshot = true
}
副本的常见坑是「参数组不一致」:主库改了参数,副本没改,导致复制延迟或行为差异。
resource "aws_db_instance" "replica" {
count = var.replica_count
parameter_group_name = aws_db_parameter_group.postgres16.name
# 副本必须与主库使用同一参数组族
}
| 场景 | 推荐形态 | 备注 |
|---|---|---|
| 报表查询压力大 | 同区域只读副本 | 延迟通常毫秒级 |
| 跨区域容灾 | 跨区域副本 | 注意数据传输费用 |
| 读写分离 | 应用侧路由 | 需处理复制延迟 |
Aurora 的模型不同:副本是集群成员,用 aws_rds_cluster_instance 描述,容量由集群统一管理。
resource "aws_rds_cluster_instance" "members" {
count = 3
identifier = "${local.prefix}-aurora-${count.index}"
cluster_identifier = aws_rds_cluster.main.id
instance_class = "db.r6g.large"
engine = aws_rds_cluster.main.engine
engine_version = aws_rds_cluster.main.engine_version
}
一句话:副本不是「免费的读能力」,它会放大写入的复制开销与存储成本。
6. 版本升级与维护窗口
一句话总结: 小版本可以自动升级,大版本必须显式规划;Terraform 无法替你完成数据迁移,它只能把「升级动作」变得可审计。
resource "aws_db_instance" "primary" {
engine_version = "16.3"
auto_minor_version_upgrade = true
allow_major_version_upgrade = false
apply_immediately = false
}
apply_immediately = false 表示变更在维护窗口执行,生产环境应保持关闭;开发环境可设为 true 以加速迭代。
大版本升级的推荐路径是「先建新实例再迁移」,而不是原地升级:
resource "aws_db_instance" "next" {
identifier = "${local.prefix}-primary-next"
engine_version = "17.2"
snapshot_identifier = aws_db_snapshot.baseline.id
# 从快照恢复后由迁移工具同步增量数据
}
| 升级类型 | 停机 | Terraform 行为 |
|---|---|---|
| 小版本 | 秒级 | 维护窗口内滚动 |
| 大版本原地 | 分钟到小时 | 需显式开启开关 |
| 蓝绿迁移 | 接近零 | 两个实例并存,切换 DNS |
6.1 变更前的防护动作
在 apply 前用 plan 检查是否出现 must be replaced:
terraform plan -out=tfplan
terraform show -json=tfplan | jq -r '
.resource_changes[]? | select(.change.actions | index("delete")) | .address'
任何针对数据库实例的 delete 动作都必须人工确认。
一句话:把「是否会重建」当成数据库变更的第一审查项,比事后恢复便宜得多。
7. 删除保护与生命周期策略
一句话总结: 三层防护叠加才够:实例侧删除保护、Terraform 侧
prevent_destroy、以及最终快照——任一层单独使用都有绕过路径。
resource "aws_db_instance" "primary" {
deletion_protection = true
lifecycle {
prevent_destroy = true
ignore_changes = [password]
}
}
ignore_changes 常用于密码:密码由外部系统轮换,Terraform 不应在每次 plan 时检测到漂移。
| 防护层 | 生效位置 | 绕过方式 |
|---|---|---|
deletion_protection | 云 API | 先关开关再删 |
prevent_destroy | Terraform | 删掉该行 |
| 最终快照 | 存储 | 无(需手动清理) |
| 备份保留期 | 引擎 | 无(自动过期) |
四者叠加后,误删需要「连续四个显式动作」,这在实际操作中几乎不可能发生。
resource "aws_db_instance" "primary" {
skip_final_snapshot = false
final_snapshot_identifier = "${local.prefix}-primary-final-${var.git_sha}"
}
一句话:防护的目标不是「让删除变难」,而是「让删除必须经过一个有意识的决定」。
8. 总结
托管数据库的编排可以归结为「三防一控」:
| 环节 | 要点 |
|---|---|
| 建模 | 实例、参数组、子网组分开管理 |
| 参数组 | 注意 family 绑定与需重启参数 |
| 网络 | 子网组跨 AZ,安全组只对应用开放 |
| 备份 | 自动备份 + 手动快照 + 定期演练恢复 |
| 副本 | 参数组与主库一致,警惕复制延迟 |
| 升级 | 大版本走蓝绿,避免原地升级 |
| 防护 | 删除保护 + prevent_destroy + 最终快照 |
| 审查 | plan 中出现 delete 即人工复核 |
一句话收尾:数据库是 IaC 世界里唯一不能「重来一次」的部分。无状态资源的编排追求效率,有状态资源的编排追求确定性——参数组的版本绑定、子网组的可用区覆盖、备份窗口与维护窗口的互斥、删除保护的多层叠加,本质上都是在把「不确定性」逐项消除。下一篇讨论 IAM 策略建模,那是权限层面的另一套确定性工程。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。