★★ 中級

06. RDS / DynamoDB

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

どっちを使う?

RDSDynamoDB
データモデル表(リレーショナル)キー・値(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_REQUESTPROVISIONED
料金リクエストごと容量予約 + 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、の順で考える。