Use Cases (更新: 2026/6/3)

Claude CodeとAWS DynamoDB実践ガイド: テーブル設計から安全な更新まで

Claude CodeでDynamoDB設計と実装を進める実践手順。キー設計、TTL、IAM、コストの落とし穴まで。

Claude CodeとAWS DynamoDB実践ガイド: テーブル設計から安全な更新まで

Claude Codeに「DynamoDBを使うAPIを作って」と頼むだけでは、あとで高い確率で詰まります。DynamoDBはスキーマレスに見えますが、実務では最初のキー設計がほぼ設計書です。リレーショナルDBのように後からJOINで逃げる前提で作ると、Scanが増え、コストが読めず、アクセスが集中するパーティションキーでスロットリングにぶつかります。

この記事では、Claude Codeを「コード生成係」ではなく「設計レビューの相棒」として使い、DynamoDBのテーブル設計、パーティションキー、単一テーブル設計とシンプル設計の分け方、条件付き書き込み、TTL、ローカル検証、IAM、コスト管理までを一通りつなげます。Masaが検証するときも、最初にClaude Codeへ渡すのは「テーブルを作って」ではなく「アクセスパターン、失敗条件、読み書き量を表にして」です。この順番にすると、AIがそれらしいNoSQL用語を並べるだけの出力をかなり減らせます。

公式仕様は必ず原典で確認してください。この記事ではAWS公式のDynamoDB data modeling foundationspartition key best practicesQuery key condition expressionscondition expressionsTTLDynamoDB localthroughput capacityIAM fine-grained access controlを基準にします。Lambda連携はClaude Code × AWS Lambda完全ガイド、権限設計はClaude Code × AWS IAM完全ガイド、ログ運用はClaude Code × AWS CloudWatch活用も合わせて読むと、実装後の運用まで見通せます。

Claude Codeに任せる前に決めること

DynamoDBの初心者が最初に覚えるべき言葉は「アクセスパターン」です。アクセスパターンとは、アプリが実際に行う読み書きの形です。例えば「プロジェクトのタスク一覧を開く」「タスクを完了にする」「ユーザーのセッションを7日で消す」「Webhookの二重処理を防ぐ」といった、画面やAPIから見える具体的な操作です。

Claude Codeには、次のように頼むと設計の抜けが見えます。

このアプリのDynamoDB設計をレビューしてください。
前提:
- プロジェクトごとにタスクを一覧表示する
- タスクIDで1件更新する
- ユーザーセッションは7日で期限切れにする
- Webhookイベントは同じeventIdを二重処理しない

出力:
1. アクセスパターン表
2. PK/SK案
3. Queryで読めるものと読めないもの
4. 条件付き書き込みが必要な箇所
5. ホットパーティションとコストのリスク

ここで大事なのは、Claude Codeに最初からCDKやアプリコードを書かせないことです。DynamoDBではQueryがパーティションキーの等価条件を必要とし、必要に応じてソートキー条件で絞ります。つまり「あとで好きな列で検索する」設計ではなく、「どのキーで読むか」を先に決める設計です。Claude Codeの出力にScanが頻繁に出てきたら、設計がまだ固まっていない合図です。

シンプル設計か単一テーブル設計か

単一テーブル設計は、複数のエンティティを1つのテーブルに入れ、PKSKのプレフィックスで種類を表す設計です。たとえばPROJECT#alpha配下にMETATASK#001EVENT#...を並べます。関連データを1回のQueryで取れる利点がありますが、初心者には難しく、IAMやStreams、バックアップの境界も1つになります。

一方、シンプル設計は「タスクテーブル」「セッションテーブル」のように用途ごとに分ける考え方です。AWS公式のデータモデリング資料でも、単一テーブル設計と複数テーブル設計にはそれぞれ利点と欠点があります。Claude Codeに任せるなら、最初から複雑な単一テーブルに寄せるのではなく、次の基準で判断します。

判断軸シンプル設計単一テーブル設計
初期開発理解しやすい設計レビューが必要
一覧画面1種類のデータ中心なら十分複数種類を同時に読むなら強い
IAMテーブル単位で分けやすい条件キーの設計が重要
変更耐性テーブル追加で逃げやすいキー命名の一貫性が重要
Claude Code向き初心者のMVP向きアクセスパターンが固い業務向き

