06. RDS / DynamoDB
AWS のデータベース。ざっくりは 「リレーショナル=RDS」「NoSQL=DynamoDB」。それぞれ Terraform でどう書くか、最低限のセキュリティ設定込みで見ます。
この章の目次
どっちを使う?
| RDS | DynamoDB | |
|---|---|---|
| データモデル | 表(リレーショナル) | キー・値(NoSQL) |
| クエリ | SQL(柔軟) | キー指定 / GSI(限定的) |
| 料金 | インスタンス常時稼働 | 従量(リクエスト数 + ストレージ) |
| 運用 | スケール手動/停止可 | サーバ管理ゼロ |
| 得意 | 複雑なクエリ、トランザクション | セッション / TTL / 高 QPS |
RDS 最小例(PostgreSQL)
# DB Subnet Group: RDS をどの subnet に配置できるかの集合
# for 式で aws_subnet.private の全要素の id を集めて list 化
resource "aws_db_subnet_group" "main" {
name = "main"
subnet_ids = [for s in aws_subnet.private : s.id]
}
# RDS 用 SG(最小限の空 SG。下で ingress ルールを別資源として追加)
resource "aws_security_group" "rds" {
name_prefix = "rds-"
vpc_id = aws_vpc.main.id
}
# 「アプリ層 SG からだけ 5432 (PostgreSQL) を許可」
# referenced_security_group_id で SG 間の許可を表現(CIDR より安全)
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"
}
# RDS インスタンス本体
# 必須引数: identifier, engine, instance_class, allocated_storage, username, password
resource "aws_db_instance" "main" {
identifier = "myapp-prd"
engine = "postgres"
engine_version = "16.4"
instance_class = "db.t4g.micro" # Graviton ベースの最小インスタンス
allocated_storage = 20 # GB
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 # public IP を割り当てない
# 削除時に最終スナップショットを取る(false=スキップ)
skip_final_snapshot = false
final_snapshot_identifier = "myapp-prd-final"
}
本番向け RDS の必須設定
# 本番グレードの RDS:HA・暗号化・バックアップ・監視・削除保護を全部入り
# 上の最小例に対して、本番運用で「必ず」入れたい項目だけ追加した形
resource "aws_db_instance" "prd" {
# ... 基本項目は上と同じ ...
# マルチ AZ 構成(別 AZ にスタンバイを置いて自動フェイルオーバ)
multi_az = true # 別 AZ にスタンバイ
# バックアップ保持期間(日)。0 だと自動バックアップ無効になるので注意
backup_retention_period = 30 # バックアップ 30 日保持
# バックアップ取得時間帯(UTC)。サービス低負荷時間に寄せる
backup_window = "16:00-17:00" # JST 1:00-2:00 想定
# メンテナンス(パッチ適用など)時間帯(UTC)
maintenance_window = "Sun:17:00-Sun:18:00"
# ストレージを KMS CMK で暗号化(顧客管理キーで監査ログも取れる)
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
# Performance Insights(クエリレベルの性能分析ダッシュボード)
performance_insights_enabled = true
performance_insights_retention_period = 7 # 無料枠は 7 日
# スロークエリ等を CloudWatch Logs に流す
enabled_cloudwatch_logs_exports = ["postgresql"]
# AWS 側の削除保護:consoleや API での delete を拒否
deletion_protection = true # destroy がエラーに
# スナップショットにもタグを引き継ぐ
copy_tags_to_snapshot = true
# Terraform 側でも二重保護
lifecycle {
# terraform destroy / replace を阻止(コードを消しても破壊できない)
prevent_destroy = true # Terraform 側でも保護
# password は Secrets Manager で別管理 → diff を出さない
ignore_changes = [password] # 外部で変える運用に
}
}
本番 DB の保護
deletion_protection = true(AWS 側)と lifecycle.prevent_destroy = true(Terraform 側)の二重保護が定石。terraform destroy で一発消去事故を防ぎます。
パスワードを Secrets Manager で管理
# 強力なランダムパスワードを Terraform で生成(state にだけ残り、コードには出ない)
resource "random_password" "db" {
length = 32 # 32 文字
special = true # 記号も含める
}
# Secrets Manager の「箱」(中身は別資源で書く)
resource "aws_secretsmanager_secret" "db" {
name = "myapp/db/master" # アプリ側からはこの名前で取得する
}
# 箱に中身(接続情報の JSON)を入れる
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
# jsonencode でマップ → JSON 文字列。アプリは parse して使う
secret_string = jsonencode({
username = "postgres"
password = random_password.db.result # 生成した値を埋め込む
host = aws_db_instance.main.address # RDS のエンドポイント
port = aws_db_instance.main.port # 5432
})
}
# RDS 本体側もランダム生成値を参照(同じ値が両方に入る)
resource "aws_db_instance" "main" {
# ...
username = "postgres"
password = random_password.db.result # tfvars に書かなくてよくなる
}
アプリは aws_secretsmanager_secret_version から接続情報を取得します。Terraform 経由でパスワードを生成すれば、.tfvars に書く必要すらありません。
DynamoDB
テーブル定義のキモは 「key(hash_key・range_key)」と「attribute」の宣言。attribute は「key で使う列の型を予告」するだけで、他の列は自由に書き込めます(スキーマレス)。
# セッションストア用 DynamoDB テーブル
# 単一キー(session_id)で取得 + TTL で自動削除 + バックアップ + 暗号化
resource "aws_dynamodb_table" "sessions" {
name = "sessions"
billing_mode = "PAY_PER_REQUEST" # 従量制(PROVISIONED ではなく)
hash_key = "session_id" # パーティションキー(必須)
# hash_key / range_key / GSI で使う列は、ここで型を予告する必要がある
# それ以外の列は attribute 不要で自由に書き込める(スキーマレス)
attribute {
name = "session_id"
type = "S" # String("N"=Number, "B"=Binary)
}
# TTL: expires_at(epoch 秒)が現在時刻を過ぎたアイテムを自動削除
ttl {
attribute_name = "expires_at"
enabled = true
}
# PITR(Point-In-Time Recovery):過去 35 日以内の任意の時点に復元可能
point_in_time_recovery {
enabled = true
}
# 保管時の暗号化(デフォルトは AWS 所有キーだが、これで AWS マネージドキーに)
server_side_encryption {
enabled = true
}
tags = { Name = "sessions" }
}
billing_mode の選び方
| PAY_PER_REQUEST | PROVISIONED | |
|---|---|---|
| 料金 | リクエストごと | 容量予約 + Auto Scaling |
| 突発負荷 | 強い | 容量超過でスロットリング |
| 低負荷時のコスト | ほぼ 0 | 予約分は常に発生 |
| 初学者向け | こちら推奨 | 定常負荷が読めてから |
GSI(グローバルセカンダリインデックス)
主キー以外で検索したい時に追加するインデックス。
# users テーブル:主キーは user_id だが、email でも検索したい → GSI を追加
resource "aws_dynamodb_table" "users" {
name = "users"
billing_mode = "PAY_PER_REQUEST"
hash_key = "user_id" # 主キー(partition key)
# 主キーで使う列の型予告
attribute {
name = "user_id"
type = "S"
}
# GSI で使う列も attribute 宣言が必要
attribute {
name = "email"
type = "S"
}
# GSI(グローバルセカンダリインデックス):別の列を主キー扱いで検索できる
global_secondary_index {
name = "by-email" # インデックス名(query 時に index-name で指定)
hash_key = "email" # この列で検索できるようになる
# ALL=全列をインデックスにコピー / KEYS_ONLY=キーだけ / INCLUDE=指定列のみ
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、の順で考える。