amazon-dynamodb について
エージェントや開発者向けに、DynamoDBのデータレイヤーを設計・検証するホスト非依存のスキルです。設計原則に加え、任意でコスト見積もり、AWSへのデプロイ、ベンチマーク、レポート、リソース削除を行えます。
主な機能
- 01アクセスパターンとスキーマの設計
- 02データモデルからのコスト見積もり
- 03検証用AWSリソースのデプロイ
- 04ベンチマークと性能レポート
- 05確認用の削除スクリプト生成
活用シーン
- 01DynamoDBアプリケーションのデータレイヤー設計
- 02モデル化したワークロードのコスト見積もり
- 03AWSのベンチマークによる設計検証
MCP 対応
AWSとのやり取りにはAWS MCPサーバーを推奨していますが、必須ではありません。スクリプトは環境内の認証情報を使ってboto3とAWS CLIからも実行できます。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ aws/ agent-toolkit-for-aws --skill amazon-dynamodbインストール後、Claude Code で/amazon-dynamodbを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/cost-model-schema.mdloop-state-schema.mdperformance-model-schema.mdperformance-report-format.mdreference-architecture.mdvector-search.mdscripts/benchmark_lambda.pybenchmark_model.pycalculate_costs.pydeploy_model.pyfind_skill_dir.shgenerate_perf_report.pygenerate_teardown.pyiterate_design.py詳細
DynamoDB の公理
このドキュメントは、DynamoDB アプリケーションの設計公理をまとめたものです。アプリケーションについてほかの情報を持たないエージェントが読み、根拠のあるデータレイヤー設計を作成するためのものです。
ガードレール — このスキル独自のファイルの場所(MCP とローカルインストール)
このスキルは 2 通りの方法で読み込めます。それぞれ、スキルに同梱されたファイルの参照先が異なります。リファレンスを読む前、またはスクリプトを実行する前に、スキルがどの方法で読み込まれたかを確認してください。
- AWS MCP の
retrieve_skillツール経由で読み込まれた場合: このスキルはローカルファイルシステムにインストールされていません。各リファレンスまたはスクリプトは、fileパラメーターを指定してretrieve_skill経由で取得する必要があります(例:file="references/vector-search.md"またはfile="scripts/calculate_costs.py")。スクリプトは返されたコンテンツから実行してください。これらのパスはディスク上に存在しないため、ローカルでfile_readを使わないでください。 - ローカルにインストールされている場合(例:
.kiro/skills/amazon-dynamodb/または~/.claude/skills/amazon-dynamodb/):相対パスを使い、ローカルのスキルディレクトリからファイルを読み込んで実行してください。
この区別は、スキルに同梱されたファイルにのみ適用されます。ユーザーデータとセッションの成果物は、常にユーザーの作業ディレクトリから読み込み、そこに書き込んでください。retrieve_skill を使って顧客データを取得または書き込まないでください。
これらのファイルの場所とは別に、このスキルは設計の検証、スクラッチテーブルのデプロイ、またはベンチマークの実施時に実際の AWS API を呼び出します。AWS とのやり取りには、利用可能な場合は AWS MCP サーバーを推奨します。ホスト間で一貫した認証情報の処理とリージョンの解決が可能になるためです。必須ではありません。ここにあるすべてのスクリプトは、環境に設定された認証情報チェーンを使って boto3 と AWS CLI を実行するため、MCP サーバーがなくてもスキルを変更せずに利用できます。
以下の ${SKILL_DIR} の解決は、ローカルにインストールされている場合に適用されます。MCP 経由では解決するディレクトリがなく、このスキルに同梱されたスクリプトは返された文字列から実行できないため、コスト見積もりやライブ検証にはローカルにインストールされた形式が必要です。
スキル自身のパスを解決する
このスキルはホストに依存せず、Claude Code、Kiro、Codex、Cursor、通常のターミナル、CI で動作します。ディスク上の保存場所はホストによって異なります(~/.claude/skills/…、~/.kiro/…、~/.codex/…、~/.cursor/…、リポジトリのチェックアウト先など、どこでもかまいません)。エージェントの作業ディレクトリはユーザーのプロジェクトであり、スキルのバンドルではないため、scripts/calculate_costs.py のような相対パスは解決できません。このドキュメント全体で、${SKILL_DIR} はこの SKILL.md ファイルを含むディレクトリの絶対パス(scripts/ と references/ を含むスキルのルートディレクトリ)を指します。
セッションごとに一度 ${SKILL_DIR} を解決し、その後は再利用してください。 使用環境で機能する最初の方法を選んでください。
-
すでに場所がわかっている場合。 SKILL.md をパスから読み込んだ場合、
${SKILL_DIR}はそのファイルが置かれているディレクトリです。これが最も信頼できる情報源なので、優先してください。 -
環境変数。
$DDB_SKILL_DIRが設定されている場合は、その値を信頼してください。 -
同梱のリゾルバー(ホストに依存せず、特定のホストを前提としません)。一般的なインストール先を検索し、さらに目印となるファイルで見つかった場所を検証するため、誤ったディレクトリを気づかないまま返すことはありません。
# If you already know the path to the script, just run it directly: # SKILL_DIR="$(sh /path/to/amazon-dynamodb/scripts/find_skill_dir.sh)" # If you don't, this host-neutral one-liner searches the common roots # (~/.claude, ~/.kiro, ~/.codex, ~/.cursor, ~/.config, ~/.local/share, $PWD): SKILL_DIR="$(find "$HOME" "$PWD" -maxdepth 7 -type f -name SKILL.md -path '*amazon-dynamodb*' 2>/dev/null \ | head -1 | xargs -I{} dirname {})" # Verify it before trusting it (sentinel check), then hand off to the resolver # for its loud-on-failure diagnostics: SKILL_DIR="$(sh "$SKILL_DIR/scripts/find_skill_dir.sh" 2>/dev/null || echo "$SKILL_DIR")"リゾルバーは検証済みのスキルルートを出力して終了コード 0 で終了します。見つからない場合は何も出力せず、修正方法を示すメッセージとともに非ゼロで終了します。そのため、成功時の
SKILL_DIR="$(sh …/find_skill_dir.sh)"は信頼できます。POSIXshのみを使用しているため、どのホストでも同じように動作します。
解決できたら、エクスポートして、以降のすべてのコマンドで簡単に置き換えられるようにし、スクリプトからも参照できるようにしてください。
export DDB_SKILL_DIR="$SKILL_DIR"
python3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.md
スクリプトは内部で、自身と同じ場所にあるファイル(ほかのスクリプトや scripts/benchmark_lambda.py)を基準にして関連ファイルを見つけるため、必要なのはルートのパスだけです。個々のスクリプトのパスを指定する必要はありません。
ルール:
- スクリプトは常に絶対パス(
$DDB_SKILL_DIR/…の形式)で実行してください。スキルのディレクトリにcdしないでください。ユーザーの作業ディレクトリはそのままにし、成果物(dynamodb_data_model.json、cost_report.mdなど)が想定どおりの場所に出力されるようにしてください。 - 3 つの方法のいずれでもディレクトリを解決できない場合は、推測せずにスキルのインストール先をユーザーに尋ねてください。誤った
${SKILL_DIR}を指定すると、その後「ファイルが見つかりません」という紛らわしいエラーが発生します。確認の質問を一つするほうが簡単です。
パイプラインの概要
このスキルでは、各ステージに1つのツールを使用します。デフォルトの進め方ではAWSに一切触れません。作業の大半はステージ1(会話形式で話し合い、改良できる設計)です。 ステージ2(コスト)は、依頼された場合、または設計を最終決定する際に実行します。毎回のやり取りで機械的に実行するものではありません。ステージ3〜6は、実際のAWSリソースを作成し、実費が発生する、明確なオプトインが必要な大規模な分岐です。ユーザーが明示的に同意した場合にのみ進めてください。各ステージの詳しい仕様は、最後の列に記載されたセクションを参照してください。
| # | ステージ | コマンド(export DDB_SKILL_DIR=… の実行後) | 読み込み | 書き込み | AWS? | セクション |
|---|---|---|---|---|---|---|
| 1 | 設計 | (スクリプトなし — アクセスパターンの一覧とスキーマを作成します) | — | (返信内の成果物) | いいえ | 作成する成果物 |
| 2 | コスト | python3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.md | dynamodb_data_model.json | cost_report.md | いいえ | コスト見積もり |
| 3 | デプロイ | python3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model dynamodb_data_model.json --config benchmark_config.json --manifest-out created_resources.json --yes-deploy | model + config | created_resources.json | はい | 実環境での検証 |
| 4 | ベンチマーク | python3 "$DDB_SKILL_DIR/scripts/benchmark_model.py" --model dynamodb_data_model.json --config benchmark_config.json --manifest created_resources.json --raw-out perf_raw.jsonl --summary-out perf_summary.json | model + config + manifest | perf_raw.jsonl、perf_summary.json | はい | 実環境での検証 |
| 5 | レポート | python3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model dynamodb_data_model.json --summary perf_summary.json --output performance_report.md | model + summary | performance_report.md、design_findings.json | いいえ | 実環境での検証 |
| 6 | リソース削除 | python3 "$DDB_SKILL_DIR/scripts/generate_teardown.py" --manifest created_resources.json --out teardown.sh → 確認 → bash teardown.sh --confirm | manifest | teardown.sh | はい(--confirm 実行時) | 実環境での検証 ステップ6 |
| — | 反復 | python3 "$DDB_SKILL_DIR/scripts/iterate_design.py" …(人が進行する1ラウンドとして3→4→5→コストを実行します) | model + config + loop-state + manifest | loop_state.json + 上記の成果物 | はい(許可が必要) | 反復設計ループ |
誰が何を読むか。 エージェントであるあなたは、簡潔な成果物を読みます。cost_report.md、design_findings.json、loop_state.jsonが該当します。ユーザーはperformance_report.mdを読みます。大容量のperf_raw.jsonlは決して読まないでください。これはステージ5への入力にのみ使われます。
同意の確認。 ステージ3以降には--yes-deployが必要です。ベンチマークは、--allow-spendが指定されていない場合、cost_guardrail_usdを超える支出を拒否します。リソース削除を実行する前には、ユーザーが確認したことを明示し、意図を示したうえで、bash teardown.sh --confirmを実行する必要があります。詳しくは実環境での検証を参照してください。
AWSアクセス(MCPの使用を推奨しますが、必須ではありません)。 ステージ3〜6ではAWSを使用し(テーブル、Lambda、IAMロールの作成後、ベンチマークとリソース削除を行います)、AWSとやり取りします。AWS API呼び出しを最適に行うには、AWS MCPサーバーの使用を推奨しますが、必須ではありません。このスキルのすべてのスクリプトはboto3を直接使用し、標準のAWS認証情報(プロファイル、SSO、または環境変数の認証情報)があれば通常のシェルから実行できます。そのため、MCPサーバーの有無にかかわらず同じように動作します。このスキルはMCP固有のツールを前提としていません。
これらの公理の使い方
- タスクが、アプリ全体のデータレイヤー(複数エンティティのスキーマ、複数テーブルのレイアウト、ストリーム/検索/通知とのエンドツーエンドの統合)の設計、レビュー、または批評である場合は、まずリファレンスアーキテクチャを読みます。
${SKILL_DIR}/references/reference-architecture.mdには、DynamoDB を基盤とする AWS 上のマルチテナント型カンバン・タスクボード「TaskBoard」の SaaS が、周辺要素(Cognito、CloudFront、HTTP API、Lambda、Streams、OpenSearch、AppSync Events、SQS/EventBridge、カスケード、冪等性ミドルウェア)も含めて、具体的に設計され、その根拠が示されています。公理が示すのは「何が」正しくなければならないかであり、リファレンスが示すのは、実際にこれらの要素を「どのように」組み合わせるかです。複数テーブルの設計でこれを読まないと、公理だけから再導出するのが難しい、リファレンスに記載されたパターンを見落とします。たとえば、冪等性ミドルウェア、ファントムアップサートの防止策、AppSync のチャネル認可、Notifications をテーブルではなく EventBridge として扱う判断、チャンク化したBatchWriteItemによるカスケード削除などです。単一テーブルに関する質問、クエリコストの計算、具体的なデバッグの質問など、対象範囲が小さい場合に限り、この手順を省略してください。 - 以下の公理を適用する前に、アクセスパターンの一覧(次のセクション)を作成します。すべてのモデリング公理は、この一覧があることを前提としています。「このパターンは頻繁に使われるか」や「このクエリは何を返すか」と問う公理は、一覧なしでは適用できません。
- 作成する成果物に記載された成果物を作成します。これらは設計の出力であり、途中のメモではありません。公理が成果物の形を定め、成果物がエージェントからの提出物になります。
- 公理とあわせて、パターンのセクションも適用します。パターンは公理ではありません。リファレンスによって具体化された、実装を支える重要な詳細であり、公理に明示的な指示がない場合でも、設計に必要となるものです。
- 2つの公理が相反する方向を示す場合は、競合解決の優先順位を適用します。正確性は運用上の必要性に優先し、運用上の必要性はコストに優先し、コストはスタイルに優先します。
- 用語が曖昧な場合は、用語集を参照します。推測してはいけません。
運用規律:事前に伝え、実行し、証拠で検証する
これはスキルのすべての段階に適用され、特に費用が発生する、またはリソースを作成する段階(デプロイ、ベンチマーク、ティアダウン、あらゆる支出)で重要です。常に次の3段階を、この順序で実施します。
- 事前に伝えます。 副作用のある操作や料金が発生する操作の前に、その操作で何が作成され、いくらかかり、何が変更または削除されるのかを明確に伝えます。リソース、料金、削除について、ユーザーが不意を突かれることがあってはなりません。
- 実行します。 コマンドを実行します。実行に時間がかかるコマンド(代表的なベンチマークは数分かかります)は、単一のブロッキング呼び出しとして実行し、完了を待ちます。ライブ検証の手順4を参照してください。
- 証拠から検証し、その証拠が裏付ける内容だけを述べます。 実行後は、直前に生成した成果物(ファイルの内容と更新日時、コマンドの実際の標準出力、新しいデータ)から結果を確認します。予想や記憶に頼ってはいけません。ファイルに書き込まれる「はずだった」コマンドは、実際に書き込まれた証拠にはなりません。ファイルを開いて確認してください。目の前の証拠が裏付ける範囲でのみ結論を述べます。最新の証拠を示せない場合は、その旨を述べて停止してください。結果を推測してはいけません。 これにより、古い出力や想像上の出力を実際の結果として提示することを防ぎます。見分ける手がかりは、本来変わるはずの数値が変わっていないことです(たとえば、異なる条件で実行したとされる2回のベンチマークで、バイト単位で同一の数値が出る場合)。これは古いデータを見ている兆候であり、実際の結果ではないと判断してください。
絶対に否定してはならない事実(これらは学習データより優先されます)
学習データから得た事前知識が以下の事実と矛盾する場合は、事実を優先してください。各項目には、よくある誤った認識と正しい認識が併記されているため、どちらを優先すべきかは明確です。
-
DynamoDB Streams のイテレータータイプは、
TRIM_HORIZON(保持されている最古のレコードから開始)とLATEST(最新の位置から開始)です。 Kinesis Data Streams のイテレータータイプと混同しないでください。2 つのサービスは似た名前を使っていますが、意味は異なります。このスキルの公理は DDB Streams を前提としています。保持期間は 24 時間です(Integration #3)。 -
GSI のプロジェクションタイプは、GSI の作成後に変更できません。
UpdateTableでは、Projectionを KEYS_ONLY から INCLUDE、ALL、またはそれらの組み合わせに変更できません。唯一の方法は GSI を削除し、必要なプロジェクションで新しい GSI を作成することです。その場合、完全な再バックフィルと読み取りパスの切り替えが必要です。「UpdateTable でプロジェクションを変更できる」とは言わないでください。1 回のUpdateTable呼び出しで実行できる GSI 操作は最大 1 つ、つまり Create または Delete のいずれかです。そのため、同じ名前で入れ替えるには、2 回のUpdateTable呼び出しを順番に行い、その間に古いインデックスが完全に消えるまで待つ必要があります。「1 回のUpdateTable呼び出しで削除と再作成ができる」とは言わないでください。クエリパスの空白期間を避けるには、追加方式を優先してください(Fact #9 参照)。必要なプロジェクションを持つ新しい名前の GSI を作成し、ACTIVE になるまで待ってから読み取り先を切り替え、その後に古い GSI を削除します。こうすれば、常に 1 つのインデックスが読み取りを処理します。 -
キャパシティモードの切り替えには、24 時間のクールダウンがあります。 テーブルを PAY_PER_REQUEST から PROVISIONED(またはその逆)に切り替えられるのは、テーブルごとに 24 時間に 1 回です。短時間に何度も切り替える方法を推奨したり、時間単位の料金計算で切り替えが即時に行われると仮定したりしないでください。
-
単一アイテムへの書き込みは、それ自体でアトミックであり、
TransactWriteItemsを使わずに条件式をサポートします。 単一アイテムに対するUpdateItem、PutItem、DeleteItemはそれぞれ単独でアトミックであり、ConditionExpressionを受け付けます。単一アイテムへの書き込みをTransactWriteItemsでラップすると、アトミック性の利点がないまま WCU コストが 2 倍になります(Mechanics #18)。単一アイテムへの条件付き書き込みにTransactWriteItemsを推奨しないでください。ConditionExpressionは書き込み側のパラメーターのみです。PutItem、UpdateItem、DeleteItemと、TransactWriteItemsの書き込みオペレーションでのみ使用できます。GetItem、BatchGetItem、Query、ScanはConditionExpressionを受け付けません。DynamoDB に条件付き読み取りはなく、ConditionalCheckFailedExceptionは書き込み専用のエラーです。GetItemが「条件を満たす場合にのみアイテムを返す」と説明したり、ConditionalCheckFailedExceptionをスローすると説明したりしないでください。そのような動作はありません。キーを指定した人には誰でも、読み取りによってそのアイテムが返されます。読み取り側のフィルターはFilterExpressionだけです(Query/Scan のみで使用でき、アイテムの読み取りと課金の後に適用されます。GetItemでは使用できません)。また、これは認可を行うものではなく、呼び出し元がすでに料金を支払って読み取った結果を絞り込むだけです。呼び出し元が別のテナントのアイテムを読めないようにするには、そのデータを呼び出し元がアドレス指定できないようにします。テーブルのパーティションキーに認可識別子を使い(Data modeling #14)、他のテナントのキーが呼び出し元のアクセス可能なパーティションに存在しないようにしてください。「条件付き GetItem」を追加する方法ではありません。 -
アイテムの最大サイズは 400 KB で、これを超えることはできません。 1 MB の制限は
Query/Scanの応答ページに対するものであり、アイテムの上限ではありません。アイテムの上限として 1 MB を挙げないでください。400 KB に近いアイテムは、書き込みコストも高くなります(WRU = 切り上げで 1 KB あたり 1、Mechanics #18)。そのため、上限に達する前から大きなアイテムは高コストです。 -
BatchGetItemとBatchWriteItemはアトミックではありません。 部分的な失敗は通常どおり発生し、未処理の部分はUnprocessedKeys(BatchGetItem)またはUnprocessedItems(BatchWriteItem)として返されます。クライアントは指数バックオフを使って、未処理の部分を再試行する必要があります。バッチオペレーションをアトミック、または全件成功か全件失敗かのいずれかであると説明しないでください。複数アイテムにまたがるアトミック性が必要な場合は、Mechanics #14 の制限の範囲内でTransactWriteItemsを使用してください。 -
Reserved Capacity が適用されるのは PROVISIONED キャパシティのみであり、オンデマンド(PAY_PER_REQUEST)には適用されません。 オンデマンドテーブルに Reserved Capacity を推奨しないでください — そのような製品は存在しません。オンデマンドの節約は、予約ではなく、使用量ベースの割引またはテーブルクラスの選択(Standard と Standard-IA)から得られます。
-
失敗した
ConditionExpressionでも書き込みキャパシティを消費します。ConditionalCheckFailedExceptionは、同じ形の成功した書き込みと同じ WCU を課金します。失敗した条件付き書き込みが無料であるとか、条件チェックが書き込みコストの評価の「前に」行われると主張しないでください。予想される失敗率を前提にコストモデルを設計してください(Mechanics #18 がこの理由でconditional_fail_rateを使用しています)。 -
GSI のキースキーマ(パーティションキー / ソートキー)は、GSI の作成後は変更できません。
UpdateTableは新しい GSI の追加や既存の GSI の削除はできますが、既存の GSI の KeySchema を変更することはできません。インデックスの再キー設定 — ハッシュサフィックスを追加してホットな GSI パーティションキーを書き込みシャーディングすることを含む — は、したがって追加型のマイグレーションであり、コードのみの変更ではありません。新しいキーで新しい GSI を作成する → populate させる → 読み取りを切り替える → 古い GSI を削除する、という流れになります。履歴のバックフィルが必要になるのは、新しいインデックスが、すでに書き込まれ今後再び触れられることのない項目をカバーしなければならない場合のみです。スパースな、または小規模な進行中のインデックス(たとえばアクティブな注文のみを保持するもの)は、進行中の書き込みだけから populate され、バックフィルは不要です。GSI のキー変更を「単なるコード変更」や「スキーママイグレーションなし」と表現しないでください。 -
DynamoDB にはネイティブのベクトル検索があります。セマンティック類似性に別のデータストアは必要ありません。 ベクトルインデックスは、GSI や LSI と並ぶ 3 番目のインデックスタイプです。
CreateTableのVectorIndexesパラメータ、またはUpdateTableのVectorIndexUpdatesパラメータで宣言し、埋め込みを項目上の数値のリストとして保存し、SearchVectorsAPI(近似最近傍探索)でクエリします。DynamoDB がベクトル検索や類似性検索を行えないと述べないでください。また、セマンティック検索の要件をデフォルトで OpenSearch、pgvector、S3 Vectors にルーティングしないでください — ベクトルは運用データと同じ項目上に存在し、レプリケーションパイプラインはありません(Integration #8 がワークロードタイプをルーティングします)。Query、Scan、PartiQL、DAX はベクトルインデックスに対して動作しません。QueryはValidationException: Query operation not supported on this index type.で拒否されます。ベクトルインデックスは GSI ではなく、GSI を通じて構成されるものでもありません —VectorIndexConfigは存在せず、セカンダリインデックスにベクトルブロックもありません。ベクトルインデックスにはオンデマンドキャパシティが必要です(Mechanics #19)。DynamoDB は埋め込みを再計算しませんし、インデックスは古くなった埋め込みを修正できません。 ベクトルは通常の項目データです。DynamoDB は書き込まれた数値をインデックスするだけで、それ以上は何もしません。ソーステキストを編集しても、保存された埋め込みは依然として古いテキストを表しているため、古い意味にマッチし続けます — いつまでも、エラーもなく、どこにも古さのシグナルもなく。これはインデックスの伝播遅延ではありませんし、ベクトルインデックスの書き込み伝播が「ウィンドウを閉じる」こともありません。どれだけ待ってもベクトルが再生成されることはありません。埋め込みの背後にあるコンテンツが可変である場合は常に、そのことを明示し、修正方法を示してください — コンテンツの変更を検出し、同じモデルで再埋め込みし、新しいベクトルを書き戻します(DynamoDB Streams から再埋め込みコンシューマーへ、というのが通常の形です。書き込みパスでの同期的な再生成も問題ありません)。インデックスが自らベクトルを更新すると示唆しないでください。
Dimensions、DistanceFunction、Projection、SearchSchemaはすべて変更できません — プロジェクションだけでなく、そのすべてがCreateTable時に固定されます。いずれか 1 つを変更するということは、代替インデックスを作成してマイグレーションすることを意味します。それらに対するインプレースのUpdateTableはありません(Mechanics #7)。 古い SDK は、機能が存在しないことの証拠にはなりません。 これらのオペレーションは botocore/boto3 ≥ 1.43.64 および AWS CLI v2 ≥ 2.36.16 に含まれています。それより前のバージョンでは、クライアントにまったく存在しません —aws dynamodb search-vectorsはFound invalid choiceで失敗し、hasattr(client, "search_vectors")はFalseになります。そのhasattrをテストする方が、バージョン文字列を比較するより優れています。機能を直接チェックでき、古くなることもないからです。存在しない場合は、ユーザーにアップグレードを伝えてください。機能が存在しないと結論づけてはいけません。API の全範囲、注意すべき点、トラブルシューティングの詳細は${SKILL_DIR}/references/vector-search.mdにあります。引用されているサービスの制限は設計上の想定範囲であり、議論の対象となる事実ではありません。 このスキルで引用されているテーブルごとのインデックス数、最大次元数、
TopKの範囲、インラインフィルター数は変更される可能性があり、この種のクォータは一般的に引き上げられます。それらの範囲内で設計してください。ただし、ユーザーの設計が制限を超えていると伝える前に、AWS Service Quotas または DynamoDB デベロッパーガイドで現在の値を確認してください。このスキルが追いついていない引き上げられたクォータが、誤って却下された設計になるべきではありません。
これらの 10 の事実は公理のすべてではありません — LLM の事前知識が最も誤りやすい部分集合です。ユーザーの質問がこれらのいずれかに関わる場合、正しい事実を明確に述べて先に進んでください。「私はこう思います」や「一般的には」といった曖昧な表現で濁してはいけません。
アクセスパターンのリスト
スキーマに触れる前に、アプリケーションが対応しなければならないすべてのパターンを列挙してください。各パターンについて次を記録します。
- 呼び出し元が何を求めているかの 1 行の説明。
- 想定 RPS(「不明」は、Mechanics #2 に従い、埋めるべき設計上のギャップとして扱います)。
- 呼び出しごとに返されるアイテム数と、おおよそのアイテムサイズ(KB)。
- 一貫性の要件(強整合性、結果整合性、またはトランザクション)。
- 認可スコープ — 呼び出しが許可される前に検証しなければならない識別子(Data modeling #14 に従います)。
リストは番号付きで優先順位付けされたテーブルです。このドキュメントの残りの部分は、それが存在することを前提としています。このリストの項目に遡ってたどれないモデリング上の決定は、根拠がありません。
エンティティごとの運用設定に関する入力
テーブル境界を提案する前に、このヒアリングを必ず行ってください。 まず複数テーブルの設計を完成させ、その後で「ここに前提を記載しました」と補うのは、近道ではなくワークフロー違反です。以下のエンティティごとの質問は、Data modeling #3 を通じてテーブル分割の判断を導きます。回答がユーザーの明言ではなくエージェントの推測に基づくと、シグナルが誤って発火し、設計が過度に分割されます(または、エージェントが単純化のために「差異なし」と推測した場合は分割不足になります)。先に質問し、その後で設計してください。
エンティティをテーブルにまとめる前に、エンティティごとに(または論理的な集約ごとに。親と密接に結び付いた子は、同じ回答セットを共有できます)運用設定の要件をユーザーから確認してください。これらのデフォルトを暗黙に仮定してはいけません。Data modeling #3 では運用設定の差異をテーブル分割のシグナルとして使うためです。差異が ユーザーの明言 ではなく エージェントの推測 による場合、シグナルが誤って発火し、設計が過度に分割されます。
各エンティティについて、次の点を質問してください。
- バックアップと復旧の粒度。 このエンティティには PITR が必要ですか? 必要な場合、保持期間はどのくらいですか(デフォルトは 35 日ですが、短縮できます)。このエンティティを他のエンティティとは独立して復元することがありますか。それとも常に他のエンティティと一緒に復元しますか。(独立した復元の要件がある場合、Data modeling #5 に従ってテーブルを分離する必要があります。)
- Streams の利用側。 このエンティティの変更イベントを必要とする下流システムはありますか。たとえば、検索インデックス作成、分析データのエクスポート、通知、監査、CDC などです。どの Streams ビュータイプを使いますか(Integration #3 に従い、デフォルトは
NEW_AND_OLD_IMAGESです)。ここでの「いいえ」は前向きな回答です。Streams の利用側がなければ、Streams を無効のままにでき、コストも手間も抑えられます。 - キャパシティモード。 このワークロードの特性から、プロビジョンド(Mechanics #19 にある、数か月にわたって持続する予測可能なトラフィック)にする根拠はありますか。それともオンデマンドをデフォルトのままにしますか。「不明」の場合はオンデマンドです。
- TTL。 アプリケーションが設定するアイテムごとの有効期限属性はありますか。ある場合、その属性は Unix エポック秒です(Patterns #3)。ない場合、TTL は無効のままとなり、アイテムは削除されるまで保持されます。
- 暗号化と IAM の適用範囲。 デフォルト以外の要件はありますか。たとえば、カスタマー管理の KMS キー、特定の IAM 境界、クロスアカウントのリソースポリシーなどです。デフォルトは AWS 所有の KMS と標準的な IAM であり、そこから異なる場合は明示的な回答が必要です。
これらを RPS と同等の設計入力として扱ってください。回答がない項目は推測して埋める値ではなく、解消すべきギャップです。ユーザーが「すべてのエンティティで同じ」と言った場合は、そのように記録し、エンティティ間に運用上の差異があるとは扱わないでください。そうすれば、Data modeling #1 による同一テーブルへの配置を妨げるものはありません。ユーザーが実際の差異を明言した場合は、Data modeling #3 がその差異に基づいて発動し、テーブルを分割します。
エンティティごとの属性の確認(アイテムサイズを決定)
アイテムサイズは RPS に次ぐコスト見積もりの大きな要因であり、見積もりがいつの間にか最も大きくずれる箇所でもあります。20 items × 1,536 B と定義した Query が、実際には 20 × 512 B を返すなら、モデル上のコストは実際の 3 倍になります。Mechanics #2 では、RPS が不明であることを設計上のギャップとしています。同じ規律をアイテムサイズにも適用してください。根拠のない estimated_item_size_bytes の推測は安全なデフォルトではなく、設計上のギャップです。
各エンティティについて、数値を確定する前に、ユーザーと一緒に属性リストを確認してください。まず質問するのが望ましい方法であり、推測した属性をもとに進めるのは代替手段です。どちらの場合でも、属性ごとの内訳をコスト見積もりの入力にする前に、ユーザーに提示して承認を得る必要があります。アイテムサイズが 2~10 倍ずれる原因は、暗黙に値を埋めることです。
- ドメインに基づいた属性を提案します。Waypoint の場合、それは
waypoint_id、courier_id、lat、lng、recorded_atです。Contract の場合、firm_id、contract_id、title、status、body、created_by、created、updatedです。 - 各属性について、次の出発点を使ってバイト数を見積もります:
- ID と短い文字列(ULID、UUID、スラッグ、enum 値): それぞれ 40 B。
cost-model-schema.mdにあるS=100というヒューリスティックは、無料枠のストレージパスに対しては保守的です。アイテム単位のサイズ見積もりには、現実的な値を使用してください。 - タイトル、表示名、短い説明: 100〜300 B。
- 長文コンテンツ(契約本文、メッセージ本文、シリアライズされた JSON 集約): ユーザーに明示的に尋ねてください。確認なしに 4 KB や 50 KB と推測しないでください。
- 数値属性: 約 8 B。
- ISO 文字列としてのタイムスタンプ: 約 25 B。エポック数として: 約 8 B。(メカニクス #11。)
- Boolean: 約 1 B。Map/List: おおよそのデフォルトとしてインスタンスあたり 約 200 B ですが、ユーザーが Map の中に大きな blob を保存する場合は尋ねてください。
- ID と短い文字列(ULID、UUID、スラッグ、enum 値): それぞれ 40 B。
- ユーザーは知っていてあなたは知らないであろう修正点を尋ねます: 「このアイテムはメカニクス #10 に従って非正規化された親データを持ちますか?」「長さが大きく変動する自由記述フィールドはありますか?」「完全なドキュメントを保存しますか、それとも要約を保存しますか?」回答に基づいて見積もりを更新します。
- 属性ごとの見積もりを合計して、エンティティの
estimated_item_size_bytesを導出します。サブセットを射影する Query(INCLUDE / KEYS_ONLY、またはアプリケーション側の射影)の場合、そのアクセスパターンのestimated_item_size_bytesにはより小さな数値を使用します。DynamoDB によって課金されるバイト数は、完全なアイテムではなく、射影されたビューから実際に読み取られたバイト数です。 - ユーザーが特定の属性について不確かな場合、その属性を成果物内で仮定としてラベル付けします(不明な RPS と同じ規律)。黙って数値を選ばないでください。
- 回答内で完全なリストを提示してください — 常に、これが対話的な会話であってもワンショットプロンプトであっても関係なく。 エンティティごとに、
attribute | type | bytes | source (user or guess)という列を持つコンパクトな Markdown テーブルを出力します。これは返信の形式に関する要件であり、対話のゲートではありません。フォローのターンが存在しないワンショット設定でも、テーブルは回答内に含めるべきです。そうすることで、ユーザーはあなたが正確に何を仮定したかを確認できます — この明示が、body フィールドに対する 3 倍の過大評価が下流のあらゆるコスト数値を汚染する前に、ユーザーがそれに気づく方法です。不確実な見積もりにはすべて明示的に「guess」とラベル付けしてください。推測をユーザー提供の数値として紛れ込ませないでください。明示的に修正を促してください: 「これらは注記された箇所では私の推測です — 誤っているものがあれば修正してください。」ユーザーが「妥当な値を適当に選んで進めて」と言った場合でも、テーブルを出力してください。
キャリブレーション: cost-model-schema.md にある参照 Contracts アプリの例では、Contract は約 2 KB(50 KB ではありません — 50 KB という値は最悪ケースの body サイズであり、典型的な値ではありません)、Clause は約 512 B です。宣言された estimated_item_size_bytes が、名前付き属性の合計の 2 倍を超えており、ユーザーがその差を説明していない場合、あなたは推測しています — 見直してください。
このウォークスルーを省略した実行は、個々のパターンで 2〜10 倍ずれる可能性があります。ライブ検証のステップ(以下)でそのずれは明らかになりますが、コスト見積もりを正しい桁に収めるためにライブ検証が必要であるべきではありません。
作成する成果物
成果物は以下の順序で作成してください。 スキーマとパターンごとの計画が主要な成果物です。コスト見積もり(項目 7)とライブ検証(項目 8)は、設計ができてから行うものであり、設計の代わりではありません。コスト分析から始め、スキーマを付録に埋もれさせる回答は、依存関係を逆にしています。ユーザーが求めているのは設計であり、コストは設計の性質です。アクセスパターンの一覧(項目 1)、スキーマ(項目 2)、パターンごとの計画(項目 3)は、コストの数値を示す前に、回答内で確認・検討できるようにしてください。項目 1~6 は AWS を使わない設計そのものであり、デフォルトの納品物です。項目 7(コスト)は依頼時または最終確定時に作成します(コスト見積もりを参照)。項目 8(ライブ検証)はオプトインの AWS フローです。これらの成果物を
dynamodb_data_model.jsonにだけ記載しても要件を満たしません。ユーザーが読むのは JSON ではなく、あなたの文章です。❌ 悪い回答例(実際に起こりうる失敗例):「## CFO 向け概要 — 月額 $1,019」から始まり、スキーマはディスク上のdynamodb_data_model.jsonにしかなく、回答内の設計内容は最後にある「作成した成果物」というファイル一覧だけの回答。✅ 良い例:アクセスパターンの一覧 + テーブルごとのスキーマ + パターンごとの計画 + エンティティごとのバイト数の表(エンティティごとの属性の詳細のステップ 6 に従う)を回答内に示し、その後にコスト概要を記載し、最後に「cost_report.mdを作成しました」と記します。
完全な設計では、次のものを提示します。
-
アクセスパターンの一覧(上記を参照)。
-
テーブルごとのスキーマ:主キー(データモデリング #7 に従った命名)、キー属性とプロジェクションタイプを含む GSI、運用設定(Streams、PITR、TTL、キャパシティモード、Global Tables のレプリケーション、暗号化、IAM スコープ。すべてデータモデリング #3 に従う)。
-
パターンごとの計画:一覧にあるアクセスパターンごとに、正確な API 呼び出し(Mechanics #16 に従い、
GetItem、Query、またはBatchGetItem)、対象となるテーブルまたは GSI、キー条件、該当する場合はフィルター式、Mechanics #18 の数式を使った予測コスト。 -
ファンアウトトポロジー:Streams を有効にした各テーブルについて、コンシューマー(Lambda、EventBridge Pipe、Kinesis shim)、ソース側のフィルター(Integration #4)、障害発生時の送信先と再試行の上限(Integration #5)。
-
逸脱とその正当化の一覧:適用しなかった公理やパターンと、その明確な理由。
-
冪等性と条件付き書き込みのガード:冪等性キーのミドルウェアを使うルート(Patterns #1)、架空のアップサートを防ぐ
attribute_existsガードを付けるUpdateItem(Patterns #2)、二重作成を防ぐattribute_not_existsガードを付けるPutItem。 -
月額コストの見積もり — すべての設計ターンで反射的に行うのではなく、依頼時または最終確定時に作成します。ユーザーがまだモデルを検討・調整している間は設計の議論を続け、ターンごとに計算ツールを実行しないでください。費用について尋ねられたとき、または設計が確定しつつあるとき(採用してレビューに回す意思を示したとき、あるいは数値を求めたとき)に、
${SKILL_DIR}/scripts/calculate_costs.pyを使って見積もりを作成してください(下記のコスト見積もりを参照)。見積もりを作成する際は必ず計算ツールを使い、暗算で計算してはいけません。完全な設計の対象にならない狭い範囲の質問(単一クエリのサイジング、デバッグのやりとり、特定の仕組みに関する質問)では、見積もりを省略してください。 -
ライブ検証(任意。提案する場合は最後のステップ):コスト見積もりの後、ユーザーが指定する AWS アカウントにこのスキーマをデプロイし、稼働中の DynamoDB で操作ごとの実容量、レイテンシ、GSI の増幅を測定するかどうか、ユーザーに尋ねます。希望する場合は、下記のライブ検証に従ってください。質問の範囲が狭い場合、コスト見積もりを省略した場合、またはユーザーにサンドボックスアカウントがない場合は省略してください。コスト見積もりとは異なり、この手順では実際のリソースが作成され、実際の料金が発生します。そのため、提案と同意の両方を明示的に得る必要があります。
優先順位に基づく競合の解決
公理が互いに矛盾する場合は、次の優先順位を適用します。
- 正確性 — 認可境界との整合性(データモデリング #14)、一貫性要件、トランザクションの原子性、冪等性。テナント間でデータが漏えいする設計や、強い一貫性が必要な場面で古いデータを提供する設計は、ほかの長所にかかわらず誤りです。
- 運用上の必須要件 — PITR、Streams、キャパシティ、レプリケーションの設定の相違(データモデリング #3)、復旧の粒度(データモデリング #5)、パーティションごとのスループット上限(仕組み #3)、トランザクションの上限(仕組み #14)。これらは物理的制約またはサービス上の制約であり、好みを優先することはできません。
- コストとパフォーマンス — アクセスパターンに基づく同居(データモデリング #1)、専用 GSI(データモデリング #6)、プロジェクションの選択(仕組み #7)、コストの計算式(仕組み #18)、キャパシティモード(仕組み #19)。
- スタイルと慣習 — 命名(データモデリング #7)、ほかに判断材料がない場合のシングルテーブルとマルチテーブルの考え方(データモデリング #11)。上位の公理と矛盾した場合、最も容易に覆せる項目です。
具体例を 2 つ示します。
- データモデリング #1(共有アクセスに基づく同居)とデータモデリング #14(認可境界に基づくパーティション分割):#14 を優先します。自然なアクセスキーと認可キーが異なる場合は、認可識別子をキーとし、代替アクセスには GSI を使用します。
- データモデリング #1(同居)とデータモデリング #3(運用設定の相違に基づく分割):#3 を優先します。同じ読み取りパターンを共有していても、異なる PITR 保持期間または Streams コンシューマーを必要とする 2 つのエンティティは、別々のテーブルに配置します。
用語集
- アクセスパターン — データレイヤーに対してアプリケーションが行うリクエストです。キー条件、返される項目、頻度、一貫性要件で定義されます。DynamoDB の設計における基本単位です。
- 集約 — まとめて読み書きされるエンティティの集合です。単一の項目、項目コレクション、異なるキーを持つ項目の集合のいずれも集約になり得ます。どれを選ぶかは、メカニクス #1 の主題です。
- 項目コレクション — 1 つのパーティションキー値を共有する項目の集合です。項目コレクションに対するクエリは、パーティションが固定されるため低コストですが、パーティションをまたぐ読み取りはそうではありません。
- 識別関係 — 子エンティティのキーが、親の識別子と子自身の識別子で構成されるデータモデルです。子は親の外部では独立して存在しません。
- オーバーロードキー — 値に型プレフィックス(例:
USER#42、ORDER#42)を含むパーティションキーまたはソートキーです。これにより、1 つの物理キーで複数の論理エンティティタイプを保持できます。 - スパース GSI — インデックス対象の属性がベーステーブル項目の一部にしか存在しない GSI で、インデックスにはその項目だけが反映されます。読み取り時に大半の項目が除外されるアクセスパターンに有効です。同じセマンティクスがベクトルインデックスにも適用され、サイレントなインデックス除外の仕組みとなっています。
- ベクトルインデックス — GSI および LSI に続く第 3 のインデックスタイプです。名前付き項目属性のベクトル埋め込みを保持し、
Query/ScanではなくSearchVectorsで読み取ります。CreateTableのVectorIndexesまたはUpdateTableのVectorIndexUpdatesで宣言します。オンデマンドキャパシティのみ対応し、テーブルあたり最大 5 個です。 - ANN(近似最近傍)— ベクトルインデックスが使用する検索戦略です。速度のために厳密性を犠牲にするため、同一データに対する結果やその順序は、実行やリージョンによって若干異なる場合があります。
- SearchSchema — ベクトルインデックスのオプションのスキーマです。最大 1 つの
HASH要素(ベクトルインデックスのパーティションキー。各検索のスコープとスケーリングに使用され、呼び出しごとに必ず指定する必要があります)と、最大 18 個のINLINE_FILTER要素(ストレージレイヤーで適用するオプションの等価フィルター)を指定します。作成時に固定されます。すべての SearchSchema 属性は、テーブルのAttributeDefinitionsにも含める必要があります。 - 距離関数 —
COSINE、EUCLIDEAN、DOT_PRODUCTのいずれかで、インデックス作成時に選択し、変更できません。Scoreとソート順の両方を決定します。COSINE/EUCLIDEANでは値が小さいほど類似度が高く、DOT_PRODUCTでは値が大きいほど類似度が高く、スコアが負になる場合もあります。 - サイレントなインデックス除外 — ベクトルインデックスの SearchSchema
HASH属性を含めずに書き込まれた項目は、エラーなしでベーステーブルに受け入れられますが、インデックスには複製されません。そのため、GetItemでは完全な状態で取得できますが、SearchVectorsの結果には表示されなくなります。これは想定された動作であり、不具合ではありません。${SKILL_DIR}/references/vector-search.mdを参照してください。 - VS / VWR — ベクトル検索およびベクトル書き込みのリクエスト単位です。ベクトルインデックスのキャパシティはバイト単位で計測され、
VectorSearchRequestBytesとVectorWriteRequestBytesに記録されます。ベーステーブルの RCU/WCU とは別に計測され、リクエストあたり 1 KB の最小値が適用されます。 - GSI 書き込み増幅 — GSI 投影属性を持つベーステーブル項目への書き込みごとに、該当する各 GSI に対して書き込みが 1 回発生し、それぞれ WCU で課金される性質です。
- LWW(最終書き込み優先)— 標準の Global Tables で使用される競合解決戦略です。最新のタイムスタンプを持つ書き込みが優先され、それ以前の書き込みは通知なく破棄されます。
- MRSC(マルチリージョン強整合性)— コンセンサスによりレプリカ間で強い一貫性を提供する、オプトイン方式の Global Tables モードです。書き込みレイテンシーとコストは高くなります。
- ホットパーティション — パーティションごとのスループット上限(メカニクス #3)を超えるトラフィックを受けるパーティションです。テーブル全体のキャパシティに余裕があっても、スロットリングが発生します。
- ポイズンピル — ストリームコンシューマーが正常に処理できないレコードです。破棄、再試行の上限到達、または障害時の送信先へのルーティングが行われるまで、そのシャードでの処理の進行を妨げます。
- RCU / WCU — 読み取りおよび書き込みキャパシティユニットです。プロビジョンドモードにおける、操作あたりのスループット単位の表記です。メカニクス #18 の計算式は RCU/WCU で記述されていますが、オンデマンドにも同じように適用されます。
- RRU / WRU — 読み取りおよび書き込みのリクエストユニットです。同じ操作あたりの単位をオンデマンド(PAY_PER_REQUEST)で表す名称で、プロビジョニング済みの容量秒あたりではなく、リクエストごとに課金されます。1 RRU は 1 RCU 分の処理に相当し、1 WRU は 1 WCU 分の処理に相当します。メカニクス #18 の消費量の計算は同じで、異なるのは課金の単位だけです。コストに関する参照先(
cost-model-schema.md)では、計算ツールがオンデマンド料金を算出するため、RRU/WRU を使用します。一方、公理とパフォーマンスレポートでは RCU/WCU を使用します。これらは同じ量を表すため、モデルの RRU/WRU の値とレポートの RCU/WCU の値を別のものとして扱わないでください。
データモデリング
-
ドメインではなく、共通のアクセスパターンに基づいてデータを同じ場所に配置します。アプリケーションのリクエストで2つのエンティティを一緒に取得または書き込みする場合に限り、同じテーブルに配置します。「ユーザーデータ」や「請求データ」といった共通のビジネスドメインだけでは、同じテーブルにする十分な根拠になりません。共通のクエリがないまま同じ場所に配置すると、結合が生じる一方でパフォーマンス上の利点は得られません。
-
テーブル数は設計の目標ではなく、設計の結果として扱います。テーブルを1つにすることも、エンティティごとに1つのテーブルを用意することも目標にしないでください。適切なテーブル数は、アクセスパターンによって決まります。分析の結果、テーブルが3つ必要なら、3つを用意します。
-
テーブルレベルの設定は、モデリングの入力であると同時に、インターフェースの宣言でもあります。ストリーム、ポイントインタイムリカバリ、TTL、キャパシティモード、アタッチされた Kinesis ストリーム、Global Tables のレプリカ、暗号化、IAM スコープは、すべてテーブルレベルで適用されます。つまり、テーブルがシステム全体にどう関わるかを宣言するものです。2つのエンティティで異なる運用設定(異なる PITR の保持期間、異なるストリームコンシューマー、異なるレプリケーションリージョン、異なるキャパシティモード)が必要な場合、その違いは後で調整すべき二次的な懸念ではなく、別々のテーブルに配置すべきことを示す主要なシグナルです。
-
DynamoDB Streams で同時に利用できるコンシューマーは2つで、ネイティブのエンティティ単位のフィルタリングはなく、保持期間は24時間です。これらの特性は、キーのスキーマと同様に、下流のアーキテクチャを厳しく制約します。テーブルレイアウトを確定する前に、ファンアウトのトポロジーを決定してください。
-
復旧の粒度を考慮して設計します。ポイントインタイムリカバリはテーブルレベルで動作し、部分復元には対応していません。2つのエンティティを一緒に復元することがないのであれば、同じテーブルに配置すべきではありません。インシデントのコストは、復元時に関連のないデータをどれだけ多く復元する必要があるかに応じて増加します。
-
過負荷になったキーよりも、追加の GSI を優先します。従来、インデックスの過負荷を促していた制約(テーブルあたりの GSI 数の上限の低さ、インデックスごとのプロビジョニング済みキャパシティ、オンデマンドモードがないこと)は、共有キャパシティとオンデマンド課金に対応し、テーブルあたり多数の GSI をサポートする最新の DynamoDB では一般に当てはまりません(テーブルあたりの正確な GSI 上限は、最新の AWS サービスクォータのドキュメントを確認してください)。特定の測定済みの理由がない限り、アクセスパターンごとに専用の GSI を選択してください。
ベクターインデックスは数が限られており、GSI と作成・削除操作を共有します。 テーブルあたりのベクターインデックスは最大 5 個で、GSI の上限よりはるかに少ないため、クエリの形ごとに追加するのではなく、計画的に割り当ててください。さらに重要なのは、テーブルごとに実行できるオンラインインデックス操作(作成または削除)は一度に 1 つだけで、その上限は GSI とベクターインデックスで共有されることです。1つの
UpdateTableで2つのCreateアクションを実行すると、LimitExceededException: Subscriber limit exceeded: Only 1 online index can be created or deleted simultaneously per tableにより失敗します。インデックス操作は一度に1つずつ順番に行ってください。また、この制約により、ベクターインデックスの移行(Mechanics #7)は時間がかかります。次のインデックスの作成を開始する前に、各新規インデックスのバックフィルが完了する必要があるためです。 -
キーには、そのキーが表すものを示す名前を付けます。
PK、SK、GSI1ではなく、customer_id、order_created_at、OrdersByCustomerを使用してください。自己記述的なキーを使うと、オンボーディングにかかる時間が短縮され、スキーマの参照資料がなくてもコードレビューができ、ツールからモデルを調査できるようになります。汎用の過負荷キーは、本当に多相的な階層に限って使用してください。 -
1つのリクエストで一緒に読み取るエンティティだけを事前に結合します。アイテムコレクションは、「親とすべての子を取得する」といった1回のクエリを満たすためのものです。2つのエンティティを一緒に読み取るアクセスパターンがない場合は、それらを一緒に保存しないでください。対応する読み取りのない事前結合は、利点のない結合です。
-
サービス境界を越えてテーブルを共有しないことを原則とします。サービス間でテーブルを共有すると、GSI の割り当て、キーの規約、スキーマの変更についてチーム間の調整が必要になります。その調整コストは、テーブルが大きくなるにつれて増大します。サービスの所有権は、テーブルの所有権にも反映させるべきです。
-
モデルが変化することを前提に設計します。アクセスパターンは進化します。新しいパターンに対応するために複数テーブルのリファクタリングが必要となるテーブルレイアウトは、負債になります。アクセスパターンの追加や変更が、データアクセスレイヤー全体ではなく、単一テーブル内の局所的な変更で済む設計を優先してください。
-
単一テーブル設計はデフォルトではなく、手法として適用します。単一テーブル設計が適しているのは、注文と明細項目、テナントとサブリソースのように、密接に結び付いた親子階層で、共同アクセスのパターンが予測可能な場合です。全体に適用するルールとしては適していません。このパターンの価値は局所的なものです。グローバルに適用すると、本番環境の事後分析で記録されている運用上の問題が生じます。
-
分析ワークロードは運用テーブルから切り離します。DynamoDB は OLTP ストアです。SQL 形式のクエリ、全テーブルの集計、アドホックな BI は、エクスポートまたはゼロ ETL 統合によってデータを取り込むダウンストリームシステムで処理します。分析に対応するために運用テーブルの形を変えてはいけません。
-
標準の Global Tables では最終書き込み優先を前提に設計するか、それを許容できないドメインでは Multi-Region Strong Consistency(MRSC)を選択します。標準の Global Tables は、結果整合性と最終書き込み優先の競合解決によるマルチリージョンのアクティブ・アクティブレプリケーションを提供します。CRDT ではありません。最終書き込み優先(LWW)を許容できないドメイン(在庫、残高、カウンター)には、アプリケーションレベルで競合を回避する方法(書き込み先リージョンの固定、シャードによるパーティション分割、条件付き更新)と、コンセンサスプロトコルを通じて指定されたレプリカセット全体で強整合性の読み取りと書き込みを提供するオプトインモードの MRSC という、2 つの選択肢があります。MRSC には 3 リージョンのトポロジー(2 つのアクティブレプリカと 1 つのウィットネス)が必要で、書き込みレイテンシーとコストが高く、テーブルごとに設定します。どちらのモードでもクロスアカウントレプリカがサポートされ、同じ整合性セマンティクスが適用されます。アカウントの境界は IAM と所有権の境界であり、整合性の境界ではありません。標準の Global Tables と MRSC のどちらを選ぶかはモデリング上の判断であり、そのテーブルでホストできるドメインを決定します。
特殊なケースとして、競合が発生している単一のホットアイテムがあります。たとえば、フラッシュセールで、数千人の購入者が 1 つの SKU に集中して在庫を減らすケースです。ここでの問題は整合性だけではなく、スループットです。1 つのアイテムへの書き込みは内部で直列化されるため、パーティションの生の上限に達する前に、レイテンシーが上昇し、パーティションあたりの WCU 上限(Mechanics #3)に達します。また、条件付き減算に失敗しても、すべて WCU を消費します(Fact #8)。単一アイテムのリージョン固定は役に立ちません。競合をなくさずに、1 つのリージョンに集中させるだけです。解決策はシャーデッドカウンターです。数量を N 個の兄弟アイテム(パーティションキーとして
sku#shard0..sku#shardNを使い、別々のパーティションに分散させます)に分割し、ランダムなシャードを> 0条件で減算します。失敗したら別のシャードを再試行し、すべてのシャードを分散読み取りして合計します。各シャードが独自の下限を適用するため、過剰販売ではなく、販売数を少なめに見積もるようにサイズを決めます。これは兄弟カウンターアイテムに適用する、パーティションキーの書き込みシャーディング(Mechanics #3)であり、競合の解決策です。矛盾ではありません。リージョン固定とアイテム単位の競合回避は、高カーディナリティでアイテムあたりの競合が少ないデータ(各ユーザーが自身のパーティションキーとなり、競合が約 1 となるアカウントごとの残高など)には引き続き適切ですが、競合が集中する単一の行には適しません。 -
パーティションキーを認可またはテナントの境界に合わせます。読み取りや書き込みを許可する前に検証する識別子(
tenant_id、account_id、board_idなど、呼び出し元のアクセス範囲を定めるもの)は、そのスコープ内のデータを保持するすべてのテーブルでパーティションキーにする必要があります。認可と検索が同じパーティションを共有していれば、呼び出し元がアクセス権を証明できないパーティションへのクエリは単に何も返しません。異なるパーティションを使っていると、呼び出し元が別のユーザーの子リソース ID を渡して、認可境界を越えたデータにアクセスできる可能性があります(IDOR)。正当にテナント間で共有されるデータは、メインテーブルに妥協を持ち込むのではなく、独自のアクセスルールを持つ別のテーブルに置きます。 あらゆる設計レビューにおいて — そしてまず第一に、ユーザーがセキュリティ上の問題は何かを尋ねたときには — パーティションキーが認可・テナンシーの境界と整合しているかを評価することが、最初に行い、かつ最初に述べるべきチェックです。これはホットパーティション、プロジェクション、コストに関する指摘よりも優先されます(競合解決の順序付けにより、正確性はコストより上位です)。典型的な穴は、パーティションキーがサーバー導出の認可識別子(user_id、tenant_id、account_id)ではなく、クライアントが指定した識別子(URL からのreview_id/order_id)に設定されているケースです — これが IDOR です。設計がすでに整合しているなら、その旨を述べて先に進んでください。もし逸脱しているなら、それを真っ先に指摘し、「ConditionExpressionを追加する」に格下げしてはいけません — 呼び出しごとのチェックは規律であって、構造的な修正ではありません。ConditionExpressionが修正にならない理由を説明するときは、メカニズムを正確に述べてください。書き込み側の条件はオプトインであり(1 つの呼び出しパスの忘れ = IDOR)、読み取りをまったくカバーしません — しかし読み取りをカバーしない理由は、GetItem/QueryがそもそもConditionExpressionを受け取らないからであり(事実 #4)、「条件付き GetItem はアイテムを返す/とにかくConditionalCheckFailedExceptionをスローする」からではありません。反論のために読み取り側の条件メカニズムをでっち上げないでください。構造的な修正はキースキーマであり(外部キーがアドレス指定不可能になります)、読み取りは呼び出し元が所有していないパーティションを指定できないため安全であり続けます — そのように述べてください。
統合
-
Streams のコンシューマーが冪等であるか、イベント識別子によって重複排除されていることを確認してください。DynamoDB Streams は少なくとも 1 回の配信を行うため、Lambda のタイムアウト、バッチのリトライ、またはインシデント中のリプレイの後で、ハンドラーは同じレコードを 2 回目にすることになり、1 回呼び出されても何度も呼び出されても同じ結果を生成する必要があります。
2 つの戦略が実行可能です:
- 冪等なハンドラー:操作を、繰り返しても no-op になるように構造化します。条件付き書き込み、セットベースのミューテーション(セットへの
ADD)、ダウンストリーム API の冪等性トークンはすべて該当します。 - 明示的な重複排除:ストリームレコードの
eventIDを重複排除ストア(通常は TTL 付きの DynamoDB テーブル)に記録し、すでに存在するレコードをスキップします。
例(
eventIDによる重複排除、TTL は再配信ウィンドウに制限):def handler(event, context): for record in event["Records"]: try: dedupe_table.put_item( Item={"event_id": record["eventID"], "ttl": int(time.time()) + 86400}, ConditionExpression="attribute_not_exists(event_id)", ) except ClientError as e: if e.response["Error"]["Code"] == "ConditionalCheckFailedException": continue # already processed raise process(record)重複排除は
order_idのようなアプリケーション識別子ではなくeventIDで行ってください — 同じ集約は正当に多数のイベントを生成するため、ビジネス識別子をキーにすると有効な変更レコードを破棄してしまいます。 - 冪等なハンドラー:操作を、繰り返しても no-op になるように構造化します。条件付き書き込み、セットベースのミューテーション(セットへの
-
信頼性の高いイベント発行には、トランザクショナルアウトボックスパターンを使用してください。状態変更とアウトボックスレコードを単一の
TransactWriteItems呼び出しで書き込み、ダウンストリームのコンシューマーに Streams 経由でアウトボックスを読み取らせ、イベントを発行させます。これにより、二相コミットに頼ることなく、「更新されたが発行されなかった」という障害モードを排除できます。 -
ストリームビューはデフォルトで
NEW_AND_OLD_IMAGESにしてください。OpenSearch とのゼロ ETL 統合、ほとんどの変更データキャプチャコンシューマー、および差分に依存するあらゆるロジックは、両方のイメージを必要とします。 -
イベントはハンドラー内ではなく、ソースでフィルタリングしてください。ソース側のフィルタリングには2 つのメカニズムがあります — 診断または修正案を提示するときは両方に言及してください:
- Lambda イベントソースマッピングの
FilterCriteria— Lambda 呼び出しの前にストリームレコードをフィルタリングします。以前は 1 日 1,000 万イベントで実行されていたがそのうち 50 万件のみを対象とするハンドラーは、50 万回実行されるハンドラーになり、1,000 万回ではなく 50 万回の呼び出しに対して課金されます。フィルターの形状(DynamoDB ストリームレコードに対する JSON パターンマッチング):{"dynamodb": {"NewImage": {"status": {"S": ["shipped", "delivered"]}}}}。 - EventBridge Pipes のフィルター — 同じソース側の破棄を、Pipe がコンシューマーである場合に適用します。ファンアウトが複数のダウンストリームターゲット(イベント → Lambda + SQS + EventBridge バス)にまたがる場合は Pipes を使用してください。Streams の同時コンシューマー 2 件の制限(データモデリング #4)により、Pipes が 2 コンシューマー超のトポロジーの一般的な経路となります。 どちらのメカニズムも、呼び出しコストとならびにハンドラーがフィルタリングで行うはずだった処理を排除します。ハンドラー内のインラインフィルターロジックは、破棄されるすべてのイベントに対して完全な呼び出しコストを支払います。トラフィックの 95% を落とすフィルターは、呼び出しの 95% が純粋な無駄であることを意味します。ソース側のフィルタリングに関する質問であれば、その回答は両方のメカニズムを挙げ、コンシューマーのトポロジーに適合する方へ推奨を振り分けます。
- Lambda イベントソースマッピングの
-
Event Source Mapping の設定を信頼性のサーフェスとして扱ってください。
BatchSize、MaximumBatchingWindowInSeconds、ParallelizationFactor、MaximumRetryAttempts、MaximumRecordAgeInSeconds、BisectBatchOnFunctionError、および失敗時の送信先は、あわせて配信セマンティクスを定義します。いくつかのデフォルト値は積極的に危険です。MaximumRetryAttemptsとMaximumRecordAgeInSecondsはどちらもデフォルトで-1であり、これは処理不能な 1 件のレコードがそのシャードを 24 時間のストリーム保持期間いっぱいブロックし、その後静かに消えることを意味します。リトライを制限し、レコードの経過時間を制限し、失敗時の送信先を設定して、ポイズンレコードがシャードを停滞させたり消えたりするのではなく隔離されるようにしてください。これらのいずれかをデフォルトのままにすることは、中立な選択ではなく意図的な選択です。 -
ストリームの有効化を永続的なインターフェースのコミットメントとして扱ってください。ストリームを無効化して再度有効化すると、新しいストリーム ARN が生成されます。古い ARN にバインドされている Lambda Event Source Mapping や EventBridge Pipe は孤立します。ストリームのライフサイクルはテーブルの公開コントラクトの一部であり、それに応じて管理する必要があります。
-
TTL 駆動の削除とユーザー駆動の削除を区別してください。DynamoDB は TTL 削除を Streams 経由で配信し、その際
userIdentity.principalIdはdynamodb.amazonaws.comに設定されます。コンシューマーはこの属性で分岐し、TTL イベントを通常のユーザー削除として扱うのではなく、別個に(通常はアーカイブや監査パイプラインへ)ルーティングすべきです。 -
分析ワークロードまたは検索ワークロードを適切なターゲットに一致させてください。どちらかを提供するために運用テーブルを作り変えないでください。DynamoDB は分析ワークロード向けの OLTP ストアであり、読み取り最適化システムへのサポートされた経路はマネージド統合であり、
Scanでも独自の GSI でもありません。セマンティック類似性は例外であり、DynamoDB に留まります(Fact #10)。ワークロード種別ごとにルーティングしてください:- セマンティック類似性 — 「類似を検索」「これに似たもの」、RAG 検索、レコメンデーション、AI エージェントのメモリ、埋め込みベースの異常検知 → DynamoDB テーブル自体のベクトルインデックスで、
SearchVectorsを使ってクエリします。2 つ目のデータストアも、レプリケーションパイプラインも、PITR の前提条件もありません。これは埋め込みに対する類似性として表現されるあらゆるもののデフォルトです。${SKILL_DIR}/references/vector-search.mdを参照してください。 - 全文検索、キーワード、タイポ許容またはあいまい一致、ファセット、集計、地理空間/半径検索 → Amazon OpenSearch Service へのゼロ ETL。PITR スナップショットからの初回ロード、DynamoDB Streams による継続的な変更キャプチャ、ほぼリアルタイムの鮮度(秒単位)。ソースで PITR が有効であり、Streams が
NEW_AND_OLD_IMAGESで有効である必要があります。 - SQL 分析、BI、マテリアライズドビュー、データ共有 → Amazon Redshift へのゼロ ETL。PITR エクスポートからの初回ロード、15〜30 分ごとの増分エクスポートによる継続的な変更キャプチャ。ソースで PITR が有効であり、KMS が AWS 所有またはカスタマー管理キーで設定されている必要があります(AWS マネージド KMS はサポートされていません)。
- オープンフォーマットのデータレイク、Apache Iceberg、マルチエンジン分析(Athena、EMR、Spark、Redshift)、または ML フィーチャーストア → Amazon SageMaker Lakehouse へのゼロ ETL。Glue がオーケストレーションする初回エクスポートに加え、Iceberg テーブルを S3 または S3 Tables に書き込む増分エクスポートで、通常 15〜30 分の鮮度です。PITR が有効であり、Glue にエクスポートアクションを許可するリソースベースポリシーが必要です。
- カスタムのダウンストリーム処理を伴い、鮮度要件のないポイントインタイムのバッチダンプ → DynamoDB から S3 へのエクスポート(ゼロ ETL というブランドではありませんが、ゼロ ETL ターゲットが適合しない場合の正しいフォールバックです)。RCU を消費しません。
上記のゼロ ETL の各経路では、ソーステーブルでの PITR が必須の前提条件です。ただし、統合ではないベクトルインデックスの経路には当てはまりません。
begins_withクエリや独自の GSI で検索を代用しないでください。分析のために運用テーブルをスキャンしないでください。これらの統合があるのは、こうしたワークロードがカラムストア、検索エンジン、または Iceberg テーブルと組み合わせて使われるものであり、NoSQL のキーバリューストアとは組み合わせられないためです。
ひとまとめの要望は分解し、すべてを一か所に送らないでください。 「検索」のリクエストには、複数の種類が含まれていることがよくあります。意味的類似性の検索はベクトルインデックスに、字句検索、あいまい検索、地理空間検索は OpenSearch に、SQL は Redshift に振り分けます。意味的検索と全文検索の両方が必要なワークロードでは、ベクトルインデックスと OpenSearch の両方を使用します。どちらの方向も誤りです。意味的類似性検索を反射的に OpenSearch に振り分けることも、誤字に寛容なキーワード検索や地理的範囲検索をベクトルインデックスに振り分けることも誤りです。埋め込みベクトルに対する ANN では、部分文字列、プレフィックス、編集距離によるマッチングはできません。
1 つの運用テーブルを Redshift(SQL/BI)と SageMaker Lakehouse(Iceberg)の両方の経路に接続する場合は、DynamoDB から Lakehouse への Iceberg エクスポートを 1 つだけ行い、Redshift からも同じ Iceberg テーブルを読み取る(Redshift Spectrum/ネイティブの Iceberg サポート)方法を優先してください。ゼロ ETL 統合を 2 つ並行して実行する必要がなくなり、SQL/JOIN 機能を損なうことなく、2 回目の増分エクスポートと GB あたりの重複料金を回避できます。利用側で鮮度や分離の要件が実際に異なる場合に限り、別々の統合を使用してください。
- セマンティック類似性 — 「類似を検索」「これに似たもの」、RAG 検索、レコメンデーション、AI エージェントのメモリ、埋め込みベースの異常検知 → DynamoDB テーブル自体のベクトルインデックスで、
仕組み
-
エンティティが一緒に読み取られる頻度と、個別に書き込まれる頻度を比較し、集約の結合度を選択します。子要素を 1 つのアイテムに埋め込む、共通のパーティションキーの下でアイテムコレクションとしてまとめる、または別々の集約として保存するという 3 つの選択肢は、親と子の結び付きの強さを示す連続的な尺度に位置します。この選択に一律の基準はありません。読み取りの相関が高ければ同じ場所への配置が有利ですが、アイテムサイズが大きく、書き込みの多いワークロードでは逆の選択が有利です。更新のたびにアイテム全体を書き換える必要があるためです。アクセス頻度と同じくらい、アイテムサイズと子要素の数が上限付きかどうかも重要です。目安として、注文の明細がほとんどの場合に注文と一緒に取得され、明細数に上限があるなら、注文を親として埋め込むか、アイテムコレクションにすることを検討してください。個々の明細が単独で頻繁に更新されるなら、書き込みのたびに親全体を書き換えずに済むよう、別々に保持してください。適切な階層を選べないことが、「単一テーブル設計の失敗」とされる事例の大半に潜む原因です。
-
アクセスパターンごとに RPS を記録します。リクエストレートがわからなければ、パーティションのサイズを決めたり、オンデマンド容量とプロビジョニング済み容量のどちらを選ぶか判断したり、GSI を正当化したりできません。ビジネス上の背景に基づく概算で十分ですが、レートが未記載なのは問題です。「不明」は解消すべき設計上の不足として扱ってください。
-
パーティションごとのスループット上限を守ります。1 つのパーティションでサポートできるのは、1 秒あたり最大 1,000 書き込みキャパシティーユニットと 3,000 読み取りキャパシティーユニットです。この上限を超えるワークロードでは、パーティションキーをシャーディングする必要があります。通常、書き込みの多いトラフィックにはハッシュサフィックスを、連続するキーには時間バケットを使用します。
-
ベーステーブルのキースキーマでは、
HASHをちょうど 1 つ、RANGEを最大 1 つ指定できます。ベーステーブルで複合キーが必要な場合は、tenant_id#user_idのような安定した区切り文字を使って連結した文字列としてエンコードします。GSI は ネイティブの複数属性キー をサポートします。パーティションキーには最大 4 つの属性、ソートキーには最大 4 つの属性を指定でき、DynamoDB が PK 属性をまとめてハッシュ化して分散させます。合成された連結キーよりもネイティブの複数属性 GSI キーを優先してください。項目はドメインモデルの自然な属性で書き込まれ、クライアントコードは連結やパースを行わず、既存のテーブルに新しい複数属性 GSI を追加する際にも合成属性のバックフィルが不要です。合成された連結キーは、コンポーネントの数が 4 を超える場合、または古いテーブルがすでにそのパターンを採用している場合にのみ使用してください。 -
複数属性 GSI キーには厳格なクエリルールがあります。パーティションキー側では、すべての PK 属性を等価条件で制約する必要があります。PK が (
tenant_id,region) の GSI をtenant_idだけでクエリすることはできず、PK 属性には不等演算子を使用できません。ソートキー側では、属性は定義された順序で左から右へ制約する必要があり、途中の属性をスキップすることはできません。等価条件は不等条件より前に置く必要があり、不等条件は 1 つだけ許可され、キー条件式の最後の条件でなければなりません。BETWEEN、>、<、>=、<=、begins_withはすべて不等条件として扱われます。これらのルールに違反することは、GSI が想定したアクセスパターンを満たせない最も一般的な原因です。 -
項目の 50% 以上がフィルタリングされる場合は、スパース GSI を検討してください。属性の存在をインデックス化する方が、すべての項目をインデックス化して読み取り時にフィルタリングするよりも実質的に安価です。スパースインデックスは、インデックス化された属性を持つ項目だけが射影されるため、書き込み増幅も削減します。
-
アクセスパターンが読み取る属性だけを射影してください。
ALL射影は、ベーステーブルの更新ごとにストレージコストと書き込み増幅をほぼ 2 倍にします。読み取りレイテンシが重要な場合、またはアプリケーションが書き込みに比べて読み取り中心の場合はALLを使用してください。項目サイズが大きい場合やテーブルが書き込み中心の場合はINCLUDEを使用し、レイテンシが許すならKEYS_ONLYと逆方向のテーブルルックアップを使用してください。ベクトルインデックスでは、射影は厳格な読み取り境界であり、変更できません。
SearchVectorsはインデックスに射影されていない属性を返すことができず、それを要求しても サイレントに無視されます — エラーは発生せず、結果から単にその属性が欠落します。射影は作成後に変更できないため、射影が不足している場合は、代替インデックスを作成して移行するか(追加的な移行。Fact #9 を参照)、検索後にGetItem/BatchGetItemでベーステーブルから不足している属性を取得するかになります。そして変更できないのは射影だけではありません。Dimensions、DistanceFunction、SearchSchemaも同様に変更できません。 したがって「このインデックスを COSINE に切り替える」や「検索スキーマにパーティションキーを追加する」も新しいインデックスへの移行であり、UpdateTableになることは決してありません。デバッグのやり取りで誤った距離関数に行き着いた場合は、射影の場合だけを説明するのではなく、そのことを明示的に伝えてください。また、ベクトルデータは大きいため、ベクトル属性自体はALLの場合でもデフォルトで結果から除外される点に注意してください。実際に返す必要がある場合にのみ、ProjectionExpressionで明示的に要求してください。 -
可変属性を GSI キーとして使用することは避けてください。DynamoDB はキー属性の変更を、インデックス内での削除と挿入として実装するため、その更新の書き込みコストが 2 倍になります。頻繁に変更されるフィールドは、GSI のパーティションキーやソートキーとして使用しないでください。
-
子へのアクセスが主に親のコンテキストで行われる場合は、識別関係を使用します。子エンティティが親から独立して存在できず、子の取得や更新に通常親の識別子が伴う場合は、親の識別子をパーティションキー、子の識別子をソートキーとしてベーステーブルに関係をモデル化します。これにより「親ごとに子を一覧表示する」パターンのための GSI が不要になり、関連するインデックス増幅を排除することで通常は書き込みコストが半減します。例外は、親なしで子を自身の id によって個別に更新するホットパスです。たとえば、明細項目にスコープされた Webhook などです。
UpdateItemは完全なプライマリキーを必要とするため、呼び出し元が通常order_item_idのみを持って到達する場合、識別関係スキーマではすべての書き込みでorder_idを解決するために GSI ルックアップが強制され、削減効果が打ち消されます。その場合は逆にします。子を自身の id でキー付けし、親を GSI に持たせます。正しい形は、スタイルの好みではなく、ホットなアクセスパターンに従います。 -
非正規化は、ほとんど変化しない属性に限定します。ショートサーキットコピー — 注文上のユーザーの表示名、明細項目上の商品 SKU — は安全であり、追加の読み取りを排除します。可変属性をコピーすると、すべての更新が複数項目へのファンアウトになり、読み取りの利点をすぐに上回ってしまいます。
-
必要なソート動作に基づいて時間エンコーディングを選択します。文字列は辞書順にソートされるため、
"10"は"2"より前に来ます。自然な文字列ソートが望ましく、人間が読めることが重要な場合は ISO 8601 タイムスタンプを使用します。コンパクトさ、算術、または精度が重要な場合は Unix エポック数値を使用します。1 つのテーブル内でエンコーディングを混在させないでください。キー属性(テーブルまたは GSI のパーティション/ソートキー)が数値 — エポックタイムスタンプ、数値 id — またはバイナリである場合、ライブデプロイが正しく作成できるよう、その型をデータモデルで宣言する必要があります。 DynamoDB はキー属性のみに型を適用し、書き込み時にその型を強制します。型を宣言せずに放置したキーは文字列 (S) としてデプロイされ、最初の実際の整数/バイナリ書き込みはValidationExceptionで失敗します — ベンチマークではなく本番で発生します(シードは通過してもライブ書き込みは失敗する可能性があります)。型はentities[].attributes[]で{"name": "order_date", "type": "N"}として宣言するか、テーブルレベルでattribute_definitionsに{"attribute_name": "order_date", "attribute_type": "N"}として宣言します(どちらの書き方もデプロイとベンチマークによって読み取られます。競合時はattribute_definitionsのエントリが優先されます)。詳細は${SKILL_DIR}/references/cost-model-schema.mdを参照してください。 -
時間に敏感な有効期限のために TTL に依存しないでください。TTL 削除は結果整合です — バックグラウンドのスイーパーはベストエフォートのスケジュールで実行され、期限切れの項目はスイーパーが削除するまで、実際には TTL タイムスタンプを最大約 48 時間過ぎても表示されたままになることがあります。TTL はストレージの再利用とクリーンアップには適していますが、セッション、トークン、エンタイトルメント、リアルタイムイベントトリガーなどセキュリティに敏感な有効期限には適していません。
これには 2 つの帰結があります。
- 読み取り時にフィルタリングします。 スイーパーが実行されるまで項目は TTL を超えて存続し得るため、有効期限を扱うすべての読み取りパスは TTL 属性自体をチェックする必要があります —
#ttl > :nowのFilterExpression、またはアプリケーション側のチェックです。DynamoDB から返された項目が論理的に最新であると仮定しないでください。 - タイミングが重要な場合は EventBridge Scheduler と組み合わせます。 有効期限に正確なアクション — セッションの失効、リマインダーの発火、ロックの解放 — が必要なワークロードでは、書き込み時に正確なタイムスタンプに対して 1 回限りの EventBridge Scheduler 呼び出しを作成し、その呼び出しに処理を行わせます。TTL は結果整合のクリーンアップを処理し、スケジューラーは時間に敏感なトリガーを処理します。その後、Streams は実際の TTL 削除をアーカイブや監査に伝播できます(統合 #7 を参照)が、Streams はタイミングの仕組みではありません。
TTL 属性はミリ秒ではなく秒単位の Unix エポック値でなければなりません。
- 読み取り時にフィルタリングします。 スイーパーが実行されるまで項目は TTL を超えて存続し得るため、有効期限を扱うすべての読み取りパスは TTL 属性自体をチェックする必要があります —
-
一意性はアプリケーションロジックではなく、トランザクションで保証します。兄弟となる検索アイテム(たとえば、
UNIQUE#email#[email protected])を作成し、エンティティと合わせて単一のTransactWriteItems呼び出しで書き込みます。アプリケーションコードで確認してから書き込む手順には競合が発生する時間帯がありますが、トランザクションならその心配はありません。 -
トランザクションの上限内で処理します。
TransactWriteItemsには**3 つの厳格な上限があり、トランザクションの上限に関する質問では、必ず 3 つすべてを示してください。1 つや 2 つだけでは不十分です。**上限はそれぞれ独立しており、いずれか 1 つでも超えるとトランザクションは拒否されます。- トランザクションあたり 100 アイテム — 配列内のアイテムの合計数です。
- 合計ペイロード 4 MB — トランザクション内のすべてのアイテムの合計サイズです。この上限を省略したり、100 アイテムの箇条書きにまとめたりしないでください。たとえば、各アイテムが 500 KB のトランザクションは、100 アイテムの上限は満たしますが、4 MB の上限には違反します。ペイロード上限は正確に 4 MB です。16 MB(
BatchGetItemの上限)、1 MB(Query/Scanのページ上限)、400 KB(アイテムの最大サイズ)ではありません。 - 単一リージョン — トランザクションは 1 つのリージョン内でのみ実行され、Global Tables のレプリカをまたいで実行されることはありません。異なるリージョンの呼び出し元が同じトランザクションに参加する必要がある場合、そのワークロードに標準の Global Tables は適切な整合性モデルではありません(MRSC を使用するか、各トランザクションがリージョン内で完結するように再構成してください)。
これらの上限を頻繁に超える操作は、回避策で乗り越えるべきデータベースの制限ではなく、集約境界が粗すぎることを示しています。インポートや一括操作が 100 アイテムの上限を超える場合は、複数のトランザクションに分割し、部分的な失敗からの復旧を明示的に処理してください(ジョブ ID、チェックポイント付きの進捗、各チャンクの冪等な再試行など)。
**100 アイテムの上限ではレコード数ではなく、アイテム数を数えてください。**1 つの論理レコードに複数の DynamoDB アイテムが必要な場合(Mechanics #13 に従った連絡先と一意性センチネル、さらにカウンターのインクリメントなど)、TransactWriteItems 配列内のすべてのアイテムが数に含まれます。連絡先あたり 3 アイテムなら、1 トランザクションに収まる連絡先は約 30~33 件であり、100 件ではありません。4 MB のペイロード上限も独立した第 2 の上限です。レコードがそれぞれ約 30 KB の場合、100 アイテムの上限より先に 4 MB の上限に達します。両方の上限を考慮してチャンクサイズを計画し、小さい方の上限を基準にしてください。
**100 アイテムの上限を超える一括インポートは、同期 API 呼び出しではなくバックグラウンドジョブにしてください。**数万件のレコードをページングしながら TransactWriteItems を繰り返すループは、合理的なリクエストタイムアウト(API Gateway の 30 秒、Lambda の上限 15 分、ブラウザーのリクエストタイムアウト)を超えます。この操作は永続化されるジョブとしてモデル化してください。リクエストを受け付け、ジョブレコードをキューに入れ、チェックポイント付きの進捗を保存して再起動後も途中から再開できるように、ワーカー(Step Functions、SQS + Lambda、バッチジョブ)からチャンクごとに実行し、完了を非同期で通知します。「チャンク化されたトランザクション」が単一の同期操作として構成されるかのように扱わないでください。
- 読み取りは結果整合性をデフォルトにします。強い整合性のある読み取りは、2 倍の RCU を消費します。読み取り直後に書き込み結果を参照する必要がある境界でのみ使用し、全体のデフォルトとして採用しないでください。
呼び出し元がアイテムを更新し、直後に新しい値を必要とする場合は、後続の読み取りを行う代わりに、UpdateItem で ReturnValues=ALL_NEW(または用途に応じて ALL_OLD/UPDATED_NEW/UPDATED_OLD)を使用してください。書き込みのレスポンスには、書き込み後のアイテムが追加の RCU 消費なしで含まれ、整合性の懸念もありません。書き込みの戻り値は、構造上、信頼できるデータです。UpdateItem の後に強い整合性のある GetItem を行うと、すでに無料で得られる情報のために 1 RCU と追加のラウンドトリップを消費します。これが DynamoDB における、最もコストの低い「書き込み直後に読み取る」方法です。
-
すべてのアクセスパターンを
GetItem、Query、またはBatchGetItemで解決してください。Scanは管理操作には適していますが、アプリケーションの読み取りパスには適していません。スキャンを必要とする本番環境のアクセスパターンは、許容できるクエリの選択肢ではなく、データモデル上の不足を示しています。 -
すべてのクエリに制約を設けます。ソートキーの範囲またはフィルター条件を指定し、予想されるページサイズを計算して、
LastEvaluatedKeyでページネーションを行います。制約はキー構造そのものから得られる場合もあります。子の数が上限内に収まる親パーティション(1 つの注文に属する明細、1 つのチームのメンバーなど)は、すでに制約されており、「この親の下にあるすべてを返す」は有効なアクセスパターンです。一方、データセットの増加に伴って結果サイズが際限なく大きくなるクエリ、たとえば期間の制限も、ページの目標も、終了条件もない「ある顧客の何年分ものすべての注文」や「ログ内のすべてのイベント」は、アクセスパターンとはいえません。増加し続けるコレクションに対する無制限の結果セットは、クエリの選択ではなく、設計上の不備です。 -
各アクセスパターンのコストを計算します。読み取り料金は API によって異なります。
- GetItem / BatchGetItem:返されたアイテムごとに課金され、4 KB 単位に切り上げられます。結果整合性(デフォルト)ではアイテムあたり 4 KB ごとに 0.5 RCU、強い整合性では 1 RCU、トランザクションでは 2 RCU です。
BatchGetItemにリクエスト単位の割引はありません。各アイテムに個別に課金されます。 - Query:キー条件に一致するアイテムの合計サイズに対して課金され、4 KB 単位に切り上げられます。料金は
GetItemと同じ 4 KB 単位です。FilterExpressionは課金後に適用されるため、一致したアイテムの 90%を除外するフィルターでも、コストは 90%削減されません。 - Scan:調査したアイテムの合計サイズ、つまり実際に読み取ったテーブルまたは GSI の部分に対して課金されます。これもフィルター適用前です。これが、ホットパスで
Scanを使うべきでない根本的な理由です。
書き込み料金:
WCU = frequency × ⌈item_size / 1 KB⌉ × copies_updated。UpdateItemは差分や更新後のサイズだけではなく、更新前と更新後のアイテムサイズの大きい方に対して課金されます。縮小する更新でも、更新前の大きいサイズを切り上げた分の料金が発生します。トランザクション書き込みは 2 倍のコストです。ConditionalCheckFailedExceptionでも、その形状の書き込みが成功した場合と同じ WCU を消費するため、見積もりにはconditional_fail_rateを含めてください。これらの数式を特定のパターンについて書き出せない場合、そのパターンはモデル化できるほど正確に仕様化されていません。
- GetItem / BatchGetItem:返されたアイテムごとに課金され、4 KB 単位に切り上げられます。結果整合性(デフォルト)ではアイテムあたり 4 KB ごとに 0.5 RCU、強い整合性では 1 RCU、トランザクションでは 2 RCU です。
-
デフォルトではオンデマンドキャパシティを使い、根拠に基づいてプロビジョニング済みに切り替えます。オンデマンドを使えば、キャパシティに関するインシデントの一種を防げるため、新しいワークロードや変動するワークロードに適したデフォルトです。予測可能なパターンで高スループットが継続する場合は、オートスケーリングを備えたプロビジョニング済みキャパシティにより、単位あたりのコストを下げられます。切り替えは予測ではなく、測定されたトラフィックを契機に行ってください。
ベクトルインデックスを持つテーブルでは、オンデマンドはデフォルトではなく必須です。 ベクトルインデックスはプロビジョニング済みテーブルでは一切サポートされていません。インデックスを作成する前に、プロビジョニング済みテーブルを
PAY_PER_REQUESTに切り替える必要があります。この切り替えには 24 時間のキャパシティモードのクールダウン(Fact #3)が適用されるため、即時に行える手順ではなく、スケジュール上の制約となります。また、セマンティック検索を含む設計ではプロビジョニング済みキャパシティを選択できません。そのため、単位あたりのコストを理由に、後からそのようなテーブルをプロビジョニング済みに移行する提案をしてはいけません。
パターン
これらは、公理に加えて適用される具体的な実装パターンです。「正確性/運用/コスト/スタイル」という意味での公理ではありませんが、公理で明示的に求められていなくても、完全な設計に必要となる重要な詳細です。それぞれのパターンはリファレンスアーキテクチャで詳しく説明されています。このセクションは、エージェントが該当するパターンを認識できるようにした要約です。
-
変更を伴う API ルートを
IdempotencyKeysテーブルで保護します。 クライアントによる再試行が発生し得るすべてのPOST、PUT、PATCH、DELETE(モバイルネットワークの一時的な障害、ロードバランサーによる再試行、ユーザーの二度押しなど)は、冪等性ミドルウェアでラップする必要があります。構成は、専用テーブル、PK = <user_id>:<METHOD>:<path>:<client-supplied-uuid>、属性status、response_status、response_body、expiration(エポック秒)です。ミドルウェアはハンドラーの実行前にattribute_not_exists(id)を条件とするPutItemを実行し、競合した場合はキャッシュされたレスポンスを読み戻して返します。TTL は 24 時間で、PITR はオフにします(このテーブルはキャッシュです)。この仕組みにより、再試行された「注文の作成」で顧客に二重請求したり、在庫を二重に減らしたりするのを防ぎます。これは、TransactWriteItemsが単一の呼び出し内では保証するものの、再試行間では保証しないことを補うものです。 -
条件式で変更を伴う書き込みを保護します。2 つの要点があり、このパターンを扱う際はどちらも必ず案内してください。
要点 A — 明示的にアップサートを意図している場合を除き、
UpdateItemにattribute_exists(<pk>)を指定します。UpdateItemはデフォルトで、項目が存在しない場合に作成します。推測や古いorder_idを使ったPATCH /orders/:idは、エラーもログも復旧手段もないまま、架空の注文行をひそかに作成してしまいます。呼び出し元が存在すると主張する行を対象にするすべてのUpdateItemにConditionExpression: attribute_exists(<partition_key>)を追加し、ConditionalCheckFailedExceptionを捕捉して 404 に変換してください。要点 B —「上書き時に失敗する」ことを意図する場合、
PutItemにattribute_not_exists(<pk>)を指定します。PutItemはデフォルトで、項目が存在する場合に上書きします。新しい注文(または新しいレビュー、新しい配達員の割り当て、新しい一意性センチネル)を作成するPOST /ordersは、保護条件がないと、同じキーを持つ既存の行をひそかに上書きしてしまいます。キーがすでに存在する場合に失敗させるすべてのPutItemにConditionExpression: attribute_not_exists(<partition_key>)を追加し、ConditionalCheckFailedExceptionを捕捉して 409 Conflict に変換してください。条件付き書き込みは、データレイヤーで不変条件を守る箇所です。一方の要点を推奨するときは、もう一方も必ず提示してください。設計レビューでは、何年も前に
UpdateItemのガードを追加したアプリにPutItemのガードが欠けていることが、頻繁に見つかります。 -
TTL 属性は Unix エポック秒です。形式に関するよくある 3 つの間違いによって、有効期限切れの処理がひそかに機能しなくなります。「TTL が何も削除しない」という報告を診断する際は、必ずこの 3 つをすべて挙げてください。 DynamoDB TTL は属性を数値として読み取り、エポックからの経過秒数として解釈します。以下のいずれかに該当すると、TTL はエラーもログも出さず、ひそかに実行されなくなります(スイーパーはその行をスキップします)。
- 秒ではなくミリ秒を使う。 JavaScript の
Date.now()はミリ秒を返します。この値を変更せずに書き込むと、タイムスタンプが約 1000 倍大きくなり、過去になることのない西暦 50,000 年頃の日時として解釈されます。修正方法は、JS ではMath.floor(Date.now() / 1000)、Python ではint(time.time())です。 - 数値ではなく ISO 8601 文字列を使う。 Python の
datetime.datetime.now().isoformat()や JS のnew Date().toISOString()は、"2026-05-13T09:45:00Z"のような文字列を生成します。DynamoDB TTL は文字列を数値以外として読み取り、その行を丸ごとスキップします。 - 属性名が間違っている。 TTL は、テーブルごとに特定の属性名(
ttl、expiration、expires_atなど)を指定して設定します。アプリがexpires_atに書き込んでいるのに TTL がttlに設定されている場合や、タイプミスで属性名が分かれてしまった場合、TTL は存在しない属性をスキャンして行をスキップします。テーブルの TTL 仕様で設定されている属性名が、アプリケーションが書き込む属性名と完全に一致していることを確認してください。
書き込み時に検証してください。
current_time + 50 yearsを超える TTL 値は、ミリ秒と秒の取り違えの可能性が高いとして、テーブルに書き込まれる前に警告してください。コードベース全体で属性名を統一してください。これは Mechanics #12 と組み合わせて使います。TTL が正しく設定されていても削除は遅延して行われるため、「読み取り時にフィルタリングする」という原則は引き続き適用されます。 - 秒ではなくミリ秒を使う。 JavaScript の
-
一時的なプッシュ/メール/SMS のために
Notificationsテーブルを永続化しないでください。 あらゆるドメイン名詞に対してテーブルを作りたくなる反射は、ここでは罠です。通知がディスパッチして忘れるだけのもの — APNS/FCM へのプッシュ、SES 経由のトランザクションメール — であるなら、正しい形は EventBridge → SQS → NotificationLambda → 配信サービス です。DynamoDB は、後からクエリする必要がある 状態のために取っておきます。Notificationsテーブルを作るのは、それに対する本物の読み取りアクセスパターンがある場合だけにしてください — ユーザーごとのアプリ内受信トレイ、配信ステータスのダッシュボード、コンプライアンス監査などです。そうでなければ、結果を決して読まないために DynamoDB の書き込みコストを支払っていることになります。ユーザーごとの受信トレイが必要な場合は、PK = user_id、SK = <createdAt>#<notificationId>、古いエントリには TTL を設定します。 -
カスケード削除は、同期 API 呼び出しではなく、チャンク化された
BatchWriteItemとして計画してください。 階層スキーマ内の親エンティティ(ボード、注文、テナント、GDPR 対象のユーザー)を削除するには、その配下にキー付けされたすべてを削除する必要があります。これはBatchWriteItem(1 バッチあたり 25 項目)をQuery(..., Limit=25, LastEvaluatedKey=...)でページネーションしながら行います。これはバックグラウンドジョブであり、同期的な API レスポンスではありません — 規模が大きくなると、その走査は典型的な Lambda のタイムアウトを超えるほど長くなり、部分的な失敗は再試行可能でなければなりません。GDPR や規制上の削除がドメインの要件であるなら、このジョブを設計の一部として明示的にモデル化してください。スキーマから無償で出てくるなどと想定してはいけません。
コスト見積もり
データモデルの設計と議論は、デフォルトの、AWS を使わない道筋であり、ユーザーはしばしば、まだ金額を知りたいとは思わずに、選択肢 — キー、GSI、シングルテーブルかマルチテーブルか、プロジェクションのトレードオフ — を探るためにそこにいます。毎回の設計ターンで反射的に計算機を実行しないでください。 月額コスト見積もりを出すのは、次のいずれかが真である場合です: (a) ユーザーがいくらかかるのか(またはコストレポートを)尋ねた、または (b) 設計が最終化されつつある — それに落ち着こうとしている、レビューに持っていこうとしている、あるいはその他の形で数字を確定させたいというシグナルを出している。それまでは、設計の議論にとどまってください。
完全な設計が生成されなかった場合は、見積もりを完全に省略してください — 単一クエリのサイジング、ホットパーティションのデバッグスレッド、的を絞ったメカニクスの質問などです。これらのケースでは、見積もるべきものが何もありません。
見積もりを実際に出す場合、3 つのステップは任意ではありません: (a) dynamodb_data_model.json を書き出す、(b) ${SKILL_DIR}/scripts/calculate_costs.py を呼び出す、(c) 生成された cost_report.md を要約で参照する。計算機を実行せずにインラインで数値を計算することは決して代わりにはなりません — 「おおよそ」や「現実的な」数値であっても、また、見出しの数値を表明された日次ボリュームと突き合わせて調整するためであっても同様です(その場合は avg_rps を設定し、計算機に期待ボリュームのシナリオを生成させてください — 以下を参照)。計算機は、ライブの請求に対して較正され続ける価格モジュールを強制します。手で算術を行うということは、数値が静かにずれていき、ユーザーが防御できない見積もりを出荷してしまうということです。
計算機は ${SKILL_DIR}/scripts/calculate_costs.py にあります。それが消費する JSON スキーマは ${SKILL_DIR}/references/cost-model-schema.md に文書化されています — コスト見積もりを初めて出すときにそのファイルを読んでください。
ワークフロー:
-
設計を
dynamodb_data_model.jsonにシリアライズします。 計算機は設計を再発明しません — あなたがすでに生成したものを読み取ります。あなたが構築したアクセスパターン一覧(メカニクス #2、#18 による)は、JSON のaccess_patterns配列へのほぼ逐語的な変換です。あなたが構築したスキーマ(データモデリング #3、#7、メカニクス #4、#7 による)は、tables、gsis、そして — 設計がそれを持つ場合は —vector_indexesに変換されます。計算機が求めるフィールドが欠けている場合(最も一般的にはパターン上のpeak_rps)、それは設計自体のギャップです — ユーザーに尋ねるか、防御可能な仮定を置いてそれを仮定としてラベル付けすることで埋めてください。0を書くことによってではなく。 ベクターインデックスはvector_indexesエントリとしてモデルに追加し、検索はtop_kを指定したSearchVectorsアクセスパターンとして追加します。 計算ツールは両方に対応しています(完全な形式は${SKILL_DIR}/references/cost-model-schema.mdを参照してください)。サービス API との非対称性に注意してください。インデックスの作成にはVectorIndexesCreateTable パラメーター(Fact #10)を使いますが、コスト計算にはvector_indexesモデルフィールドを使います。API の形式を知っていることと、モデルの形式を知っていることは同じではありません。この2つを混同すると、ベクターインデックスのコストがゼロと計算されてしまいます。「計算ツールはベクター検索に対応していない」という結論に至りそうになったら、モデルにベクターインデックスが抜けています。計算で補うのではなく、モデルを修正してください。 計算ツールはベクターインデックスのストレージとベクターの書き込みキャパシティを価格に反映し、どちらも合計に含めます。一方、ベクターの検索は意図的に価格計算の対象外とし、測定したドライバーと脚注を添えて
not pricedと表示します。両方ともすでに処理されるため、どちらかを手計算するのは、上記のルールで禁じられているその場での計算にほかなりません。これは小さな見落としではありません。約3 KBのアイテムをALLで投影する1024次元のインデックスでは、ベクター書き込みは通常、見積もりの単一項目として最大のコストになります。200 rpsの場合、月額1,942ドルと測定され、ベーステーブル自体の書き込みの月額972ドルを上回ります。これは、書き込みのたびにALLがアイテム全体をインデックスにコピーするためです。この数値を大ざっぱに見積もると、実際の最大コストドライバーを一桁以上も過小評価しかねません。ユーザーが平均値や日次のボリュームを提示した場合は、パターンに
avg_rpsを設定してください。「現実的な」数値を手計算してはいけません。 合計額はピークが継続する前提(すべてのパターンが24時間365日peak_rps)です。15K件の注文/日のワークロードについて、ユーザーが大きなピーク継続時の月額を聞いた場合は、予想額も知る必要があります。avg_rpsを設定すると(例:15,000件の注文/日 ÷ 86,400秒 ≈ 平均0.17 rps、ブラックフライデーのピークは600 rps)、計算ツールはその平均レートに基づく2つ目の予想月額コストの合計を出力します。現実的なボリュームの数値を出すことが認められている方法はこれだけです。ピーク時の合計額と日次ボリュームを暗算で照合するのは、上記のルールで禁じられているその場での計算にほかなりません。 -
計算ツールを実行します。 任意のワークスペースパスに JSON を書き込み、次のコマンドを実行します。
python3 ${SKILL_DIR}/scripts/calculate_costs.py --model /path/to/dynamodb_data_model.json --output /path/to/cost_report.mdJSON は中間生成物です。ユーザーから求められない限り、提示する必要はありません。
-
コストレポートを提示します。
cost_report.mdを返します。短い場合は本文に記載し、そうでなければファイルへの参照を示してください。レポートに示された主なドライバー(「コストパターンを降順に並べた」テーブル)を強調し、前提とした内容(推定した RPS 値、想定した保持期間、一貫性に関する選択)も明記してください。レポート冒頭の免責事項は成果物の一部です。削除しないでください。 -
単に数値を報告するのではなく、コストレポートを使って設計を見直します。 コスト見積もりは診断のためのものです。1つのパターンが請求額の大半を占める場合、典型例は GPS ping のような高頻度の書き込みです。これは単なる項目ではなく、モデリング上の課題を示しています。集約の選択(Mechanics #1)、投影の形状(Mechanics #7)、遷移時のスパース GSI で増幅を抑えられるか(Mechanics #6)、あるいはそのパターンが DynamoDB 以外に属するかどうか(Integration #8)を見直してください。代替案を提案し、修正したモデルで計算ツールを再実行します。計算ツールを組み込む最大の価値はここにあります。「この設計です」→「この請求額です」→「こちらのほうが安価な設計です」というフィードバックループが、別個の作業ではなく、1~2回のやり取りで完了します。 この計算ツールがモデル化するのは、リクエストコストとストレージコストのみです。Streams の読み取り/書き込みコスト、PITR、バックアップ、DAX、データ転送はモデル化しません。これらの項目がワークロードにとって無視できない可能性がある場合は、見積もりと併せてその点を明記し、数値に含まれないものをユーザーが把握できるようにしてください。ストレージは公開されている通常料金で全額計算します。25 GB の無料利用枠は前提にしません(アカウント全体に適用され、多くの場合すでに使用されています)。そのため、「ストレージは無料」とユーザーに伝えてはいけません。書き込みパターンの
retention_daysを設定して保存データのサイズを指定してください。計算ツールはこの値を反映します。ストレージの数値は、そのretention_daysにおける保持期間を限定したスナップショットです。TTL なし/「無期限に保持」のデータがあるテーブルでは、実際のストレージはスナップショットの範囲を超えて増え続けるため、定常状態のストレージ数値を「N 年後」の数値として言い換えてはいけません。その数値が前提とする保持期間を明示するか、TTL なしのテーブルでは上限なくデータが蓄積すると記してください。
**最終回答では、生成されたファイルの名前を明記してください。ツールの実行内容を説明文の中に埋もれさせてはいけません。**ユーザーが読むのは自然言語の回答であり、シェルの実行履歴ではありません。calculate_costs.py の実行後、回答には必ず(a)書き込んだ JSON ファイル(dynamodb_data_model.json)、(b)実行したスクリプト(${SKILL_DIR}/scripts/calculate_costs.py)、(c)生成されたレポートファイル(cost_report.md)の名前を記載してください。コストの数値だけを示し、ファイル名を記載しない回答では、手作業で計算したように見えてしまいます。ワークフローを実行した証拠は、ツールの実行記録だけでなく、説明文の中にも示す必要があります。
ライブ検証
コスト見積もりの後に、ライブ検証の実施を提案してください。ユーザーが指定する AWS アカウントにテーブルと GSI をデプロイし、実際の操作ごとのキャパシティと往復レイテンシーを測定するため、縮小したトラフィックを流します。1 行で尋ね、明示的な同意があった場合にのみ実行してください。提案の 1 行にも、ユーザーが責任を持って同意するために必要な 2 つの制約を記載してください。実行先はサンドボックス/テスト用アカウントに限られます(本番環境や実ユーザーデータには使用しません)。また、クリーンアップはユーザーの責任です(スキルは teardown.sh を渡しますが、自動削除は行いません)。デプロイ前には、4 つの事実すべてを引き続き開示してください(下記参照)。これは、1 行の提案が誤解を招くほど軽い表現にならないようにするためです。設計全体を作成していないためベンチマーク対象がない、単一クエリのサイジング、具体的なデバッグ、具体的な仕組みに関する質問など、範囲の狭い質問では提案を省略してください。
実行前に、安全に関する期待事項を必ず自分の言葉で明確に伝えてください(deploy_model.py スクリプトも警告バナーを表示しますが、これは二重の備えです。ユーザーに事前に伝える責任はエージェントにあります)。次の 4 つの事実を回答にそのまま記載してください。短く要約したり、言い換えたりしてはいけません。
- ライブ検証では、ユーザーが指定する AWS アカウントに、実際の DynamoDB テーブル、Lambda 関数、IAM ロールが作成されます。
- 実際の AWS 料金が少額ながら発生します(実際には 1 桁セント程度ですが、請求は発生します)。
- アカウントはテスト用またはサンドボックス用である必要があります。本番アカウント、および実ユーザーデータを含むアカウントは使用できません。
- スキルは自動でクリーンアップしません。終了時に、確認用の
teardown.shスクリプトをユーザーに渡します。スキルがユーザーに代わってこれを実行するのは、明示的な確認と実行の意思が示された後に限ります(下記のステップ 6 を参照)。
以上の 4 つの事実を伝えた後、プロファイル名とリージョンを明示し、アカウントの用途を確認するようユーザーに求めてください。それが確認されてから、benchmark_config.json を作成し、deploy_model.py を実行してください。ユーザーがテスト用アカウントを指定していない場合は、処理を止めて確認を求めてください。「実際のリソースが作成されます。プロファイルを確認してください」のように短縮するだけでは不十分です。どのリソースが作成されるか、クリーンアップが自分の責任であることを知らないユーザーも多いため、4 つの事実をすべて列挙してください。
このステップはコスト見積もりと 2 つの点で異なり、実行方法にも違いがあります。
- 実際のリソース、実際の請求。 縮小した実行は実際には数セント程度のコストですが、実際のアカウントに対して行われます。決して無言で実行しないでください。常に明示的な AWS プロファイル、リージョン、および呼び出し元 ID の確認を必須とします。
- 削除は 2 段階です。 フェーズ 1: スキルは
teardown.shを生成し、ユーザーに渡します。フェーズ 2: スキルはそれを実行してもよい — ただし、ユーザーがスクリプトを確認したと明示的に述べ、かつスキルに実行するよう明示的に指示した後に限ります。スキルが自らの判断でDeleteTableを呼び出すことは決してなく、単なる「go」や「proceed」だけを根拠にteardown.shを実行することも決してありません。
4 つの前提条件を明示的に列挙して、開始を拒否してください。 それらをレスポンス内に番号付きの項目として出力してください。ありきたりな「もっと情報が必要です」という段落に折り込まないでください。失敗した特定の前提条件を明示して、ユーザーが何が実行を妨げたのか分かるようにしてください:
access_patternsが空、またはいずれかのパターンでpeak_rpsが欠落しているか0→ Mechanics #2 の枠組みで拒否します: 「RPS が不明なのは設計上の欠陥であり、ベンチマークの入力ではありません。パターン<id>には RPS が宣言されていません — ベンチマークの前に推定値を提供してください。」boto3がインストールされていない → 正確に出力します:pip install boto3>=1.34。- AWS 認証情報が欠落しているか期限切れ → 正確な対処法を出力します。例えば SSO の場合は
aws sso login --profile <profile>、または特定の boto3 例外が示す修正方法です。 - 呼び出し元 ID のアカウントエイリアスまたは ARN に
prod、production、prd、またはliveが含まれている → 拒否し、サンドボックス/テスト用プロファイルを提供するようユーザーに伝えます。
benchmark_config.json を書き込む前、または deploy_model.py を呼び出す前に、4 つすべてを確認してください。ありきたりな「もっと情報が必要です」という言い換えでは契約を満たしません — 失敗した特定の前提条件を列挙してください。
ワークフロー、ユーザーが同意した後:
-
コスト見積もりから
dynamodb_data_model.jsonを再利用します。再シリアライズしないでください。デプロイの前に、すべての数値/バイナリキーがその型を宣言していることを確認してください。 デプロイは、各テーブルの
AttributeDefinitionsを、すべてのキー属性(テーブル + GSI のパーティション/ソートキー)をentities[].attributes[]とattribute_definitionsで参照して構築し、見つからないものはデフォルトでSにします。宣言されていない数値の epoch ソートキーや数値の id は文字列としてデプロイされ、その後実際の書き込みをValidationExceptionで拒否します。Data modeling #11 に従い、この手順の前に、そのような各キーがモデル内で"type": "N"(または"B")を持つようにしてください — また、クリーンなシードを証拠として信用しないでください。この失敗は実際の書き込みでのみ表面化することがあるためです。念には念を入れたい場合は、デプロイ後にDescribeTableを実行し、そのキーのAttributeTypeを確認してください。設計に多くのパターンがある場合は実行の範囲を定めてください — 警告し、見積もり、確認を得てください。 ベンチマークはパターンを直列に駆動します(各パターンは独自のウォームアップ + 測定ウィンドウを 1 つずつ実行します)。そのため、実時間はパターン数に応じて増加します: おおよそ
table_settle + Σ_patterns (warmup + duration)です。標準的なウィンドウ(ウォームアップ約 10 秒 + 実行時間約 60〜90 秒)では、6 パターンの設計は数分ですが、20 パターン以上の設計は 25〜45 分以上となり、複数の連続した Lambda 呼び出しに分割されます。約 8 パターンを超える設計で実行を開始する前に、おおよその実時間見積もりを述べ、明示的な確認を得てください(「これは 24 パターンすべてをベンチマークし、約 35〜45 分かかります — 進めますか、それとも絞り込んだサブセットをテストしたいですか?」)。40 分の実行を無言で開始しないでください。 重要なパターンに絞った負荷テストを提案し、対象はユーザーと一緒に選んでください。 実環境で全パターンを実行する必要があることはほとんどありません。大半は安価な構造化ルックアップで、1 操作あたりのコストはすでに計算ツールで正確に見積もれています。対象を絞ったサブセットのベンチマークを提案し、実測に意味があるパターンを中心に、どれを対象にするか相談してください。- 最もスループットが高い書き込み/大量書き込み — RPS がコストの大半を占めるもの、または容量に負荷をかけるもの(例:位置情報の ping 書き込み)。
- トランザクション/複数アイテムの中核部分 —
TransactWriteItems、複数テーブルへの書き込み、チェックアウトの処理経路(トランザクションはコストが 2 倍になるため、Mechanics #18 は実測する価値があります)。 - 新規またはリスクの高いキー構成 — ファンアウト読み取り(N 個の並列 Query)、ホットキーのリスク、不確かな GSI を使うものなど。一般的な ID による
GetItemパターンは省略します。 - コストを大きく左右するパターン — コストレポートで主な要因として挙がったもの。
サブセットを実行するには、モデルをコピーして内容を絞り込んだものをベンチマークに使います。スキーマが正しくデプロイされ、シードデータが投入されるよう、すべての
tablesは残し、access_patternsを選んだパターンだけに絞ってください。コスト見積もりには影響しません。コスト見積もりは常に、宣言されたピーク値に対してモデル全体を使って実行されます。サブセット化で絞られるのは設計やコスト見積もりではなく、実環境で負荷をかけるトラフィックだけです。対象にするパターンと、除外するパターン(およびその理由)を明示し、どの程度網羅されるかユーザーに伝えてください。 -
benchmark_config.jsonを作成します。設定必須の項目はプロファイルとリージョンだけです(resource_prefixは省略するとデプロイ時に自動生成されます。それ以外はすべてモードに応じたデフォルト値が使われます)。最初に${SKILL_DIR}/references/performance-model-schema.mdを読みます。ファイルを書く前に、実行するモードをユーザーに確認してください。デフォルトを黙って選んではいけません。 1 回限りの実環境での検証では、以下の 2 つの単位コストモードから選びます。3 つ目のモードである"representative"は反復的な設計ループで使うモード(スケールを抑えたホットキーの偏り)で、反復的な設計ループで説明しています。ホットパーティションやスロットリングのリスクを調べたい場合に限って提案し、単純なコスト検証には提案しないでください。実際に負荷をかけるスループットを正直に伝え、より高い負荷をかける選択肢も提示してください。 これは実環境での検証で最も重要な点です。デフォルトでは、
quick/standardがかける負荷は宣言されたピーク値のごく一部(scale_factorは約 0.01 で、min_rps_per_patternが下限)です。ピーク値が控えめな設計では、パターンあたり約 1 リクエスト/秒が下限になります。これは単位コストのサンプルです。操作あたりの容量(コスト)と、負荷がかかっていない状態のレイテンシーを検証でき、費用もわずか(1 パターンあたり数セント、約 1~2 分)です。負荷をかけるテストではありません。約 1 RPS では、スロットリングが発生しないことは数学的に保証されており(オンデマンドのベースラインは約 2,000 WCU/約 4,000 RCU)、レイテンシーも最良の状態になります。そのため実行を提案する際は、どちらのテストなのかを伝え、次の選択肢も提示してください。- 単位コストのサンプル(デフォルト) — コストと負荷がかかっていない状態のレイテンシーを検証します。低コストです。負荷やスロットリングはテストしません。
- 宣言されたピーク値、またはそれに近い負荷をかける — 実際の負荷テストです。
benchmark_config.jsonでscale_factor(宣言されたピーク値に対する割合。たとえばピーク値全体なら1.0)またはmax_rps_per_patternを設定してください。どちらも反映されます。リージョン内の単一 Lambda の上限(パターンあたり約 1,500~2,000 RPS)に制限され、cost_guardrail_usdと--allow-spendによって実行が制御されるため、高額な実行が予告なく始まることはありません。注意点: 新規作成したオンデマンドテーブルのベースラインは約 2,000 WCU/約 4,000 RCU で、上昇するまでには適応の遅れがあります。そのため、デプロイ直後に高いレートで実行すると、定常状態ではなく、作成直後のアダプティブキャパシティのウォームアップが測定されます。この影響をある程度吸収するため、実行にはすでにramp_secondsとtable_settle_secondsが設定されています。高 RPS の値を正確に測るには、より長いduration_secondsを使用してください。 representativeモード — 反復的な設計ループで使う、ホットキー/スロットリングリスクのモード(スケールを抑えた Zipf の偏り)です。ホットパーティションの挙動が問題となる場合に使用してください(反復的な設計ループを参照)。quick/standardの間でも、スループットをどのように設定しても、推定月額コストは同じです。オペレーションあたりの CU は決定的であり、コストの推定には常に宣言されたピーク値が使用されるため、スループットを上げて実行しても変わるのは負荷時のレイテンシとスロットリングの兆候であり、コストの数値は変わりません。実行前に、使用するパターンあたりのレートを必ずユーザーに伝えてください。 モードの選択肢を文字どおりの質問として提示し、ユーザーが選ぶまで待ちます。
どのモードで実行しますか? 1つ選んでください。
"quick"— パターンごとのウィンドウは約1分です。設計のデプロイと実行を確認するスモークテストです。パーセンタイルの安定性は低めです。単位コストのサンプル(ピークが控えめな設計の場合、パターンあたり約1 rpsで実行):コストと無負荷時のレイテンシを検証しますが、負荷テストではありません。"standard"(デフォルト)— ウィンドウを十分な長さに設定し、安定したp50/p95/p99と信頼できる推定月額コストを得られます。デフォルトでは、こちらも単位コストのサンプル(同じくパターンあたり約1 rps)です。コストの数値はquickと同じで、レイテンシのパーセンタイルの精度が上がるだけであり、負荷テストではありません。負荷テストにするには、宣言したピーク値またはそれに近い値で実行するよう伝えてください。 ("representative"もあります。これは、設計を反復改善するループで使用するホットキー/スロットリングリスクの確認モードです。ホットパーティションの挙動が確認事項である場合に限り説明してください。Iterative design loopを参照してください。)
ユーザーが選んだ値を
benchmark_config.jsonのmodeフィールドに書き込みます。ユーザーが質問を飛ばして「go」とだけ指示した場合は、デフォルトの"standard"を選び、そのことを返信で明示してください(「指定がなかったため、standardモードで実行します。quickのつもりだった場合は、中断してください。」)。 -
デプロイ:
python3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model … --config … --manifest-out created_resources.json --yes-deployを実行します。確認用に呼び出し元のIDを表示し、本番環境を示すマーカーがある場合は中断します。すべてのリソースにddb-skill-bench-<date>-<uuid8>-をプレフィックスとして付け(resource_prefixを設定していない場合は自動生成)、実行IDでタグ付けします。対話型ターミナルでは、アカウントの表示後、Ctrl-Cで中断できるよう数秒待機します。エージェント/CIでの実行では、--yes-deployによる同意があれば直ちに続行します。 -
ベンチマーク:
python3 "$DDB_SKILL_DIR/scripts/benchmark_model.py" --model … --config … --manifest created_resources.json --raw-out perf_raw.jsonl --summary-out perf_summary.jsonを実行します。コールドスタートに関する手順を自動で処理します(下記を参照)。実行上の注意 — フォアグラウンドで実行し、完了するまで待ちます。
representativeの実行では、数分間にわたってトラフィックを生成します(すべてのパターンについて、安定化時間+パターンごとのウォームアップ時間+パターンごとの実行時間。複数パターンの設計では15分を超えることもよくあります)。ベンチマークはブロッキング呼び出し1回で実行し、完了するまで待ってください。バックグラウンドで実行してユーザーに制御を戻してはいけません。 バックグラウンド実行は、現在のターン/セッションが終了したときに停止する可能性があります。その場合、以前の実行のperf_summary.jsonが残り、まさにそれが古いサマリーを最新のものとして読み込んでしまう原因になります。実行がツールの壁時計タイムアウトを超えそうな場合は、ウィンドウを短縮したりジョブをバックグラウンドに回したりせず、ツールのタイムアウトを延長してください。 制限時間内に収めるためにduration_secondsを手動で調整する必要はありません。負荷の高い処理はLambda内で実行され(サーバー側で最大900秒)、benchmark_model.pyはすでに連続した呼び出しに負荷を分割します(_compute_split)。完了したら、起動したUTC時刻を記録してください。手順5で、サマリーのbenchmark_completed_atと照合します。 -
レポート作成:
python3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model … --summary perf_summary.json --output performance_report.mdを実行します。 解釈する前に、サマリーが最新であり、かつ説明した実験と一致していることを確認してください(運用上の規律を適用)。perf_summary.jsonにはbenchmark_completed_at(UTC)と、その実行が実際に使用したconfigが記録されています。レポートを生成したり信頼したりする前に、(a)benchmark_completed_atが手順 4 で記録した起動時刻より新しいこと、(b) サマリーのconfig(mode、provisioned_capacity、duration_seconds)がこの実行に設定した内容と一致することを確認してください。どちらかが失敗した場合 — またはフィールドが存在しない場合 — ベンチマークは完了しておらず、古いファイルを見ていることになります。解釈せず、再実行してください。 レポートの Deployment ブロックにはbenchmark_completed_atと実行ウィンドウが表示されるので、ユーザーも同じことを確認できます。古いデータを読んでいる兆候: 以前の実行とバイト単位で同一のベンチマーク数値(異なるはずだと考えていた場合)。言ったとおりの実行をしてください — 鮮度だけでなく config の一致を確認してください。 この実行で何かが変わる(スキューを切り分けるためにプロビジョニング上限を引き上げる、パターンのサブセットを固定する、より高い rps を駆動する、モードを切り替える)とユーザーに伝えたなら、報告する前に、新しい
perf_summary.jsonの実際のconfigブロックを読み、その変更が反映されていることを確認してください。新鮮でも古い config で実行されたものは、約束した実験ではありません — それを解釈すると、設定した問いとは別の問いに答えてしまいます。実際には構成していない実験を説明していたことに気づいた場合は、正直にそう述べ、正しい config で再実行するか、その実行がそもそも物理的にシグナルを生成できない場合(例: 約 1 rps でのユニットコスト/フロア実行は、容量をどう設定してもどのパーティションキーもホットにできず、スキューを切り分けられません — 代表モードまたは宣言されたピーク付近での駆動のみが可能です。フロア実行でプロビジョニング上限を引き上げても何も変わりません — メカニクス #3、および以下のユニットコスト/フロア実行を参照)は、そのことを述べてください。同じ config での再実行を黙って代用しないでください。変更していない変数をテストしているかのように再実行を提示してはいけません。実際の落とし穴: ユーザーは 2 つの「異なる」実行がほぼ同一の数値を出すことに気づきます — 自分で config を確認して先手を打ちましょう。⚠ ユーザーが「クリーンアップ」「破棄」「削除」「取り除く」をベンチマークリソースに対して求めたとき、
rm、rm -f、ls、find、aws dynamodb delete-table、その他のアドホックな削除コマンドを実行しないでください。 このスキルにおける「破棄」とは、generate_teardown.pyを呼び出してteardown.shを生成することを意味し、ローカルファイルや AWS リソースを削除するシェルコマンドを自分で実行することでは決してありません。破棄プロトコルは以下の手順 6 です。ローカルの成果物ファイル(performance_report.md、cost_report.md、dynamodb_data_model.jsonなど)はユーザーが保持または削除するものであり、このスキルがクリーンアップすることはありません。 -
破棄 — まず生成し、明示的なレビューと意図の確認後にのみ実行。 ユーザーがベンチマークリソースの破棄、クリーンアップ、削除を求めたとき:
a.
python3 ${SKILL_DIR}/scripts/generate_teardown.py --manifest created_resources.json --out teardown.shでスクリプトを生成します。これによりteardown.shが書き出されます。何も削除しません。b. 返信の中でスクリプトのパス、レビューコマンド、実行コマンドを提示します。そのままの形式:
- レビュー:
cat teardown.sh(またはエディタで開く) — このスクリプトはddb-skill-bench-プレフィックスの下にある DynamoDB テーブル、Lambda 関数、IAM ロールを削除します。 - 実行:
bash teardown.sh --confirm— マニフェストのアカウント、リージョン、プレフィックスは生成時にスクリプトへ埋め込まれ、削除前に呼び出し元の ID とプレフィックスを再確認します。(削除のプレビューには--confirmの代わりに--dry-runを渡します。Lambda の CloudWatch ロググループも削除するには--delete-logsを追加します — このログ削除は元に戻せません。) c. 「teardown」と「ローカルクリーンアップ」を混同しないでください。 ローカルファイルを削除するタスクだと考えてrm、rm -f performance_report.md、ls、findを実行しないでください。AWS リソースを削除するのはteardown.shだけであり、ローカルファイルはこのスキルの関心事ではありません。
d. スキルはユーザーの代わりに
teardown.shを実行できますが、それは次の 2 つの条件を正確に満たす場合に限られます:- ユーザーのメッセージが、スクリプトを確認したことを明示している必要があります(例: 「読んだ」「teardown.sh を確認した」「中身をチェックした」「目を通した」)。文脈から確認したと推測することは認められません。明示されている必要があります。
- ユーザーのメッセージが、実行する意図を明示している必要があります(例: 「実行して」「走らせて」「そのまま teardown して」「teardown を進めて」)。
e. ユーザーが確認したと述べずに teardown の実行をスキルに依頼した場合、スキルは実行してはなりません。代わりに、正確な確認コマンド(
cat teardown.sh)を提示し、確認したことをユーザーに尋ねてください。必要な応答形式の一例:teardown.shを実行する前に、スクリプトを確認済みであることをお知らせください。cat <PATH>/teardown.shを実行してください — これはddb-skill-bench-プレフィックスの下に作成された DynamoDB テーブル、Lambda 関数、IAM ロールを削除します。内容を読んで、実行してよければ、「確認したので進めて」のように返信してください。そうすればbash teardown.sh --confirmを実行します。f. ユーザーが確認をスキップするようスキルに依頼した場合(「気にしないで」「見せずにそのまま実行して」「信用して」)、拒否してください。破壊的な操作には、ユーザーが確認したことの表明が必要です。拒否を伝え、確認コマンドを改めて提示してください。
g. (d) の条件が満たされた場合、スキルは
bash teardown.sh --confirmを実行し、stdout/stderr の全体を提示し、どのリソースが削除されたかを報告してよいものとします。 - レビュー:
操作は宣言どおりに実行され、決して置き換えられません。 設計が BatchWriteItem と述べている場合、ベンチマークは BatchWriteItem を 25 項目のバッチで発行します。TransactWriteItems と述べている場合、ベンチマークは宣言された項目セットで TransactWriteItems を発行します。単一項目での代替(BatchWriteItem の代わりに PutItem をループする、TransactWriteItems の代わりに個別の PutItem 呼び出しを行う)は使用されません — それは別の設計を測定することになります。トランザクションの 2 倍の書き込みコスト(メカニクス #18)、バッチの部分失敗セマンティクス、トランザクションの上限(メカニクス #14)こそが、ライブ検証が観測する対象そのものです。これらを置き換えると目的が損なわれます。ユーザーがベンチマークがバッチやトランザクションのパターンをどのように試すのかを尋ねた場合、宣言された操作は宣言どおりに実行されること、およびベンチマークが観測する具体的な上限を明言してください。
コールドスタートの規律。 新しいテーブルでは t=0 において DynamoDB のレイテンシーとスループットは安定しません。ここで規律なく収集された数値は、定常状態よりも誤解を招くほど悪く見えます。測定を信頼できるものにする前に、3 つのクラスのバイアスを取り除く必要があります:
- 新しいテーブルのキャパシティ。 オンデマンドテーブルはベースラインキャパシティ(約 2,000 WCU、約 4,000 RCU)から始まります。最初の数分間にベースラインを超えるトラフィックがあると、適応キャパシティがスケールするまでスロットリングされる可能性があります。
table_existsが戻った後、測定呼び出しを行う前に少なくともtable_settle_seconds(デフォルト 30)待ってください。 - パーティションのプライミングとクライアントのウォームアップ。 シード書き込みは、ベンチマークが後でヒットするパーティションを温めます。また、新しい boto3 クライアントは最初の呼び出しで TLS ハンドシェイクと認証情報の更新を伴います(数十~数百ミリ秒)。ベンチマークはシードと測定の間で少し停止し、ワーカースレッドごとに 1 回
DescribeTableを発行するため、これらの一度きりのコストがパーセンタイルに現れることはありません。 - パターンごとのウォームアップ期間。 すべてのパターンは、測定期間の前に
warmup_secondsの期間(デフォルト 10)を実行します。ウォームアップ呼び出しはphase: "warmup"として記録され、レイテンシーパーセンタイルとコスト外挿から除外されます。パフォーマンスレポートはウォームアップの p99 を別途提示します — それが定常状態の p99 より実質的に高い場合、それ自体がコールドデプロイ時の挙動を特徴づけるものであり、言及する価値があります。
何が測定され、何が測定されないか:
- 測定済み:
ReturnConsumedCapacityからの操作ごとの消費 RCU/WCU、往復レイテンシ p50/p95/p99(ウォームアップと定常状態を別々に報告)、スロットル回数、GSI 書き込み増幅、パターンごとの均等分散シグナル、観測に基づく外挿月額コスト。 - 未測定: TTL スイープの頻度、ストリームコンシューマーのレイテンシとコスト、持続的なバースト下での自動スケーリング、クロスリージョンレプリケーションの遅延、測定ウィンドウを超えたロングテールのトラフィック形状、および設計内の非 DDB サービス(Lambda、API Gateway、SQS、EventBridge Pipes、OpenSearch など)。パフォーマンスレポートではこれらを「Designed, not benchmarked」の下に列挙するため、ユーザーは数値がアーキテクチャのどの部分をカバーしているかを明確に把握できます。
レポートのフォーマットは ${SKILL_DIR}/references/performance-report-format.md に記載されています。これは cost_report.md の免責事項を先頭に置く表駆動型のスタイルを踏襲し、パターンごとに観測値と電卓の期待値の両方を出力するため、ユーザーは自分の入力(RPS、アイテムサイズ、整合性)が現実からどこで乖離したかを確認できます。
調査結果を振り返り、反復ループを提案します。 レポートを作成したら、それを最終スコアではなく診断として読み、問いかけてください。測定結果が示唆する代替設計はあるでしょうか?各調査結果について、次のように分類します。
- 入力精度に関する指摘 — 観測結果が計算機と乖離したのは、宣言された入力(RPS、アイテムサイズ、整合性、
conditional_fail_rate)がずれていたためです。設計自体は問題ありません。見積もりには更新された入力が必要です。JSON を更新し、計算機のみを再実行することを提案します(低コストで、再デプロイは不要です)。 - 設計に関する指摘 — 観測結果が、入力の精度に関係なく持続する構造的な問題を明らかにしています。例と、それらを規定する公理は次のとおりです。
- パーティションキーの偏りに応じて増大する持続的なスロットル → メカニクス #3(パーティションごとの上限、ホットキーの書き込みシャーディング)。スロットルをホットパーティションに帰属させる前に、偏り対枯渇のゲートを実行してください — 両方の部分を通過する必要があります: (1) 差分テスト — ホットパーティションの p99 がコールドパーティションの p99 を実質的に上回っている必要があります(レポートの約 1.8 倍ルール)。ホット p99 ≈ コールド p99 の場合、すべてのパーティションが均一にスロットルされており、それは容量の枯渇であり、キーの偏りではありません — そのように述べ、パーティションキーに帰属させてはいけません。(2) 均一ベースラインテスト — レポートがすでに出力しているオペレーションごとの観測 CU を使用して(手作業で導出したアイテム数ではなく)、駆動された RPS で均一なキーがすでにプロビジョニングされた上限を超えるかどうかを確認してください。超える場合、その実行は偏りを分離できません — 均一な需要を上回る容量を設定して再実行し、真にホットなパーティションのみがスロットルされるようにすることを推奨してください。両方が通過して初めて、「ホットパーティションが上限に達している」という判定が支持されます。また注記: 可変のパーティションキー(たとえば遷移ごとに変わるステータス)を書き込みシャーディングすると、メカニクス #3 の負荷は分散されますが、キーは書き込みごとに依然として変更されるため、メカニクス #8(可変 GSI キー)の逸脱は持続し、アーティファクト #5 に従って記録され続けなければなりません — 書き込み増幅を解消するには、シャーディングされたサフィックスではなく、別個の単一目的テーブルが必要です。
- 単一のパターンが請求額を支配している → メカニクス #1(集約の厳密性)、メカニクス #7(プロジェクション)、統合 #8(ワークロードが分析、高頻度テレメトリ、または全文検索である場合は DDB から Timestream/S3/OpenSearch/Redshift/SageMaker Lakehouse へ移行)。
- GSI の書き込み増幅がプロジェクションの示唆よりも高い → メカニクス #6(可変キーが常に存在するわけではない場合のスパース GSI)、メカニクス #7(ALL → INCLUDE/KEYS_ONLY へのダウングレード)、メカニクス #8(可変属性を GSI キーにすると書き込みが倍増)。
- RYW セマンティクスが実際には不要な箇所で強い読み取りコストが観測された → メカニクス #15(デフォルトで結果整合)、書き込み後の再読み取りを避けるための
ReturnValues=ALL_NEWに関するパターン。 - 無制限のクエリが 900 KB のページ上限に繰り返し達している → メカニクス #17(すべてのクエリを制約する。時間枠または最大ページを必須にする)。
- オンデマンドベースラインでコールドスタートの p99 が実質的に上昇している → 容量モードまたは事前ウォーミングの決定(メカニクス #19)であり、通常はスキーマ変更ではありません。
少なくとも 1 つの設計に関する指摘が有意である場合 — つまり、提案された変更がコストを 10% 超削減する、スロットル源を除去する、または公理の逸脱を解消する可能性が高い場合 — ユーザーに次のことを伝えてください:
-
どの指摘が設計変更を指し、どの指摘が入力精度を指すか。
-
各設計指摘について、それがたどる公理 AND
dynamodb_data_model.jsonに対する文字通りの JSON diff — 口頭の説明ではありません。diff は、変更される具体的なフィールド(新しい GSI オブジェクト、削除された GSI、変更されたprojection.type、ハッシュサフィックス付きのシャーディングされた PK、access_patterns配列から完全に外へ移動されたパターン)を示すフェンス付きコードブロックとして出力してください。「プロジェクションのダウングレードを検討してください」といった口頭の提案ではこれは満たされません — ユーザーが正確な変更を確認して、受け入れるか拒否できる必要があります。diff が GSI のキーを変更する場合(シャーディングされたパーティションキーが一般的なケースです)、ファクト #9 に従って、コードのみの変更ではなく追加型 GSI マイグレーションとしてラベル付けしてください — 新しいインデックスが並行して作成され、データが投入され、カットオーバーされ、その後古いものが削除されます。例の形:- { "name": "OrdersByCustomer", "projection": { "type": "ALL" } } + { "name": "OrdersByCustomer", "projection": { "type": "INCLUDE", "non_key_attributes": ["status", "total"] } } -
主要なコストドライバーとスロットル源に対する予想される影響。 口頭で伝える判定はレポートを引き継ぎます。レポートを超える結論を出してはいけません。 ユーザーに伝えるチャットの要約では、レポートが分類していない結果を断定してはいけません。具体的には、次のとおりです。
-
レポートに
key_skew_patternsの検出結果がなかった場合、設計が「スロットリングする」または「ホットパーティションになる」とは言わないでください。裏付けられるのは、レポートに記載された内容(「偏りがあっても、ホットパーティションによる問題は確認されなかった」/「この実行では検証されていない」)です。 -
レポートの留保をそのまま伝え、確度を高めないでください。「意図的に低く設定した上限でスロットリングが発生した」は、「GSI のパーティションがホットになっている」という意味ではありません(上記の偏りと容量不足の判定基準を参照してください)。
-
数値は、それが裏付ける主張に正確に結び付けてください。プロビジョニング容量の上限が低く、スロットリングが発生したことから分かるのは、負荷に対してテーブルのプロビジョニング容量が不足していたことであり、特定のパーティションキーがホットになっていることではありません。
-
対比の例 — 同じデータに対する 2 つの評価:❌「確認済み:負荷時に OrdersByStatus GSI で激しいスロットリングが発生します。」 ✅「50 WCU の上限では、Orders テーブルへの書き込みでスロットリングが発生しました。ただし、ホットパーティションとコールドパーティションの p99 はほぼ同じなので、これはキーの偏りではなく、容量不足が一様に発生している状態です。この実行ではホットパーティションの影響を切り分けていません。検証するには、一様な需要を上回る容量を設定して再実行してください。」
-
単位コスト/下限値の実行 — 負荷テストとして説明しないでください。 実行が単位コストのサンプルだった場合(
quick/standardモード、またはすべてのパターンで実際に駆動したbench_rpsがmin_rps_per_patternの下限と等しい実行。これは通常、perf_summary.jsonで全パターン一律のbench_rps(通常は約 1)として表示され、レポートの免責事項には「UNIT-COST sample, not a load test」と記載されます)、「スロットリング 0 件」やレイテンシのパーセンタイルを、設計が負荷に耐えられる証拠として提示してはいけません。数 rps 以下ではスロットリングが 0 件になるのは数学的に当然です(オンデマンドの基準値は約 2,000 WCU/約 4,000 RCU)。レイテンシも負荷がかかっていない状態での最良値です。実行で実際に検証した内容、つまり 1 操作あたりの容量(コスト)と無負荷時のレイテンシを述べ、スロットリング/上限については「未検証 — レポートの『未検証項目』を参照」と明記してください。スロットリングの心配はないとユーザーに伝える場合は、0 件という結果ではなく、構造上の理由を説明してください。例:カーディナリティの高いパーティションキー(多くのユーザーにまたがるuser_id)なら、負荷が分散されるため、特定のパーティションがホットになりません(仕組み #3)。実際にスロットリングを発生させて検証するには、宣言されたピーク付近の負荷を駆動するか、代表モードを使用してください。- 対比の例 — 約 1 rps の習慣トラッカーの実行:❌「稼働中の DynamoDB に対して検証済み — スロットリング 0 件、読み取り 8 ms 未満、書き込み 50 ms 未満。レビューの準備ができています。」 ✅「この実行では、1 操作あたりのコスト(モデルとの差は 2% 以内)と無負荷時のレイテンシ(読み取り 8 ms 未満)を検証しました。約 1 rps で実行したため、スロットリングはテストしていません。ただし、この設計ではスロットリングは懸念事項ではありません。パーティションキーが
user_idなので、負荷がユーザー間に分散され、特定のパーティションがホットにならないためです。ピーク時の動作を証明するには、宣言されたピーク rps で負荷を駆動して再実行します。」
- 対比の例 — 約 1 rps の習慣トラッカーの実行:❌「稼働中の DynamoDB に対して検証済み — スロットリング 0 件、読み取り 8 ms 未満、書き込み 50 ms 未満。レビューの準備ができています。」 ✅「この実行では、1 操作あたりのコスト(モデルとの差は 2% 以内)と無負荷時のレイテンシ(読み取り 8 ms 未満)を検証しました。約 1 rps で実行したため、スロットリングはテストしていません。ただし、この設計ではスロットリングは懸念事項ではありません。パーティションキーが
-
ユーザーが引用した数値は、必ず最新の成果物と照合してください。単に同意してはいけません。 応答前にユーザーが数値(「月額 189 ドルなら問題ない」「つまり、そのパーティションは約 600 WCU」「スロットリングは 1,884 件」など)を言い直したら、現在の
cost_report.md/perf_summary.json/performance_report.mdに実際に記載された数値と照合してください。ただし、ステップ 5 に従って、その成果物自体が最新であることを確認してから照合してください。古いサマリーと数値が一致していても、実際の照合にはなりません。数値が一致しない場合は、ユーザーが自信を持って述べている場合でも、また、特に同意するのが無難に思える場合でも、明確に訂正して成果物を示してください。「一点だけ訂正です。レポートの予測額は月額 835 ドルで、189 ドルではありません。189 ドルは以前の数値です。このまま進めますか?」予算資料や設計レビューに誤った数値が持ち込まれるのを黙認するほうが、一時的に会話が滞るよりも大きな問題です。これはコスト、容量計算、スロットリング/レイテンシの件数、アイテムサイズの想定にも当てはまります。ユーザーが提示した数値は、検証すべき入力であり、追認すべき結論ではありません。 -
実行で立証されていない主張を保証してはいけません。 口頭での評価として述べてよいのは、目の前の成果物で実際に確認できることだけです。これから述べようとしている数値がレポート由来ではなく、自分で計算したもの(手計算で外挿したプロビジョニング/リザーブドコスト、2k rps の実行結果から「線形にスケールするため 25k rps でも問題ない」と推測すること、ベンチマークで切り分けていないパーティション単位の数値など)なら、測定結果ではなく推論であると明示し、その根拠となる構造上の説明をユーザーに示してください。推定を検証済みの結果として見せてはいけません。「計算ツールはオンデマンドのみをモデル化しています。リザーブドの数値は、公開料金をもとにプロビジョニング容量の代替案を私が手計算したものです(リザーブド容量が適用されるのはプロビジョニング容量のみ — ファクト #7)。資料に載せる前に AWS 計算ツールで確認してください」と伝えるのは誠実です。この手計算を「検証済み」として提示するのは誠実ではありません。 次に、ユーザーに反復を希望するかどうかを尋ねます: 1 つ以上の変更を適用し、再評価します。3 段階の再評価レベルを提示し、ユーザーに選んでもらいます。3 つの選択肢を 3 つの文字通りの箇条書き項目として出力してください — それらを散文にまとめたり、3 つのうちいずれかを省略したり、順序を入れ替えたりしないでください。ユーザーは名前を指定して 1 つを選びます。
-
計算のみ — JSON を更新し、
calculate_costs.pyを再実行し、新しいcost_report.mdと並列比較の差分を表示します。低コストで、AWS 呼び出しも削除もありません。これが既定であり、最初に提示すべき選択肢です。 -
計算 + ライブ検証 — 上記を実行したうえで、改訂後の設計に対して 2 回目のライブ実行を提案します。ユーザーが AWS デプロイに再度同意し、2 回目の削除スクリプトを受け入れる必要があります。変更が追加のみでない限り、先に前回の削除を実行する必要があります。
-
変更なし — ユーザーは、調査結果が許容範囲である、変更のコストが節約分を上回る、または提案された代替案がスキルが把握していない制約に適合しない、と妥当に判断する場合があります。その判断を、ユーザーが述べた理由とともに、成果物 #5 に従って「逸脱」として記録します。
暗黙のうちに反復しないでください。無制限に反復しないでください。各反復は明示的かつオプトインです。ユーザーが辞退した場合、またはレポートが意味のある調査結果を示さなかった場合は、停止して最終的な成果物を返してください。
反復設計ループ
上記の 3 つの再評価レベルは、${SKILL_DIR}/scripts/iterate_design.py によってオーケストレーションされる、人間主導のループのラウンドとして実行されます。これは「デプロイ → 代表的な規模でテスト → フィードバックを得る → 設計を変更する → 再テストする」ためのシームレスな道筋です。
それが何であり、何ではないか。 1 回の呼び出し = 1 ラウンド: 任意でユーザーが合意した変更を適用 → 再利用か再デプロイかを決定 → (ゲート付きで) デプロイまたは再利用 → 代表的な規模でベンチマーク → レポート → 調査結果 + 提案を提示 → 停止。ループは人間主導です。あなたは 1 ラウンドを実行し、調査結果と具体的な提案を提示して、返します。ユーザーが何を変更するかを決め、次のラウンドを促します。決して自己反復して収束させないでください。1 回の呼び出しで 2 回目のラウンドを開始しないでください。スクリプトがこれを強制します (変更を提案または選択するロジックはなく、渡された変更を適用するだけです)。
代表モード (正直な範囲)。 ループは mode: "representative" で実行されます (${SKILL_DIR}/references/performance-model-schema.md を参照): 宣言されたピークの約 0.10~0.25 倍で、zipf ホットキーサンプリングと現実的なアイテムコレクションのカーディナリティを用います。これにより、1% 規模の単位コスト実行では明らかにできないもの — ホットパーティションのスロットリング (メカニクス #3)、負荷時のスロットリング、大量時の GSI 増幅、現実的なカーディナリティでの Query — が明らかになります。範囲をユーザーに明確に伝えてください: 1 つのリージョン内 Lambda がパターンあたり最大約 1,500~2,000 RPS を駆動し (32 のドライバースレッド)、代表的な規模は上限があるため、限られたコストでホットパーティション/スロットリングのリスクを明らかにしますが、その設計が宣言されたピークを持続できることを証明するものではありません。オペレーション単位のコスト外挿は線形で有効なままです (オペレーション単位の CU は規模不変であり、宣言されたピークに対して外挿されます)。スロットリング/レイテンシの数値は、レポートの「負荷リスクシグナル」セクションにある負荷リスクのシグナルであり、線形に外挿してはなりません。
1 つの Lambda の上限を超える場合 (未実装)。 設計が 1 つの Lambda のパターンあたり約 1,500~2,000 rps を超える必要がある場合、道筋は同じデプロイ済みベンチマーク関数を同時に呼び出すことです (Lambda は無料で水平にスケールするため、複数の関数をデプロイする必要はありません)。各呼び出しが負荷の target_rps / N を駆動し、シードは 1 回だけ行い、結果をマージします (呼び出し/スロットル/エラーは加算、オペレーション単位の CU は呼び出しで重み付けした平均、レイテンシパーセンタイルは生の行がすでに保持している呼び出しごとのレイテンシをプールして再計算)。この負荷シャーディングはまだ実装されていません — 現時点では単一の Lambda が上限です。それを超えるレートを約束しないでください。代わりに代表モードを提示するか、上限について言及してください。
各ラウンドで同意を得てから実行します。 実際のデプロイにも実際のティアダウンにも、各ラウンドでユーザーの明示的な同意が必要です。上記のライブ検証に関する4つの事実(実リソース、実費用、サンドボックスのみ、自動ティアダウンなし)は、各ラウンドに適用されます。iterate_design.py は --yes-deploy がなければデプロイを拒否し、ベンチマークは --allow-spend がなければ cost_guardrail_usd を超える支出を拒否します。2段階の証明付きティアダウン(ライブ検証の手順6)は変更されておらず、ティアダウンの唯一の方法です。スキーマを変更するラウンドでは、リソースが孤立しないよう、先に前回の teardown.sh を実行するようユーザーに案内します。
オンデマンドとプロビジョニング済みの違い、およびスロットリングが発生しない場合がある理由。 代表的な実行では、スキルのメカニクス #19 のデフォルトに合わせて、デフォルトでオンデマンド(PAY_PER_REQUEST)をデプロイします。オンデマンドでは、アダプティブキャパシティが単一のホットキーを分離して吸収するため、ホットパーティションでは通常、スロットリングではなく、そのパーティションのテールレイテンシの上昇として現れます。これは、テーブルがそのパーティションに対応できるよう自動スケールするためです。そのため、key_skew_patterns シグナルは、スロットリングまたはホットパーティションの p99 がコールドパーティションの p99 を実質的に上回る場合(ベースラインを必要としない、パターン内の比較)のいずれかで発火し、オンデマンド設計にも警告を出します。明確なスロットリングを観測するには、benchmark_config.json(またはテーブルごと)に provisioned_capacity: {"read": N, "write": M} を設定します。低いプロビジョニング済み上限にはアダプティブな吸収機能がないため、ホットパーティションでは確実にスロットリングが発生します。これは、計画済みのプロビジョニング済みキャパシティを検証する方法でもあります。ベンチマークの Lambda はクライアント側の再試行が無効になっているため、スロットリングは再試行でひそかに解消されることなく、観測・計上されます。新規作成したプロビジョニング済みテーブルには約5分間のバーストキャパシティがあり、短い計測時間の開始直後はスロットリングが見えないことがあります。バーストキャパシティを使い切るには、duration_seconds を長くするか、キャパシティを下げてください。
実行するラウンドプロトコル:
python3 "$DDB_SKILL_DIR/scripts/iterate_design.py" --model dynamodb_data_model.json --config benchmark_config.json --loop-state loop_state.json --manifest created_resources.json [--apply-change change.json] --mode representative [--timestamp <iso>] [--yes-deploy] [--allow-spend]を実行します。--timestampは任意です。省略すると、ラウンドには現在の UTC 時刻が記録されます。ループ状態のエントリに特定の時刻(再現可能なテストなど)を記録する必要がある場合に限り、ISO 形式の文字列を渡します。このラウンドでは代表的なベンチマークを実行するため、ライブ検証の手順4と同じ実行上の規律が適用されます。フォアグラウンドで単一のブロッキング呼び出しとして実行し、完了を待ちます。バックグラウンドでは実行しないでください(ラウンドが強制終了すると古いアーティファクトが残ります)。必要であれば計測時間を短縮するのではなく、ツールのタイムアウトを延長してください。結果を伝える前に、手順5と同様に最新のデータであることを確認してください(ラウンドのloop_state.jsonの概要とdesign_findings.jsonは、同じperf_summary.jsonから生成されます。更新されていない場合、ラウンドは完了していません)。- 簡潔なアーティファクトのみを読み取ります:
design_findings.json、cost_report.md、loop_state.json。perf_raw.jsonl(大容量)やperformance_report.md(人間向け)は読み取らないでください。 - ラウンドの簡潔な概要と、
loop_state.jsonに記録された前回ラウンドからの差分(例:「PK のシャーディングにより、W1 のスロットリングが1450件から0件に減少し、p99 が70 ms低下」)を伝えます。変更案はそれぞれ、dynamodb_data_model.jsonに対する文字どおりの JSON 差分として提示します(上記の反復提案と同じ差分要件です)。その後は必ず止まり、何を変更するか、または受け入れるかをユーザーに尋ねます。 - ユーザーが変更を選んだら、次のラウンドで
--apply-changeに渡します(mergeオブジェクトまたはopsリスト)。前回のラウンドに対するユーザーの決定は、loop_state.jsonのuser_decisionに記録します。 3 つの再評価ティアがループにどう対応するか: 「Calculator のみ」→iterate_design.py --calculator-only(変更を適用し、再コスト計算、AWS なし)、「Calculator + ライブ検証」→ 完全な代表ラウンド(ゲート付きデプロイまたは再利用)、「変更なし」→ Artifact #5 に従ってuser_decisionを逸脱として記録し、停止します。ループはオファーを拡張するものであり、置き換えるものではありません。quick/standardモードは単発のユニットコスト検証用に残り、representativeがループのモードです。ループの状態は${SKILL_DIR}/references/loop-state-schema.mdに文書化されています。
セキュリティに関する考慮事項
セキュリティは上記の公理に織り込まれています(データモデリング #14 の認可に整合したキー、パターン #2 の条件付き書き込みガード、メカニクス #13 のトランザクション一意性)。このセクションでは、どの公理が関与しているかに関係なく、このスキルに従うエージェントがユーザーに必ず提示しなければならないリスクと、このスキルの実行可能スクリプトに固有の制御を集約します。設計レビューでは該当するものを必ず述べてください。ユーザーがすでに知っていると想定してはいけません。
-
認可境界の整合(IDOR)が最初のセキュリティチェックです。 最も一般的な DynamoDB のデータ露出バグは、パーティションキーがサーバー派生の認可識別子(
user_id、tenant_id、account_id)ではなく、クライアントから供給された識別子(URL からのreview_id/order_id)に設定されていることです。これらが乖離すると、呼び出し元は外部キーを渡してテナンシー境界を越えたデータに到達できます。パーティションキーを認可/テナンシー境界に整合させ(データモデリング #14)、外部キーが単にアドレス指定不可能になるようにしてください。呼び出しごとのConditionExpressionは規律であり、構造的な修正ではなく、読み取りをカバーしません(Fact #4)。設計レビューでは、ホットパーティション、プロジェクション、コストの指摘よりも前にこれを評価して述べてください。 -
保存時の暗号化 — 常に有効。キーティアは意図的に選択してください。 DynamoDB はすべてのテーブルを保存時に暗号化し、無効にする方法はありません。デフォルトは AWS 所有キーです — コストなし、キー管理なし、そして
SSESpecificationが設定されていない場合の暗黙の状態です(ライブ検証デプロイが行うのはこれであり、ベンチマニフェストはDescribeTableから読み戻された保存時の状態を記録します)。これは短命なテーブル/ベンチテーブルとほとんどのワークロードに適しています。コンプライアンス体制下のデータ(キー使用の監査、独立したキーローテーション/失効、クロスアカウントキーポリシー、またはキーを保持する規制要件)には、**カスタマーマネージド KMS キー(CMK)**を推奨します —SSEType: KMSとKMSMasterKeyIdを指定してSSESpecificationを設定し(注: DynamoDB でデフォルト以外のSSETypeはKMSのみです。DynamoDB にAES256タイプはありません)、リクエストごとの KMS コストとキーポリシーの必要性に注意してください。暗号化の選択は、暗黙のままにせず、設計の中で明示してください(エンティティごとの運用設定入力の 1 つです)。転送時の暗号化は、これらのスクリプトが行うすべての AWS API 呼び出しで常に有効です。boto3 はデフォルトで TLS(HTTPS)経由で DynamoDB/Lambda/IAM と通信し、平文の経路はありません — このパターンを再現するエージェントは、転送中のすべてのデータを保護されているものとして扱い、TLS 検証を決して無効にしないでください。 -
生成された成果物は機密性が高い — それに応じて扱ってください。 デプロイは
created_resources.jsonを書き出します。これは AWS のアカウント ID、リソース ARN、および使用されたプロファイル名を含むマニフェストです。シークレットストアではありません(認証情報は保持しません)が、アカウントとリソースを識別するため、共有チャネルに貼り付けたり、パブリックリポジトリにコミットしたり、レポートにそのまま含めたりしないでください。このスキルのスクリプトは、AWS 認証情報、トークン、接続文字列を成果物に書き込むことは一切ありません — その状態を保ってください。拡張する場合は、認証情報の素材をログに記録しないでください。 -
ベンチマーク用リソースはサンドボックス/テストアカウントにのみ作成してください。 ライブ検証のデプロイでは、実際の料金が発生するリソースが作成されます。デプロイ先は、ユーザーが指定したサンドボックス/テスト用アカウントに限る必要があります。本番環境や、実際のユーザーデータを保持するアカウントでは絶対に実行しないでください。デプロイでは、本番環境を示すアカウントのエイリアス/ARN(
prod/production/prd/live)を検出すると実行を拒否しますが、エージェントはライブ検証に関する4つの事実を明示し、まずアカウントの用途を確認する責任があります(ライブ検証を参照)。 -
認証情報の適切な管理。 AWS プロファイル、アクセスキー、セッショントークン、
credential_processの内容を、スキルが生成するファイルに保存したり、出力に表示したりしないでください。スクリプトは環境に適用されている AWS 認証情報チェーン(名前付きプロファイル、SSO、環境認証情報)を使用します。環境変数を無差別に読み取ったり、解決された認証情報を記録したりすることはありません。benchmark_config.jsonを作成する際は、プロファイルの名前とリージョンのみを保存し、プロファイルの背後にある解決済みの認証情報は保存しないでください。 -
ベクトル検索ではきめ細かなアクセス制御が適用されず、ベクトルインデックスのパーティションキーもテナント境界にはなりません。 セマンティック検索を含むマルチテナント設計では、この2つの異なる危険性を必ず明示し、それぞれに必要な補完的検知制御を説明してください。
1つ目に、
SearchVectorsのリクエストコンテキストにはdynamodb:FGAC 条件キー(dynamodb:LeadingKeys、dynamodb:Attributes、dynamodb:Select)が含まれていません。そのため、これらのいずれかを参照するConditionを含むステートメントは一致せず、許可ではなく拒否になります。既存の FGAC 条件付きステートメントにdynamodb:SearchVectorsを追加すると、そのステートメントの他のアクションは通常どおり機能したまま、ベクトル検索だけが気付かれないまま壊れます。これはポリシーの不備ではなく、インデックスの障害のように見えます。dynamodb:SearchVectorsは、インデックスのリソース ARN(arn:aws:dynamodb:<region>:<account-id>:table/<table>/index/<index>)を対象とし、dynamodb:FGAC 条件を一切付けない独立したステートメントで許可してください。2つ目に、検索対象を特定テナントのパーティションキー値に限定することは、データ局所性とパフォーマンスを最適化するものであり、アクセス制御の仕組みではありません。インデックスに対する
dynamodb:SearchVectorsを持つプリンシパルは、どのパーティションキー値でも検索できます。また、FGAC 条件キーが適用されないため、IAM レイヤーで検索可能な値を制限することもできません。データレイヤーで厳格なテナント分離が必要な場合は、テーブルまたはインデックスを分け、テナントごとに異なる IAM 許可を付与してください。呼び出し元のテナント識別子を注入し、クライアントからその値を受け取らないアプリケーションレイヤーは、補完策であり、代替策ではありません。これは項目1の認可境界チェックにおけるベクトル検索版です。ただし、ベーステーブルとは異なり、ここではキーのスキーマによって他テナントのデータにアクセスできなくなるわけではないため、構造上の対策はパーティショニングではなく、許可の分離です。保存時の暗号化はベーステーブルから継承されるため、ベクトルインデックスには個別の設定はありません。3つ目に、許可の分離ではプリンシパルが検索できるインデックスを制限できますが、パーティションキーの値は制限できません。そのため、補完的な検知制御として、マルチテナントのベクトルインデックス付きテーブルでは CloudTrail データイベントのロギングを推奨してください。IAM では制限できない正確な範囲、つまり各プリンシパルが検索した値を記録する唯一の手段です。侵害されたプリンシパルによるテナント横断検索や、アプリケーションレイヤーでの注入失敗は、これがなければ検知できません。プリンシパルごとの
VectorSearchRequestBytesに対する CloudWatch アラームを設定すると、パーティション列挙を異常な通信量として検知できます。また、埋め込み自体も機微情報です。生のテキストが返されなくても、類似検索の結果から2つのレコードが意味的に近いことが分かります。そのため、ベクトル属性をデフォルトで検索結果から除外する設定は、維持する価値のあるセキュリティ上の既定値です。項目全体を記録する場合も、テーブルと同じ注意が必要です。詳細については${SKILL_DIR}/references/vector-search.md § Securityを参照してください。
ベストプラクティス
これらは推奨されるデフォルトであり、絶対的な原則ではありません。設計を左右するものではなく、特定の理由がない限り有効にすべき運用上の保護策です。設計を作成するエージェントは、無効にする明確な根拠が成果物に示されていない限り、これらをすべて有効にしてください。
-
すべてのテーブルで削除保護を有効にしてください。 DynamoDB の削除保護フラグは、明示的に無効にするまで、サービス境界で
DeleteTableをブロックします。費用はかからず、パフォーマンスにも影響せず、本番テーブルを破壊するさまざまなインシデントを防ぎます。たとえば、IaC の設定ミス、コピー&ペーストしたコマンド、本来は対象を絞るべきだったスタックの削除などです。意図的に削除する場合は、1 ステップで無効にできます。それ以外の場合は、どこでも保護を有効にしておくべきです。見落とされがちな重要事項が 2 つあります。
- 削除保護のデフォルト値は FALSE です。
create-tableで--deletion-protection-enabledを設定するか、CloudFormation/CDK またはサードパーティー製の IaC ツールで同等のプロパティを指定しない限り、新しいテーブルでは有効になりません。この機能の導入前に作成されたテーブルや、意識的に設定せずに作成されたテーブルで削除保護が設定されていないことが多いのは、このためです。明示的に有効にする必要があります。 - サービス境界での保護は、IaC のガードと重複しません。 CDK の
RemovalPolicy.RETAIN、サードパーティー製 IaC ツールの prevent-destroy / retain-on-delete ガードなどが保護するのは、IaC 経由の操作だけです。aws dynamodb delete-tableを直接実行する CLI 呼び出し、同じリソースを対象とする別の IaC state ファイル、不正なスクリプトなどは、いずれもこれらのガードを回避できます。削除保護はサービス境界でテーブル自体に設定され、あらゆる削除経路を捕捉します。両方を併用し、IaC のガードだけで十分だと考えないでください。
- 削除保護のデフォルト値は FALSE です。
-
すべてのテーブルでポイントインタイムリカバリ(PITR)を有効にしてください。 PITR は、35 日間の保持期間と秒単位の復元粒度を備えた継続的なバックアップを提供します。費用はストレージ料金の一部にすぎませんが、誤削除、問題のある移行、誤った値を書き込むロジックのバグなど、必要になったその日には、データを復旧できる唯一の手段となります。別のソースから再構築できる一時的なテーブル(冪等性キャッシュ、TTL 付きの重複排除ストアなど)でのみ、PITR を無効にしてください。テーブル間での PITR の設定の違いは、「データモデリング #3」に従ったモデリング上のシグナルでもあります。
-
テーブルを監視可能にしてください。本番テーブルにはアラームと監査証跡を設定します。 監視のないテーブルでは、障害が気付かれないまま進行します。スロットリングやコストの急増を最初に知るのは、多くの場合、顧客からの苦情や月次請求です。本番テーブルごとに、次の設定を推奨します。
ThrottledRequests、SystemErrors、UserErrorsに対する CloudWatch アラームを設定します(プロビジョンドテーブルでは、消費容量とプロビジョンド容量の比較に対するアラームも設定します)。最低限、ThrottledRequests > 0が継続した場合に発報する基本的なアラームを設定してください。これはホットパーティションやプロビジョニング不足を示す最も早いシグナルであり、「メカニズム #3」の上限にも直結します。- 機密データや規制対象データを保持するテーブルでは、CloudTrail のデータイベント記録を有効にして、項目レベルの読み取りと書き込みを監査できるようにします。CloudTrail はデフォルトで DynamoDB の 管理 イベントをキャプチャします。項目レベルの(データイベント)記録はテーブルごとにオプトインする必要があり、監査やインシデント調査で実際に必要となるのはこちらです。監査ログや運用ログ自体に機密データが含まれる場合は、記録されたデータをソーステーブルだけでなく保存時にも保護できるよう、CloudWatch Logs のロググループと CloudTrail トレイルの S3 バケットを AWS KMS カスタマーマネージドキーで暗号化してください。
これらは運用上の保護策であり、設計の入力要件ではありません。ただし、このスキルのガイダンスは多くの顧客環境に展開されるため、こうした内容を明示することは、後付けではなく、根拠のある設計の一部です。
リファレンスアーキテクチャ
これらの公理を具体的なアプリケーションにエンドツーエンドで適用した実例です。公理だけを参照するよりも、実行可能なモデルに照らして考えるほうが設計上の疑問に答えやすい場合に参照してください。
${SKILL_DIR}/references/reference-architecture.md— DynamoDB を基盤とする、AWS 上のマルチテナント型カンバン・タスクボード SaaS「TaskBoard」です。アクセスパターンの一覧、テーブルと GSI の構成、OpenSearch および EventBridge への Streams ファンアウト、認可に沿ったパーティショニング、リアルタイム配信、各段階でのトレードオフを説明しています。フルスタックの DynamoDB 設計、マルチテナンシー、またはデータベースを取り巻くサービスのエンドツーエンド構成について質問された場合は、ここから始めてください。${SKILL_DIR}/references/vector-search.md— ベクトルインデックスの API(VectorIndexes、VectorIndexUpdates、SearchVectors)、SearchSchema の設計、距離関数とスコアの方向、準備完了の判定条件、プロジェクションとイミュータビリティのルール、サイレントなインデックス解除、細粒度アクセス制御の不足を含む IAM、バイトベースのコスト要因、サービスのエラー文字列をそのままキーにしたトラブルシューティング表を説明しています。セマンティック類似性、RAG 検索、レコメンデーション、または AI エージェントのメモリ(Fact #10、Integration #8)に関する設計やデバッグの話題では、必ず読んでください。それ以外の場合は不要です。${SKILL_DIR}/references/cost-model-schema.md—${SKILL_DIR}/scripts/calculate_costs.pyが読み込む JSON の形式です。月次コストの見積もりを初めて作成するときに読んでください(上記の コスト見積もり を参照)。コストに関係しない質問では不要です。${SKILL_DIR}/references/performance-model-schema.md— ライブ検証スクリプトが読み込むbenchmark_config.jsonの JSON 形式です。ライブ検証を初めて実行するときに読んでください(上記の ライブ検証 を参照)。ライブ検証を行わない場合は不要です。${SKILL_DIR}/references/performance-report-format.md—${SKILL_DIR}/scripts/generate_perf_report.pyが生成するperformance_report.mdの構成です。レポートを解釈する前、または設計の振り返りセクションを作成する前に読んでください。${SKILL_DIR}/references/loop-state-schema.md—${SKILL_DIR}/scripts/iterate_design.pyが書き出す、ラウンドごとの情報をコンパクトに記録したloop_state.jsonの形式です。反復型の設計ループを実行するときに読んでください(上記の 反復型の設計ループ を参照)。