この記事のサンプルは、学習しやすい「単一テーブル寄りのシンプル設計」にします。テーブルは1つだけですが、扱うユースケースをプロジェクト、タスク、セッションに限定します。いきなり全業務を1テーブルへ詰め込まず、小さな範囲でキー設計を試すのが現実的です。

ClaudeCodeLabDemo

PK                 SK                   entityType
PROJECT#alpha      META                 Project
PROJECT#alpha      TASK#task-001        Task
USER#u-001         SESSION#s-001        Session
WEBHOOK#stripe     EVENT#evt_001        WebhookEvent

Queryの考え方:
- プロジェクトのタスク一覧: PK = PROJECT#alpha AND begins_with(SK, TASK#)
- ユーザーのセッション確認: PK = USER#u-001 AND begins_with(SK, SESSION#)
- Webhookの重複防止: 条件付きPutで同じPK/SKを作らせない

3つ以上の実用ユースケース

1つ目はタスク管理です。プロジェクト詳細画面でタスクを一覧表示し、担当者が完了にします。PKPROJECT#projectIdにすると、同じプロジェクトのタスクをQueryでまとめて読めます。SKTASK#taskIdにすれば、begins_withでタスクだけを取り出せます。

2つ目はセッションや一時トークンです。ログインセッション、招待リンク、パスワードリセットトークンは、永遠に残してはいけません。DynamoDB TTLは、各アイテムにUnix epoch秒の数値属性を持たせると、期限切れ後に自動削除される機能です。ただし、期限時刻ぴったりに消えるわけではなく、期限切れ後もしばらく読める可能性があります。アプリ側でも期限を確認する設計にしてください。

3つ目はWebhookの冪等性です。冪等性とは、同じ処理が複数回来ても結果を壊さない性質です。決済、メール配信、外部SaaS連携では同じイベントが再送されることがあります。WEBHOOK#providerEVENT#eventIdをキーにして、attribute_not_existsの条件付きPutItemを使えば、最初の1回だけ処理できます。

4つ目を挙げるなら、レート制限です。PK = RATE#userIdSK = WINDOW#2026-06-03T10:00のように時間窓を持たせ、UpdateItemでカウントを増やします。ここはホットパーティションになりやすいので、高頻度APIではDynamoDBだけでなく、CloudFront、API Gateway、ElastiCacheなどとの役割分担も検討します。

ローカルで動かす最小セット

まずDynamoDB Localを起動します。AWS公式ドキュメントではDocker Composeの例も紹介されています。ローカルでは本物のAWS認証情報は不要ですが、SDKやCLIが認証情報とリージョンを要求するため、ダミー値を入れます。

services:
  dynamodb-local:
    image: "amazon/dynamodb-local:latest"
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal
docker compose up -d
export AWS_ACCESS_KEY_ID=fakeMyKeyId
export AWS_SECRET_ACCESS_KEY=fakeSecretAccessKey
export AWS_REGION=us-west-2

テーブルを作ります。本番で試す前に、必ず--endpoint-url http://localhost:8000が付いていることを確認してください。

aws dynamodb create-table \
  --table-name ClaudeCodeLabDemo \
  --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S \
  --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST \
  --endpoint-url http://localhost:8000 \
  --region us-west-2

TTL属性も定義します。ローカルではTTLの削除タイミングを本番と同じように期待せず、属性の型とアプリ側の期限判定を確認する目的で使います。

aws dynamodb update-time-to-live \
  --table-name ClaudeCodeLabDemo \
  --time-to-live-specification "Enabled=true,AttributeName=expiresAt" \
  --endpoint-url http://localhost:8000 \
  --region us-west-2

Node.jsの依存関係を入れます。

npm init -y
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

コピペで動くDynamoDB実装

次のapp.mjsは、タスク作成、タスク一覧、条件付き更新、TTL付きセッション作成をまとめた最小例です。Claude Codeに改善を頼む場合も、まずこのサイズで動かしてから、APIルートやLambdaへ移すほうが安全です。

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  PutCommand,
  QueryCommand,
  UpdateCommand,
} from "@aws-sdk/lib-dynamodb";

const TABLE_NAME = process.env.TABLE_NAME ?? "ClaudeCodeLabDemo";
const isLocal = process.env.DDB_LOCAL !== "0";

