AI時代を見据えたAWSアーキテクチャ理解の向上3
AI時代を見据えたAWSアーキテクチャ理解の向上 その3
前回の記事では、AWSのサーバーレス構成として、以下の3つのサービスを整理しました。
- API Gateway
- Lambda
- DynamoDB
役割を簡単に表すと、API GatewayがAPIの入口、Lambdaが処理を実行する場所、DynamoDBがデータを保存する場所です。
今回は、もう一歩具体的に進めます。
簡単なメモ管理APIを作ると仮定して、API Gateway、Lambda、DynamoDBをどのように設計するのかを整理していきます。
AWSの設定画面を順番に操作する手順ではなく、実際に作り始める前に決めておきたい内容が中心です。
AIに実装を依頼する場合でも、この設計部分が曖昧なままだと、出力されるコードやAWS構成も曖昧になります。
まずは、小さなCRUD APIを題材にして、必要な判断を一つずつ確認していきます。
今回作るもの
今回考えるのは、ユーザーが自分のメモを登録、閲覧、更新、削除できるAPIです。
必要な操作は、以下の5つとします。
| 操作 | 内容 |
|---|---|
| 一覧取得 | ユーザーが登録したメモの一覧を取得する |
| 詳細取得 | 指定したメモを1件取得する |
| 登録 | 新しいメモを登録する |
| 更新 | 登録済みのメモを更新する |
| 削除 | 登録済みのメモを削除する |
全体構成は、前回と同じです。
クライアント
↓
API Gateway
↓
Lambda
↓
DynamoDB
たとえば、メモを登録する場合は、以下のような流れになります。
クライアント
↓ POST /memos
API Gateway
↓ Lambdaを呼び出す
Lambda
↓ メモを保存する
DynamoDB
構成だけを見ると単純ですが、実際に作るためには、次のようなことを決める必要があります。
- どのようなAPIを公開するのか
- DynamoDBにどのような形式で保存するのか
- Lambdaを処理ごとに分けるのか
- どのようなエラーを返すのか
- Lambdaにどの権限を与えるのか
- エラーが起きたときに何をログへ残すのか
今回は、この順番で考えていきます。
APIを設計する
最初にAPI Gatewayへ設定するルートを考えます。
今回は、以下のようなAPIにします。
| 操作 | HTTPメソッド | パス |
|---|---|---|
| メモ一覧取得 | GET | /memos |
| メモ詳細取得 | GET | /memos/{memoId} |
| メモ登録 | POST | /memos |
| メモ更新 | PUT | /memos/{memoId} |
| メモ削除 | DELETE | /memos/{memoId} |
API Gatewayでは、HTTPメソッドとパスの組み合わせをルートとして定義します。
たとえば、同じ/memosでも、GETとPOSTでは実行する処理が異なります。
GET /memos
→ メモの一覧を取得する
POST /memos
→ 新しいメモを登録する
詳細、更新、削除では、URLの一部にメモIDを含めます。
GET /memos/{memoId}
PUT /memos/{memoId}
DELETE /memos/{memoId}
実際のURLは、以下のようになります。
GET /memos/550e8400-e29b-41d4-a716-446655440000
この場合、API GatewayからLambdaへ渡されるイベントには、memoIdがパスパラメータとして含まれます。
Lambdaは、その値を使ってDynamoDBから対象のメモを取得します。
リクエストとレスポンスを決める
APIのパスだけでなく、どのようなデータを受け取り、どのようなデータを返すのかも決めておきます。
たとえば、メモを登録するAPIでは、以下のようなJSONを受け取ります。
{
"title": "AWSの学習",
"content": "LambdaとDynamoDBの連携を確認する"
}
登録に成功した場合は、作成したメモを返します。
{
"memoId": "550e8400-e29b-41d4-a716-446655440000",
"title": "AWSの学習",
"content": "LambdaとDynamoDBの連携を確認する",
"createdAt": "2026-07-27T12:00:00Z",
"updatedAt": "2026-07-27T12:00:00Z"
}
この段階で、最低限以下の項目を決めています。
- メモを識別するID
- タイトル
- 本文
- 作成日時
- 更新日時
認証機能も含める場合は、ユーザーを識別するuserIdも必要になります。
{
"userId": "user-123",
"memoId": "550e8400-e29b-41d4-a716-446655440000",
"title": "AWSの学習",
"content": "LambdaとDynamoDBの連携を確認する",
"createdAt": "2026-07-27T12:00:00Z",
"updatedAt": "2026-07-27T12:00:00Z"
}
ここで重要なのは、コードを書く前に、APIが扱うデータの形を決めていることです。
リクエストとレスポンスが決まっていれば、AIにコードを生成してもらう場合も具体的に指示できます。
反対に、この部分が決まっていない状態で「メモAPIを作って」と依頼すると、項目名、IDの生成方法、日時の形式、エラー時の形式などをAI側の判断に任せることになります。
DynamoDBの保存形式を考える
次に、メモをDynamoDBへどのように保存するのかを考えます。
最初に思いつきやすいのは、memoIdをパーティションキーにする構成です。
| 属性 | 値の例 |
|---|---|
| memoId | 550e8400-e29b-41d4-a716-446655440000 |
| userId | user-123 |
| title | AWSの学習 |
| content | LambdaとDynamoDBの連携を確認する |
| createdAt | 2026-07-27T12:00:00Z |
| updatedAt | 2026-07-27T12:00:00Z |
この構成であれば、memoIdを指定して1件のメモを取得できます。
memoIdを指定
↓
該当するメモを取得
しかし、今回のAPIには「ユーザーごとのメモ一覧を取得する」という操作があります。
memoIdしかキーに含まれていない場合、特定ユーザーのメモだけを効率的に取得する方法を別途考えなければなりません。
DynamoDBでは、最初にテーブルを作り、その後で必要になった検索方法を追加するというより、アプリケーションが必要とするアクセスパターンからキーを考えることが重要です。
今回のアクセスパターンを整理すると、次のようになります。
- ユーザーIDを指定してメモ一覧を取得する
- ユーザーIDとメモIDを指定して1件取得する
- ユーザーのメモを登録する
- ユーザーのメモを更新する
- ユーザーのメモを削除する
この場合は、以下のようなキー構成が考えられます。
パーティションキー:userId
ソートキー:memoId
保存されるデータのイメージは、以下のようになります。
| userId | memoId | title |
|---|---|---|
| user-123 | memo-001 | AWSの学習 |
| user-123 | memo-002 | 買い物リスト |
| user-456 | memo-003 | 作業メモ |
userIdをパーティションキーにすることで、特定ユーザーのメモをまとめて取得できます。
userId = user-123
↓
memo-001
memo-002
また、userIdとmemoIdの両方を指定すれば、特定ユーザーが所有するメモを1件取得できます。
この設計には、単にデータを取得できるということ以外にも意味があります。
ユーザーIDを条件に含めるため、別のユーザーのメモをIDだけで取得してしまう事故を防ぎやすくなります。
もちろん、キー設計だけで完全な認可になるわけではありません。
Lambda側でも、認証情報から取得したユーザーIDを使用し、リクエストで自由に指定されたユーザーIDを信用しない設計が必要です。
一覧取得ではScanを避けたい
DynamoDBには、テーブル内の項目を広く読み取るScanという操作があります。
Scanを使えば、テーブル内のデータを取得した後、条件に一致する項目を絞り込むことはできます。
しかし、ユーザーごとのメモ一覧を取得するたびにテーブル全体を確認する構成は、データが増えたときに効率が悪くなります。
今回の設計では、userIdをパーティションキーとしてQueryを実行します。
Query
userId = user-123
これにより、指定したユーザーIDを持つ項目を取得できます。
RDBに慣れていると、「とりあえず項目を保存して、必要になったらWHERE句で検索する」という感覚になりやすいと思います。
DynamoDBでは、必要な取得方法を先に整理し、その取得方法に合わせてキーやインデックスを設計します。
ここは、DynamoDBを使う上で特に意識したい部分です。
Lambdaをどの単位で分けるか
次に、Lambdaをどの単位で作るのかを考えます。
大きく分けると、次の2つの方法があります。
1つのLambdaにまとめる
1つ目は、すべてのルートを1つのLambdaで処理する方法です。
GET /memos
GET /memos/{memoId}
POST /memos
PUT /memos/{memoId}
DELETE /memos/{memoId}
↓
memoFunction
Lambdaの中で、HTTPメソッドやパスを確認して処理を分岐します。
GET /memosなら一覧取得
POST /memosなら登録
DELETE /memos/{memoId}なら削除
小さな学習用APIであれば、全体の流れを確認しやすい構成です。
一方で、機能が増えると条件分岐が多くなり、1つのLambdaが大きくなる可能性があります。
処理ごとにLambdaを分ける
2つ目は、ルートごとにLambdaを分ける方法です。
GET /memos
↓
listMemosFunction
GET /memos/{memoId}
↓
getMemoFunction
POST /memos
↓
createMemoFunction
PUT /memos/{memoId}
↓
updateMemoFunction
DELETE /memos/{memoId}
↓
deleteMemoFunction
それぞれのLambdaが担当する処理が明確になります。
また、Lambdaごとに必要なIAM権限を分けやすくなります。
たとえば、一覧取得と詳細取得のLambdaには、DynamoDBの読み取り権限だけを付与できます。
登録処理には書き込み権限、削除処理には削除権限を付与します。
listMemosFunction
→ Queryのみ許可
getMemoFunction
→ GetItemのみ許可
createMemoFunction
→ PutItemのみ許可
updateMemoFunction
→ UpdateItemのみ許可
deleteMemoFunction
→ DeleteItemのみ許可
一方で、Lambdaの数が増えるため、設定やデプロイ対象も増えます。
今回のような学習目的であれば、最初は1つのLambdaで処理の流れを確認し、その後で処理ごとに分ける方法を試すのもよいと思います。
ただし、実際のサービスを想定するのであれば、個人的には最初から処理単位で分けた方が、それぞれの責務や権限を理解しやすいと感じます。
Lambdaの処理を整理する
メモを登録するLambdaを例にすると、必要な処理は以下のようになります。
1. API Gatewayからイベントを受け取る
2. 認証済みユーザーのIDを取得する
3. リクエストボディをJSONへ変換する
4. 入力値をチェックする
5. メモIDを生成する
6. 作成日時と更新日時を設定する
7. DynamoDBへ保存する
8. API Gatewayへレスポンスを返す
JavaのWebアプリケーションで考えると、Controller、Service、Repositoryなどに分けていた処理が、Lambdaの中にすべて集まりそうに見えるかもしれません。
しかし、Lambdaを使う場合でも、コードの役割を分ける考え方は変わりません。
たとえば、以下のように分けられます。
Handler
→ API Gatewayのイベントを受け取る
Service
→ メモ登録のルールを処理する
Repository
→ DynamoDBへの読み書きを行う
Lambdaは実行単位であり、必ず1つのクラスや1つのファイルにすべての処理を書かなければならないわけではありません。
サーバーレスだから設計が不要になるのではなく、どこまでを1つの実行単位にするのかを考える必要があります。
入力チェックを考える
メモ登録APIでは、クライアントから送られてくるデータをそのままDynamoDBへ保存しないようにします。
最低限、以下のようなチェックが必要です。
- リクエストボディが存在するか
- JSONとして読み取れるか
- タイトルが空ではないか
- タイトルが長すぎないか
- 本文が長すぎないか
たとえば、タイトルが空の場合は、400 Bad Requestを返します。
{
"code": "INVALID_REQUEST",
"message": "title is required"
}
入力チェックを設計せずにAIへ実装を依頼すると、チェックが不足していたり、エラー時に常に500 Internal Server Errorを返したりする可能性があります。
正常な処理だけでなく、どのような入力を不正と判断するのかも事前に決めておく必要があります。
HTTPステータスコードを整理する
今回のAPIで使用する主なHTTPステータスコードを整理します。
| ステータス | 使用する場面 |
|---|---|
| 200 OK | 取得、更新、削除が成功した |
| 201 Created | メモの登録が成功した |
| 400 Bad Request | 入力内容に問題がある |
| 401 Unauthorized | 認証されていない |
| 403 Forbidden | 操作する権限がない |
| 404 Not Found | 指定されたメモが存在しない |
| 500 Internal Server Error | サーバー内部で予期しないエラーが発生した |
たとえば、存在しないメモIDを指定した場合は、以下のようなレスポンスを返します。
{
"code": "MEMO_NOT_FOUND",
"message": "Memo was not found"
}
APIを利用する側は、HTTPステータスコードやエラーコードを見て、その後の動作を判断します。
そのため、Lambdaごとに異なる形式のエラーを返すのではなく、API全体で形式をそろえた方が扱いやすくなります。
IAMでLambdaの権限を制御する
LambdaからDynamoDBへアクセスするためには、Lambdaの実行ロールに権限を付与します。
たとえば、メモを登録するLambdaには、DynamoDBのPutItemを許可します。
イメージとしては、以下のようなポリシーです。
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem"
],
"Resource": "メモテーブルのARN"
}
一覧取得であればQuery、詳細取得であればGetItem、更新であればUpdateItemが必要になります。
一覧取得
→ dynamodb:Query
詳細取得
→ dynamodb:GetItem
登録
→ dynamodb:PutItem
更新
→ dynamodb:UpdateItem
削除
→ dynamodb:DeleteItem
すべてのLambdaにDynamoDBの全操作を許可すれば、設定は簡単です。
しかし、一覧を取得するだけのLambdaに削除権限は必要ありません。
必要以上に広い権限を与えず、その処理に必要な操作と対象リソースだけを許可することが重要です。
ここでも、Lambdaを処理ごとに分ける設計がIAMの考え方とつながってきます。
Lambdaを分けることは、コードを整理するだけではなく、障害や不正操作が発生した場合の影響範囲を小さくすることにもつながります。
CloudWatch Logsに何を残すか
Lambdaで発生したエラーや処理状況は、CloudWatch Logsで確認します。
ただし、何も考えずに値を出力するだけでは、後から原因を調査しにくくなります。
たとえば、次のような情報をログへ出力します。
- Lambdaが実行された時刻
- リクエストを識別するID
- 実行した処理
- 対象のメモID
- 処理結果
- エラーの種類
- スタックトレース
ログのイメージは、以下のようになります。
{
"level": "INFO",
"operation": "CREATE_MEMO",
"requestId": "request-123",
"userId": "user-123",
"memoId": "memo-001",
"message": "Memo created"
}
エラーが発生した場合は、以下のように出力します。
{
"level": "ERROR",
"operation": "CREATE_MEMO",
"requestId": "request-123",
"errorCode": "DYNAMODB_WRITE_ERROR",
"message": "Failed to create memo"
}
ログへは、調査に必要な情報を残す必要があります。
一方で、パスワード、認証トークン、秘密情報、必要以上の個人情報などを出力してはいけません。
リクエストボディをそのままログへ出力する実装は簡単ですが、入力される情報の内容によっては情報漏えいにつながります。
「ログがあれば安心」ではなく、「何を残し、何を残さないか」も設計する必要があります。
作成する順番を考える
ここまでの内容を踏まえると、今回のAPIは以下の順番で作成できそうです。
1. アクセスパターンを整理する
最初に、アプリケーションが必要とする操作を決めます。
ユーザーのメモ一覧を取得する
ユーザーのメモを1件取得する
メモを登録する
メモを更新する
メモを削除する
2. DynamoDBのキーを決める
アクセスパターンから、キーを決めます。
パーティションキー:userId
ソートキー:memoId
3. APIのルートを決める
API Gatewayへ設定するHTTPメソッドとパスを決めます。
GET /memos
GET /memos/{memoId}
POST /memos
PUT /memos/{memoId}
DELETE /memos/{memoId}
4. リクエストとレスポンスを決める
正常時とエラー時のJSON形式を決めます。
5. Lambdaの分割単位を決める
1つにまとめるのか、処理ごとに分けるのかを決めます。
6. IAM権限を決める
それぞれのLambdaが必要とするDynamoDB操作だけを許可します。
7. ログの形式を決める
CloudWatch Logsで処理を追跡できるように、共通のログ項目を決めます。
このように整理すると、AWSコンソールを開く前に、かなりの部分を設計できます。
AWSのサービスを理解するというと、設定画面の項目や操作方法を覚えることに意識が向きがちです。
しかし、設定画面を操作する前に、どのような構成を作るのかを決める必要があります。
AIへの依頼も具体的になる
ここまで設計できていれば、AIへの依頼内容も具体的になります。
たとえば、単に以下のように依頼する場合を考えます。
AWS LambdaでメモAPIを作ってください。
この依頼だけでは、AIが多くの部分を推測することになります。
一方、設計した内容を含めると、次のように依頼できます。
API GatewayのHTTP APIから呼び出す、
メモ登録用のAWS LambdaをJava 21で実装してください。
要件:
- POST /memosで呼び出す
- API GatewayのLambdaプロキシ統合を使用する
- 認証情報からuserIdを取得する
- リクエストはtitleとcontentを含むJSON
- titleは必須で100文字以内
- memoIdはUUIDで生成する
- createdAtとupdatedAtはISO 8601形式
- DynamoDBのパーティションキーはuserId
- ソートキーはmemoId
- PutItemを使用して保存する
- 正常時は201を返す
- 入力エラーは400を返す
- エラーレスポンスはcodeとmessageを持つ
- ログはJSON形式で出力する
このような依頼であれば、AIが勝手に判断する範囲を小さくできます。
また、出力されたコードについても、要件を満たしているか確認できます。
userIdをリクエストボディから受け取っていないか- タイトルの入力チェックがあるか
- DynamoDBのキーが設計どおりか
- 余計な権限を要求していないか
- エラー時のステータスコードが適切か
- ログに機密情報を出力していないか
AIにコードを作らせる場合、重要なのはプロンプトの書き方だけではありません。
その前提となる設計を、自分で説明できることが重要です。
最初から完璧な設計を目指さない
ここまで多くの項目を整理しましたが、最初から本番運用に耐えられる設計を完成させる必要はないと思います。
今回の目的は、API Gateway、Lambda、DynamoDBがどのようにつながるのかを理解することです。
最初は、以下のような小さい構成でもよさそうです。
- 認証なし
- 1つのLambda
- 1つのDynamoDBテーブル
- 5つのCRUDルート
- curlまたはPostmanから実行
- CloudWatch Logsで実行結果を確認
まずは、リクエストを送るとLambdaが動き、DynamoDBへデータが保存されるところまで確認します。
その後、段階的に以下を追加できます。
- Lambdaを処理ごとに分割する
- IAM権限を処理ごとに制限する
- 入力チェックを追加する
- エラーレスポンスを統一する
- Cognitoなどの認証を追加する
- CloudWatchでメトリクスやアラームを設定する
- SAMやCDKで構成をコード化する
小さく作り、動作を確認しながら改善する方が、それぞれのAWSサービスが必要な理由を理解しやすいと思います。
まとめ
今回は、メモ管理APIを題材にして、API Gateway、Lambda、DynamoDBを使ったCRUD APIの設計を整理しました。
設計した内容をまとめると、以下のようになります。
| 項目 | 設計内容 |
|---|---|
| API Gateway | CRUDに対応するHTTPメソッドとパスを定義する |
| Lambda | リクエストを処理し、DynamoDBを操作する |
| DynamoDB | userIdとmemoIdをキーとしてメモを保存する |
| IAM | Lambdaごとに必要な操作だけを許可する |
| CloudWatch Logs | 処理結果やエラーを調査できる情報を残す |
今回改めて感じたのは、API Gateway、Lambda、DynamoDBを個別に覚えるだけでは、アプリケーションの設計にはならないということです。
どのようなデータを、どのようなAPIで扱い、どのようなキーで保存するのか。
エラーが起きたときは何を返し、どこで原因を確認するのか。
それぞれの判断がつながって、初めて1つのアーキテクチャになります。
AIを使えば、LambdaのコードやIAMポリシー、DynamoDBを作成するテンプレートも生成できます。
しかし、その内容が自分のアプリケーションに合っているかを判断するためには、アクセスパターン、責務、権限、ログなどの理解が必要です。
AI時代に必要なのは、すべてのコードを暗記して書く力というより、作りたいものを構造化し、AIが生成した構成を確認できる力なのかもしれません。
次は、今回設計したメモ管理APIを題材にして、AWS SAMやAWS CDKなどを使ったInfrastructure as Codeについて整理したいと思います。
AWSコンソールから手作業で作る場合と、構成をコードとして管理する場合にどのような違いがあるのかを確認していきます。
参考
- Tutorial: Create a CRUD HTTP API with Lambda and DynamoDB - Amazon API Gateway
- Defining Lambda function permissions with an execution role - AWS Lambda
- Best practices for designing and architecting with DynamoDB - Amazon DynamoDB
- Querying tables in DynamoDB - Amazon DynamoDB
- Working with Lambda function logs - AWS Lambda