Terraform实战10:RDS + Secrets Manager——数据库与密钥管理

在这里插入图片描述

本篇目标

用Terraform创建RDS MySQL数据库,配合Secrets Manager安全管理数据库密码。理解数据库子网组、安全访问控制和密钥管理最佳实践。

学完本篇你将掌握:

  • RDS数据库的创建和配置
  • 数据库子网组(DB Subnet Group)
  • Secrets Manager存储和管理密码
  • random_password生成安全密码
  • 数据库安全组设计(只允许应用层访问)

前置条件

  • 已完成前九篇练习
  • 理解VPC公私网和安全组

为什么需要Secrets Manager

之前的练习中密码可以写在变量里,但生产环境这样做很危险:

方式问题
密码写在代码里推到Git后所有人都能看到
密码写在tfvars里tfvars可能被提交,或本地泄露
密码写在环境变量里不方便管理和轮转
Secrets Manager加密存储、API获取、支持自动轮转、有审计日志

架构图

┌─────────────────────────────────────────────────────────┐
│                 VPC (10.0.0.0/16)                        │
│                                                         │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────┐  │
│  │ 公有子网(2个)  │  │ 私有子网(2个)  │  │DB子网(2个)│  │
│  │ 10.0.1-2.0/24│  │ 10.0.10-11/24│  │10.0.20-21 │  │
│  └───────────────┘  └───────────────┘  └─────┬─────┘  │
│                                               │         │
│                                        ┌──────┴──────┐  │
│                                        │  RDS MySQL  │  │
│                                        │  db.t3.micro│  │
│                                        │  port:3306  │  │
│                                        └─────────────┘  │
│                                               ↑         │
│                                          安全组:        │
│                                     只允许VPC内部3306    │
└─────────────────────────────────────────────────────────┘

        ┌─────────────────────┐
        │  Secrets Manager    │
        │  存储DB用户名/密码   │  ← 应用通过API获取密码
        └─────────────────────┘

目录结构

10-rds-secrets/
├── main.tf          # VPC + RDS + Secrets Manager + 安全组
├── variables.tf     # 变量(DB规格、库名、用户名)
└── outputs.tf       # 输出(端点地址、Secret名称)

完整代码

main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
  required_version = ">= 1.0"
}

provider "aws" {
  region = var.region
}

locals {
  name_prefix = "${var.project_name}-${var.environment}"

  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# VPC(注意:多了database_subnets)
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.16.0"

  name = "${local.name_prefix}-vpc"
  cidr = "10.0.0.0/16"

  azs              = ["${var.region}a", "${var.region}b"]
  public_subnets   = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets  = ["10.0.10.0/24", "10.0.11.0/24"]
  database_subnets = ["10.0.20.0/24", "10.0.21.0/24"]  # 【新增】数据库专用子网

  # 社区模块自动创建数据库子网组
  create_database_subnet_group = true
  database_subnet_group_name   = "${local.name_prefix}-db-subnet-group"

  enable_nat_gateway   = false
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = local.common_tags
}

# 【新资源】随机密码(不把密码写在代码里)
resource "random_password" "db_password" {
  length           = 16
  special          = true
  override_special = "!#$%&*()-_=+[]{}<>:?"
}

# 【新资源】Secrets Manager - 密钥存储
resource "aws_secretsmanager_secret" "db_credentials" {
  name                    = "${local.name_prefix}-db-credentials"
  description             = "RDS database credentials"
  recovery_window_in_days = 0    # 练习用(生产用7-30天)

  tags = local.common_tags
}

# Secret的值(JSON格式)
resource "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = aws_secretsmanager_secret.db_credentials.id

  secret_string = jsonencode({
    username = var.db_username
    password = random_password.db_password.result
    dbname   = var.db_name
    host     = aws_db_instance.mysql.endpoint
    port     = 3306
    engine   = "mysql"
  })
}