const client = new DynamoDBClient({
  region: process.env.AWS_REGION ?? "us-west-2",
  ...(isLocal
    ? {
        endpoint: "http://localhost:8000",
        credentials: {
          accessKeyId: "fakeMyKeyId",
          secretAccessKey: "fakeSecretAccessKey",
        },
      }
    : {}),
});

const ddb = DynamoDBDocumentClient.from(client, {
  marshallOptions: { removeUndefinedValues: true },
});

const nowIso = () => new Date().toISOString();
const ttlAfterDays = (days) => Math.floor(Date.now() / 1000) + days * 86400;
const taskKey = (projectId, taskId) => ({
  PK: `PROJECT#${projectId}`,
  SK: `TASK#${taskId}`,
});

async function createTask({ projectId, taskId, title, ownerId }) {
  const item = {
    ...taskKey(projectId, taskId),
    entityType: "Task",
    title,
    ownerId,
    status: "OPEN",
    createdAt: nowIso(),
    updatedAt: nowIso(),
  };

  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: item,
      ConditionExpression: "attribute_not_exists(PK) AND attribute_not_exists(SK)",
    }),
  );

  return item;
}

async function listProjectTasks(projectId) {
  const result = await ddb.send(
    new QueryCommand({
      TableName: TABLE_NAME,
      KeyConditionExpression: "PK = :pk AND begins_with(SK, :taskPrefix)",
      ExpressionAttributeValues: {
        ":pk": `PROJECT#${projectId}`,
        ":taskPrefix": "TASK#",
      },
      ReturnConsumedCapacity: "TOTAL",
    }),
  );

  console.log("consumed capacity:", result.ConsumedCapacity);
  return result.Items ?? [];
}

async function completeTask({ projectId, taskId, expectedOwnerId }) {
  const result = await ddb.send(
    new UpdateCommand({
      TableName: TABLE_NAME,
      Key: taskKey(projectId, taskId),
      UpdateExpression: "SET #status = :done, updatedAt = :now",
      ConditionExpression: "ownerId = :ownerId AND #status <> :done",
      ExpressionAttributeNames: {
        "#status": "status",
      },
      ExpressionAttributeValues: {
        ":done": "DONE",
        ":ownerId": expectedOwnerId,
        ":now": nowIso(),
      },
      ReturnValues: "ALL_NEW",
    }),
  );

  return result.Attributes;
}

async function createSession({ userId, sessionId }) {
  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: {
        PK: `USER#${userId}`,
        SK: `SESSION#${sessionId}`,
        entityType: "Session",
        createdAt: nowIso(),
        expiresAt: ttlAfterDays(7),
      },
      ConditionExpression: "attribute_not_exists(PK) AND attribute_not_exists(SK)",
    }),
  );
}

async function main() {
  const projectId = "alpha";
  const taskId = `task-${Date.now()}`;

  await createTask({
    projectId,
    taskId,
    title: "Review DynamoDB key design",
    ownerId: "masa",
  });

  await createSession({
    userId: "masa",
    sessionId: `session-${Date.now()}`,
  });

  console.log(await listProjectTasks(projectId));
  console.log(
    await completeTask({
      projectId,
      taskId,
      expectedOwnerId: "masa",
    }),
  );
}

main().catch((error) => {
  if (error.name === "ConditionalCheckFailedException") {
    console.error("Condition failed:", error.message);
    process.exit(2);
  }

  console.error(error);
  process.exit(1);
});
DDB_LOCAL=1 node app.mjs

この例でClaude Codeに追加実装を頼むなら、次のように制約を付けます。

app.mjsをLambdaハンドラーに分割してください。
条件:
- PutCommandのConditionExpressionを消さない
- QueryはPKの等価条件から始める
- Scanを追加する場合は理由と代替案を書く
- ReturnConsumedCapacityを開発環境では残す
- TTLのexpiresAtはUnix epoch秒のNumberのままにする

IAMはテーブル名だけで終わらせない

DynamoDBのIAMは、dynamodb:Querydynamodb:PutItemを許可するだけでは粗すぎます。テーブルを1つにまとめるほど、どのパーティションキーへアクセスしてよいかが重要になります。AWS公式の細粒度アクセス制御では、dynamodb:LeadingKeysを使ってパーティションキーの先頭値を制限できます。

