★★ 中級

06. RDS / DynamoDB

AWS のデータベース。ざっくりは 「リレーショナル=RDS」「NoSQL=DynamoDB」。それぞれ Terraform でどう書くか、最低限のセキュリティ設定込みで見ます。

どっちを使う?

RDSDynamoDB
データモデル表(リレーショナル)キー・値(NoSQL)
クエリSQL(柔軟)キー指定 / GSI(限定的)
料金インスタンス常時稼働従量(リクエスト数 + ストレージ)
運用スケール手動/停止可サーバ管理ゼロ
得意複雑なクエリ、トランザクションセッション / TTL / 高 QPS

RDS 最小例(PostgreSQL)

resource "aws_db_subnet_group" "main" {
  name       = "main"
  subnet_ids = [for s in aws_subnet.private : s.id]
}

resource "aws_security_group" "rds" {
  name_prefix = "rds-"
  vpc_id      = aws_vpc.main.id
}

resource "aws_vpc_security_group_ingress_rule" "rds_from_app" {
  security_group_id            = aws_security_group.rds.id
  referenced_security_group_id = aws_security_group.app.id
  from_port                    = 5432
  to_port                      = 5432
  ip_protocol                  = "tcp"
}

resource "aws_db_instance" "main" {
  identifier             = "myapp-prd"
  engine                 = "postgres"
  engine_version         = "16.4"
  instance_class         = "db.t4g.micro"
  allocated_storage      = 20
  storage_type           = "gp3"
  storage_encrypted      = true

  db_name                = "myapp"
  username               = "postgres"
  password               = var.db_password   # 後で Secrets Manager に置き換え

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  publicly_accessible    = false

  skip_final_snapshot    = false
  final_snapshot_identifier = "myapp-prd-final"
}

本番向け RDS の必須設定

resource "aws_db_instance" "prd" {
  # ... 基本項目は上と同じ ...

  multi_az                          = true       # 別 AZ にスタンバイ
  backup_retention_period           = 30          # バックアップ 30 日保持
  backup_window                     = "16:00-17:00"  # JST 1:00-2:00 想定
  maintenance_window                = "Sun:17:00-Sun:18:00"

  storage_encrypted                 = true
  kms_key_id                        = aws_kms_key.rds.arn

  performance_insights_enabled      = true
  performance_insights_retention_period = 7

  enabled_cloudwatch_logs_exports   = ["postgresql"]

  deletion_protection               = true        # destroy がエラーに
  copy_tags_to_snapshot             = true

  lifecycle {
    prevent_destroy = true                         # Terraform 側でも保護
    ignore_changes  = [password]                    # 外部で変える運用に
  }
}
本番 DB の保護 deletion_protection = true(AWS 側)と lifecycle.prevent_destroy = true(Terraform 側)の二重保護が定石。terraform destroy で一発消去事故を防ぎます。

パスワードを Secrets Manager で管理

resource "random_password" "db" {
  length  = 32
  special = true
}

resource "aws_secretsmanager_secret" "db" {
  name = "myapp/db/master"
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
  secret_string = jsonencode({
    username = "postgres"
    password = random_password.db.result
    host     = aws_db_instance.main.address
    port     = aws_db_instance.main.port
  })
}

resource "aws_db_instance" "main" {
  # ...
  username = "postgres"
  password = random_password.db.result
}

アプリは aws_secretsmanager_secret_version から接続情報を取得します。Terraform 経由でパスワードを生成すれば、.tfvars に書く必要すらありません。

DynamoDB

テーブル定義のキモは 「key(hash_key・range_key)」と「attribute」の宣言attribute は「key で使う列の型を予告」するだけで、他の列は自由に書き込めます(スキーマレス)。

resource "aws_dynamodb_table" "sessions" {
  name         = "sessions"
  billing_mode = "PAY_PER_REQUEST"   # 従量制(PROVISIONED ではなく)
  hash_key     = "session_id"

  attribute {
    name = "session_id"
    type = "S"   # String
  }

  ttl {
    attribute_name = "expires_at"
    enabled        = true
  }

  point_in_time_recovery {
    enabled = true
  }

  server_side_encryption {
    enabled = true
  }

  tags = { Name = "sessions" }
}

billing_mode の選び方

PAY_PER_REQUESTPROVISIONED
料金リクエストごと容量予約 + Auto Scaling
突発負荷強い容量超過でスロットリング
低負荷時のコストほぼ 0予約分は常に発生
初学者向けこちら推奨定常負荷が読めてから

GSI(グローバルセカンダリインデックス)

主キー以外で検索したい時に追加するインデックス。

resource "aws_dynamodb_table" "users" {
  name         = "users"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "user_id"

  attribute {
    name = "user_id"
    type = "S"
  }

  attribute {
    name = "email"
    type = "S"
  }

  global_secondary_index {
    name            = "by-email"
    hash_key        = "email"
    projection_type = "ALL"
  }
}

# 検索: aws dynamodb query --table-name users --index-name by-email \
#       --key-condition-expression "email = :e" \
#       --expression-attribute-values '{":e":{"S":"alice@example.com"}}'
tip DynamoDB は「アクセスパターンを先に決めてからスキーマを設計する」のが鉄則。RDS のように「あとで JOIN すればいい」とはいきません。クエリ要件 → key と GSI、の順で考える。