# 安全组 - 只允许VPC内部访问3306
resource "aws_security_group" "rds" {
  name   = "${local.name_prefix}-rds-sg"
  vpc_id = module.vpc.vpc_id

  ingress {
    description = "MySQL from VPC"
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = [module.vpc.vpc_cidr_block]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-rds-sg"
  })
}

# 【新资源】RDS MySQL
resource "aws_db_instance" "mysql" {
  identifier = "${local.name_prefix}-mysql"

  # 引擎
  engine         = "mysql"
  engine_version = "8.0"
  instance_class = var.db_instance_class

  # 存储
  allocated_storage     = 20
  max_allocated_storage = 50
  storage_type          = "gp3"
  storage_encrypted     = true

  # 数据库
  db_name  = var.db_name
  username = var.db_username
  password = random_password.db_password.result

  # 网络
  db_subnet_group_name   = module.vpc.database_subnet_group_name
  vpc_security_group_ids = [aws_security_group.rds.id]
  publicly_accessible    = false    # 不允许公网访问

  # 备份
  backup_retention_period = 1
  skip_final_snapshot     = true

  # 高可用(练习关闭)
  multi_az            = false
  deletion_protection = false

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-mysql"
  })
}

variables.tf

variable "region" {
  default = "us-east-1"
}

variable "project_name" {
  default = "tf-practice"
}

variable "environment" {
  default = "dev"
}

variable "db_instance_class" {
  description = "RDS实例规格"
  default     = "db.t3.micro"
}

variable "db_name" {
  description = "数据库名称"
  default     = "myapp"
}

variable "db_username" {
  description = "数据库管理员用户名"
  default     = "admin"
}

outputs.tf

output "rds_endpoint" {
  description = "RDS连接地址"
  value       = aws_db_instance.mysql.endpoint
}

output "rds_port" {
  value = aws_db_instance.mysql.port
}

output "rds_database_name" {
  value = aws_db_instance.mysql.db_name
}

output "secret_name" {
  description = "Secrets Manager中的密钥名称"
  value       = aws_secretsmanager_secret.db_credentials.name
}

output "secret_arn" {
  description = "Secret ARN"
  value       = aws_secretsmanager_secret.db_credentials.arn
}

# 不输出密码明文!通过API获取
output "how_to_get_password" {
  value = "aws secretsmanager get-secret-value --secret-id ${aws_secretsmanager_secret.db_credentials.name} --query SecretString --output text"
}

新增资源说明

RDS关键字段

