DynamoDB テーブル設計と PII 配慮 — On-demand と PITR
本シリーズの問い合わせフォームは、入力データを DynamoDB に保存します。RDS じゃなく DynamoDB を選んだ理由・PK 選定の考え方・PITR や暗号化の設定について書きます。
なぜ RDS ではなく DynamoDB か
個人ポートフォリオ規模で「データを保存する」と聞くと、最初に思い浮かぶのが RDS。でも本シリーズでは DynamoDB を採用しました:
| 観点 | RDS | DynamoDB (本シリーズ採用) |
|---|---|---|
| 料金(無風時) | 最小 t4g.micro でも 月 $13〜 | On-demand なら 0 円 |
| Lambda との相性 | 同時接続枯渇問題(RDS Proxy 必須) | HTTP API なので問題なし |
| 運用 | マイナーバージョン適用必要 | 完全マネージド |
| スキーマ | 固定スキーマ | スキーマレス |
| クエリ | SQL(柔軟) | PK/SK ベース(制約あり) |
| 個人サイト規模 | 過剰 | 最適 |
本シリーズのデータは 「問い合わせを 1 件保存・あとで一覧で見る」 程度なので、DynamoDB の制約はまったく問題になりません。
テーブル設計
contacts テーブルの設計:
| 属性 | 型 | 用途 |
|---|---|---|
id (PK) | String | UUID v4 で一意 |
name | String | 名前 |
email | String | メールアドレス |
message | String | 本文 |
created_at | String (ISO 8601) | 作成時刻 |
source_ip | String | 送信元 IP(スパム対策の参考) |
user_agent | String | UA 文字列 |
Terraform 定義
resource "aws_dynamodb_table" "contacts" {
name = "${var.name_prefix}-contacts"
billing_mode = "PAY_PER_REQUEST" # On-demand
hash_key = "id" # PK
attribute {
name = "id"
type = "S" # String
}
point_in_time_recovery {
enabled = true # PITR 有効
}
server_side_encryption {
enabled = true # SSE 有効(デフォルト鍵)
}
tags = var.tags
}
PK と PITR・暗号化だけ書けば動きます。短い。
PK に email を入れない理由(PII 配慮)
初期設計では「PK = email」も検討しました。「同じメアドからの 2 回目はどう扱う?」と考える時にメアドベースが楽だから。でも次の理由で UUID PK + email は通常属性 にしました:
- email は 個人特定情報 (PII)。PK にすると CloudWatch メトリクスや CloudTrail に値が出る可能性
- 本人の「メアド変更」要求に対応しづらい(PK は変更不可)
- 同じメアドから複数回問い合わせるのは正常(1 回目 / 2 回目で別レコードにしたい)
- UUID なら衝突しない、URL 等にも安全に出せる
billing_mode の選び方
| モード | 料金 | 適合ケース |
|---|---|---|
PAY_PER_REQUEST (On-demand) | 読み書き 1 回ごとに課金 | アクセス薄い・予測困難 |
PROVISIONED | キャパシティ予約 + 超過分従量 | 定常的に高負荷・読み書き量予測可能 |
本シリーズの「月数件の問い合わせ」想定なら On-demand 一択。月額 0〜数円。Provisioned だと最低でも月 $1〜2 が確実に発生する。
Point-in-Time Recovery (PITR)
PITR を有効化すると、過去 35 日間の任意の時点までテーブルを復旧できます:
- 誤って
delete-itemした時の救済 - アプリのバグで全レコード上書きしてしまった時
- 本番運用で「数時間前の状態に戻したい」要件
料金: 保存サイズに応じて月額 $0.20/GB 程度。本シリーズ規模では実質無料。
暗号化(SSE)
server_side_encryption {
enabled = true
}
これだけで AWS 管理キーで自動暗号化されます。料金 0 円。コンプライアンス(PCI-DSS / SOC2 等)でカスタマー管理キー (CMK) が必要なら、kms_key_arn を追加で指定。
Lambda からの書込
Lambda 側のコード(抜粋):
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "node:crypto";
const ddbClient = new DynamoDBClient({}); // ハンドラ外で初期化(再利用最適化)
const ddb = DynamoDBDocumentClient.from(ddbClient);
const TABLE_NAME = process.env.DDB_TABLE;
export const handler = async (event) => {
const payload = JSON.parse(event.body);
const item = {
id: randomUUID(), // UUID v4
name: payload.name.trim(),
email: payload.email.trim().toLowerCase(),
message: payload.message.trim(),
created_at: new Date().toISOString(),
source_ip: event.requestContext?.http?.sourceIp,
user_agent: event.headers?.["user-agent"],
};
await ddb.send(new PutCommand({ TableName: TABLE_NAME, Item: item }));
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
};
ポイント:
- クライアントはハンドラ外で初期化。Lambda コンテナが再利用される時に同じインスタンスを使い回せる(cold start 短縮)
- UUID v4 で id 生成。Node.js 標準の
crypto.randomUUID() - email は trim + lowercase。「Foo@Example.com」と「foo@example.com」を同一視
- 環境変数からテーブル名取得。Terraform の
environment.variablesで渡される
動作確認とデータ閲覧
# 件数確認
aws dynamodb scan --table-name iigtn-lab-prod-contacts --select COUNT
# 直近 5 件取得
aws dynamodb scan --table-name iigtn-lab-prod-contacts --max-items 5
# 1 件取得(id 指定)
aws dynamodb get-item --table-name iigtn-lab-prod-contacts \
--key '{"id":{"S":"<UUID>"}}'
件数が増えてきたら(数千件超)scan ではなく query + GSI(Global Secondary Index)で時系列取得するのが定石。本シリーズではまだ追加していない。
後で GSI を追加する想定
「最新の問い合わせ 10 件を表示したい」要件が出たら、GSI を追加します:
# GSI: created_at で時系列ソート
attribute {
name = "pk_dummy"
type = "S"
}
attribute {
name = "created_at"
type = "S"
}
global_secondary_index {
name = "created_at-index"
hash_key = "pk_dummy"
range_key = "created_at"
projection_type = "ALL"
}
pk_dummy に「contact」固定値を入れることで、全件を 1 つのパーティションに集約 → created_at でソート可能、という DynamoDB 定番テクニック。
次の記事
API Gateway + Lambda + DynamoDB が動いたら、次は 「https://lab.iigtn.com/api/contact という同一ドメインで API を公開する」 構成を作ります。CloudFront に /api/* 用 behavior を追加して API GW にルーティングする方法を解説します。
📚 用語集
- DynamoDB
- AWS のフルマネージド NoSQL データベース(KVS)。スキーマレス・自動スケール・サーバレス。
- RDS (Relational Database Service)
- AWS の RDB マネージドサービス。MySQL / PostgreSQL / Oracle 等。インスタンスベースで常時課金。
- RDS Proxy
- Lambda と RDS の同時接続枯渇を解消する中継プロキシ。Lambda + RDS の本番では事実上必須。
- PK (Partition Key)
- DynamoDB の主キー。全アイテムを一意に識別する。Hash Key とも呼ぶ。
- SK (Sort Key)
- DynamoDB の補助キー。同じ PK の中で並び順を決める。Range Key とも呼ぶ。
- GSI (Global Secondary Index)
- DynamoDB の追加インデックス。PK / SK と違うキーで検索できる。
- scan
- DynamoDB テーブルの全件取得 API。コスト高いので件数増えたら避ける。
- query
- DynamoDB の PK 指定取得 API。scan より遥かに高速・安価。
- PutItem
- DynamoDB にアイテム 1 件を書き込む API。
- billing_mode
- DynamoDB の課金モード設定。
PAY_PER_REQUEST(On-demand)/PROVISIONED。 - On-demand (PAY_PER_REQUEST)
- 読み書き 1 回ごとに課金される DynamoDB 課金モード。アクセス薄いなら最安。
- Provisioned
- キャパシティ予約型の DynamoDB 課金モード。定常負荷が予測できるなら安くなる。
- PITR (Point-in-Time Recovery)
- DynamoDB の連続バックアップ機能。過去 35 日間の任意時点に復旧可能。
- SSE (Server-Side Encryption)
- DynamoDB / S3 の保存時自動暗号化。AWS 管理キー(無料)/ KMS マネージド / カスタマー管理 (CMK) から選べる。
- PII (Personally Identifiable Information)
- 個人特定情報。氏名・メール・電話・住所・マイナンバー等。GDPR / 個人情報保護法でも管理が義務付けられる。
- UUID v4
- ランダムベースの 128 ビット一意識別子。
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx形式。衝突確率は実質 0。 - DocumentClient (AWS SDK)
- DynamoDB のラッパークライアント。型変換(
{"S": "foo"}↔"foo")を自動でやってくれる。 - ハンドラ外初期化
- Lambda の handler function の外で AWS SDK クライアントを初期化するパターン。Lambda コンテナ再利用時に高速化される。
- ISO 8601
- 日時の国際標準形式。
2026-04-26T13:00:00.000Zのように表現。DynamoDB に時刻を String で入れる時の定番。