托管数据库编排:参数组、备份与升级

用 Terraform 编排 RDS、Aurora 与 Cloud SQL:实例与参数组建模、子网组与安全组定位、备份与时间点恢复、只读副本与读写分离、大版本升级与维护窗口,以及删除保护与生命周期策略。

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_destroyTerraform删掉该行
最终快照存储无(需手动清理)
备份保留期引擎无(自动过期)

四者叠加后,误删需要「连续四个显式动作」,这在实际操作中几乎不可能发生。

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 策略建模,那是权限层面的另一套确定性工程。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. Helm Provider 与应用发布:值注入与回滚
  2. 模块注册表与分发:版本、文档与测试
  3. DNS 与证书编排:托管区域与自动验证