次はプロジェクトIDをプリンシパルタグで渡す前提の例です。実際にはアカウントID、リージョン、テーブル名、タグ付与方法を自社環境に合わせてください。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ProjectScopedDynamoDBAccess",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query"
      ],
      "Resource": [
        "arn:aws:dynamodb:ap-northeast-1:123456789012:table/ClaudeCodeLabProd",
        "arn:aws:dynamodb:ap-northeast-1:123456789012:table/ClaudeCodeLabProd/index/*"
      ],
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": [
            "PROJECT#${aws:PrincipalTag/projectId}"
          ]
        }
      }
    }
  ]
}

ここでの落とし穴は、Scanを許可するとパーティションキー制限の意図が崩れやすいこと、属性レベル制御を使う場合はProjectionExpressionReturnValuesまで考える必要があることです。Claude CodeにIAMポリシーを作らせたら、「なぜScanが必要か」「index ARNを含めているか」「本番と検証のテーブルを分けているか」を必ずレビューします。

コストとホットパーティションの落とし穴

DynamoDBのオンデマンドモードは、多くの新規ワークロードで始めやすい選択です。リクエストごとの課金で、最初から読み書きキャパシティを見積もる必要がありません。一方で、アクセスパターンが予測できる定常負荷なら、プロビジョンドモードのほうがコストを制御しやすい場合があります。Claude Codeに「安い設計にして」と頼むだけでは不十分で、読み取り回数、アイテムサイズ、ピークの出方を渡す必要があります。

特に危険なのはホットパーティションです。AWS公式のベストプラクティスでは、パーティションキー全体に均等なアクセスを分散することが重要だと説明されています。単一パーティションには設計上の上限があるため、PK = GLOBALPK = TODAYのような全ユーザーが同じキーに集まる設計は避けます。

よくある失敗例は次の通りです。

  • Scanで一覧を作り、データが増えた瞬間に遅くなる
  • FilterExpressionをWHERE句の代わりに使い、読み取り後に大量に捨てている
  • USER#adminPROJECT#defaultにアクセスが集中する
  • TTLを「指定時刻に必ず消えるジョブ」だと思い込む
  • GSIを後付けすれば何でも解決できると考える
  • 条件付き書き込みを使わず、Webhookや注文処理を二重実行する
  • ローカルではendpointを付けたのに、本番コードへ残してしまう

Claude Codeへのレビュー依頼は、次の一文を入れるだけで精度が上がります。

このDynamoDB実装について、Scan依存、ホットパーティション、TTL誤解、条件付き書き込み不足、IAM過剰権限、オンデマンド課金の暴走リスクを指摘してください。修正案はコード差分ではなく、まず表で出してください。

公開前チェックリスト

実装を公開する前に、少なくとも次を確認します。

  • 主要な読み取りがQueryで表現できる
  • KeyConditionExpressionがパーティションキーの等価条件を含む
  • 重複登録や状態遷移にConditionExpressionがある
  • TTL属性がNumber型のUnix epoch秒になっている
  • 開発環境だけがDynamoDB Localのendpointを使う
  • 本番IAMがScan*に寄りすぎていない
  • CloudWatchでスロットリング、ConsumedCapacity、エラーを見られる
  • 想定ピーク時のパーティションキー分布を説明できる

このあたりをチーム標準にするなら、Claude Code研修・導入相談で既存リポジトリを題材にレビュー観点を整理できます。個人でプロンプト、チェックリスト、レビュー文面を増やしたい場合はClaudeCodeLabの商品一覧からテンプレートを選ぶと、毎回ゼロからClaude Codeに説明し直す手間を減らせます。

まとめ

DynamoDBは「スキーマを考えなくてよいDB」ではありません。むしろ、アクセスパターン、パーティションキー、ソートキー、条件付き書き込み、TTL、IAM、コストを最初に決めるほど安定します。Claude Codeは、その設計を速く文章化し、コードへ落とし込むには強力です。ただし、Scanだらけの実装や、ホットパーティションを生むキー設計まで自動で避けてくれるわけではありません。

実際に試した結果、最も効果があったのは「最初にアクセスパターン表を作らせる」「次にPK/SKと失敗条件をレビューさせる」「最後にローカルDynamoDBで条件付き書き込みを実行する」という順番でした。特にattribute_not_existsでWebhookの二重処理を止める例は、Claude Codeが生成したAPIコードのレビュー観点としてそのまま使えます。DynamoDBをClaude Codeで扱うなら、速く書くことよりも、読めるキー設計と失敗しない更新条件を先に固定してください。

#claude-code #aws #dynamodb #nosql #typescript #database
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。