字段含义示例值
identifierRDS实例标识(控制台显示的名称)“tf-practice-dev-mysql”
engine / engine_version数据库引擎和版本“mysql” / “8.0”
instance_class实例规格“db.t3.micro”
allocated_storage初始存储(GB)20
max_allocated_storage存储自动扩展上限(GB)50
storage_type存储类型“gp3”(性价比最高)
storage_encrypted是否加密存储true
db_subnet_group_name数据库子网组跨2个AZ
publicly_accessible是否允许公网访问false(必须
multi_az是否多AZ高可用生产true,练习false
backup_retention_period备份保留天数生产7-30,练习1
skip_final_snapshot删除时是否跳过最终快照生产false,练习true
deletion_protection防止误删生产true,练习false

Secrets Manager关键字段

字段含义
name密钥名称(全局唯一)
recovery_window_in_days删除后保留天数(0=立即删,生产用7-30)
secret_string密钥值(通常是JSON格式)

random_password

resource "random_password" "db_password" {
  length           = 16            # 密码长度
  special          = true          # 包含特殊字符
  override_special = "!#$%&*()..."  # 指定允许的特殊字符
}
# 引用:random_password.db_password.result

database_subnets(VPC模块新参数)

module "vpc" {
  # ... 
  database_subnets = ["10.0.20.0/24", "10.0.21.0/24"]    # 数据库专用子网
  create_database_subnet_group = true                      # 自动创建子网组
  database_subnet_group_name   = "xxx-db-subnet-group"     # 子网组名称
}

RDS要求至少2个AZ的子网才能创建子网组。社区VPC模块支持直接传database_subnets参数自动处理。


操作步骤与实际输出

Apply(RDS创建约6-7分钟)

terraform apply -auto-approve
module.vpc.aws_db_subnet_group.database[0]: Creation complete after 3s
aws_security_group.rds: Creation complete after 7s
aws_db_instance.mysql: Still creating... [6m0s elapsed]
aws_db_instance.mysql: Creation complete after 6m30s

Apply complete! Resources: 27 added, 0 changed, 0 destroyed.

Outputs:
rds_endpoint      = "tf-practice-dev-mysql.cqnyouc4a558.us-east-1.rds.amazonaws.com:3306"
rds_database_name = "myapp"
secret_name       = "tf-practice-dev-db-credentials"
how_to_get_password = "aws secretsmanager get-secret-value --secret-id tf-practice-dev-db-credentials --query SecretString --output text"

获取密码(通过API而非明文输出)

aws secretsmanager get-secret-value \
  --secret-id tf-practice-dev-db-credentials \
  --query SecretString --output text

返回JSON:

{
  "username": "admin",
  "password": "随机生成的16位密码",
  "dbname": "myapp",
  "host": "tf-practice-dev-mysql.xxx.rds.amazonaws.com:3306",
  "port": 3306,
  "engine": "mysql"
}

控制台验证

RDS实例详情:
在这里插入图片描述

  • 引擎:MySQL 8.0
  • 规格:db.t3.micro
  • 状态:Available

连接与安全:
在这里插入图片描述

  • 公网访问:No
  • VPC和子网组

Secrets Manager:
在这里插入图片描述

  • 密钥已创建

数据库子网组:
在这里插入图片描述

  • 跨2个AZ

Destroy

terraform destroy -auto-approve
# Destroy complete! Resources: 27 destroyed.

安全最佳实践

实践本篇演示生产环境建议
密码不写在代码里✅ random_password生成
密码存在Secrets Manager✅ + 开启自动轮转
RDS不可公网访问✅ publicly_accessible=false
安全组限制来源✅ 只允许VPC内部进一步限制到应用安全组
存储加密✅ storage_encrypted=true✅ + 用自定义KMS key
多AZ高可用❌ 省钱关闭✅ multi_az=true
删除保护❌ 练习关闭✅ deletion_protection=true
备份保留1天7-30天

延伸思考:面试常见问题

面试问题答案要点
RDS密码怎么管理?random_password生成 + Secrets Manager存储,不在代码中明文
为什么RDS要放私有子网?不暴露公网,只允许VPC内部应用访问
DB Subnet Group是什么?告诉RDS可以在哪些子网创建实例,至少跨2个AZ
multi_az有什么用?主从自动切换,主实例挂了自动failover到备用AZ
应用怎么获取数据库密码?通过Secrets Manager API获取,不写死在配置文件里
gp3和gp2的区别?gp3性价比更高,基准性能更好,可独立调整IOPS

费用说明

资源费用
RDS db.t3.micro~$0.017/小时
Secrets Manager~$0.40/月(按比例)
VPC / 子网 / 安全组免费
总计~$0.02/小时

本次练习(约15分钟):约$0.01


小结

本篇核心收获:

  1. RDS放私有子网 + publicly_accessible=false 是安全标配
  2. random_password 生成密码 + Secrets Manager 存储 = 安全的密码管理
  3. DB Subnet Group 需要至少2个AZ的子网(VPC模块可自动创建)
  4. 不输出密码明文,通过Secrets Manager API获取
  5. 生产环境要开multi_az、deletion_protection、更长的备份保留

下一篇预告

Terraform实战11:完整三层架构(VPC + ALB + EKS + RDS)

下一篇将串联前面所有知识,构建一个完整的生产级架构:

  • VPC网络层
  • ALB负载均衡层
  • EKS计算层
  • RDS数据层
  • 所有组件通过模块组合

参考链接

更多推荐