API開発(Java SDK)ハンズオン記録
1. はじめに
この記事はAIエージェントを利用して生成しています。
このハンズオンの目的は、Lambda(Java)で最小構成の受付APIを構築し、設計・IaC・テスト・運用まで一連で整えることです。
ハンズオンでは、次の構成にしました。
- API Gateway(REST API)
- Lambda(Java 21)
- DynamoDB(受付状態管理)
- S3(アップロード先、Presigned URL)
AWS公式ハンズオンでは、上記を満たすハンズオンが存在しなかったため、以下公式サンプルの強みを組み合わせました。
- 骨格:
aws-samples/aws-sam-java-rest - Presigned URL:
aws-samples/generate-s3-accelerate-presigned-url - 拡張観点の補助:
aws-samples/bobs-used-bookstore-serverless
2. 全体像の検討
ハンズオン構築にあたり
最初に「誰が誰と通信するか」を検討しました。
- 利用者が API Gateway の
POST /requestsを呼ぶ - Lambda が
requestIdを発行し、DynamoDB に受付情報を保存 - Lambda が S3 Presigned URL を返す
- 利用者が S3 に直接アップロード
-
GET /requests/{id}とGET /requestsで状態参照
全体構成図
3. 工程1: APIの最小構成を決める
初期版のAPIは3本に限定しました。
POST /requestsGET /requests/{id}GET /requests
工夫:
- 受付作成、単票取得、一覧取得だけに絞り、説明性を優先
- 認証・通知・高度な非同期は初期スコープから外して複雑化を回避
- 入口は
RequestApiHandlerに集約し、外部I/Oの窓口を明確化
4. 工程2: Lambda内部の責務を整理する
同期系と非同期系を分けて、責務を整理しました。
- 同期系(
RequestApiFunction):RequestApiHandler(ルーティング、DTO変換、例外レスポンス化) - 同期系(
RequestApiFunction):RequestService(業務処理、ID生成、状態決定、URL発行連携) - 同期系(
RequestApiFunction):RequestRepository(DynamoDBアクセス) - 同期系(
RequestApiFunction):RequestS3PresignService(Presigned URL生成) - 同期系(
RequestApiFunction):RequestValidator(入力妥当性、body/path) - 非同期系(
RequestStatusUpdateFunction):RequestStatusUpdateHandler(S3 Object CreatedイベントからrequestIdを抽出) - 非同期系(
RequestStatusUpdateFunction):RequestRepository(requestIdをキーにstatus=COMPLETEDへ更新)
工夫:
- Handlerを薄く保ち、入力検証は
RequestValidatorに委譲 -
Utilを安易な退避先にせず、責務が読める部品名を採用 - 1関数構成でも保守可能な内部設計を優先
5. 工程3: SAMテンプレートに落とし込む
目的:
GUIで理解した構成を、再現可能なIaCへ落とし込むことにしました。
画面で全体像を把握と設定値の確認、その後にtemplate.yamlへ戻して運用可能な形へ変換することにしました。
この節で確認すること:
-
sam deploy --guidedの入力値を再利用できること - GUIで作った構成がSAMで再現できること
template.yamlの責務(何を定義するか):
- API Gateway(REST,
Prod) - Lambda(Java 21,
RequestApiHandler) - S3 Object Created を受ける EventBridge ルール +
RequestStatusUpdateHandler - 既存 DynamoDB / 既存 S3 の参照
- API 3ルートのLambda統合
- Lambda環境変数
template.yaml と env.local.json の関係:
-
template.yaml: AWSにデプロイする本体構成を定義する -
env.local.json:sam local実行時だけ、接続先をLocalStackへ差し替える
重要前提:
-
template.yamlでは可変値(例:DDB_ENDPOINT,S3_ENDPOINT)を空文字で持つ - Java側
optional(...)がisBlank()をnullへ変換し、未指定扱いにする
設計判断(S3イベント連携):
- 最小要件だけなら S3 通知の Lambda 直結でも実現可能
- 本ハンズオンでは、将来の連携先追加や分岐に対応しやすい実務想定を重視して EventBridge 経由を採用
- SAM上で EventBridge ルールを定義し、イベント連携の責務を構成として固定した
APIの通信テスト
curl -i -X POST "<INVOKE_URL>/requests" \
-H "Content-Type: application/json" \
-d '{"userId":"u001","fileName":"a.txt"}'
API通信テストのレスポンス
{"requestId":"req-63371ecc-34d2-4157-bab7-4f3b03d657d2","userId":"u001","fileName":"a.txt","s3Key":"uploads/u001/req-63371ecc-34d2-4157-bab7-4f3b03d657d2/a.txt","status":"RECEIVED","uploadUrl":"https://handson-fileups3-","createdAt":"YYYY-mm-ddTHH:MM:SS.小数点以下","updatedAt":"YYYY-mm-ddTHH:MM:SS.小数点以下"}%
6-1. AWS実環境へのSAMデプロイ手順
目的:
SAMでAWS実環境に反映し、ApiBaseUrl でAPI疎通まで確認すること。
この節で確認すること:
-
sam deploy --guidedの設定が保存されること - 2回目以降は
sam deployだけで更新できること - デプロイ後にAPI 3ルートが利用できること
手順/設定:
- AWS CLIログイン
# IAMユーザー利用時
aws configure
# SSO利用時
aws configure sso
aws sso login --profile <profile-name>
参考:
私の過去の記事です。
わかりにくいですが、AWS CLIログインする作業の流れ自体はまとまっていると思います。
- 認証確認
aws sts get-caller-identity
- テンプレート検証
sam validate
- ビルド
sam build
- 初回デプロイ(対話形式)
sam deploy --guided
--guided の入力は次を使用します(環境固有値は作成済みリソース名を指定)。
- 固定値:
- Stack Name:
fileupapi-aws - AWS Region:
ap-northeast-1 - Confirm changes before deploy:
n - Allow SAM CLI IAM role creation:
y - Disable rollback:
n -
...has no authentication...:y - Save arguments to configuration file:
y
- Stack Name:
- 環境依存値:
- UploadBucketName: 作成済みS3バケット名
- RequestsTableName: 作成済みDynamoDBテーブル名
このハンズオンではap-northeast-1リージョンにしていますが、おそらく基本的にどこでも大丈夫です。
ただし、全体で統一するように注意してください。
sam deploy では、Lambdaアーティファクトを一時保管するための管理用S3バケットが自動生成される。
このバケットは、アプリ本体のアップロード先S3(UploadBucketName)とは用途が異なる。
補足. 著者の失敗例: 管理用S3を削除してしまった場合
著者は一度、SAM管理用S3バケットを不要と誤認して削除し、sam deploy で次のエラーになりました。
Error: Unable to upload artifact ... S3 Bucket does not exist.
この場合は、まず管理スタックの状態をAWS CLIで確認します。
aws cloudformation describe-stacks --stack-name aws-sam-cli-managed-default --region ap-northeast-1
管理スタックが壊れた状態で残っている場合は、管理スタックを削除して再作成させます。
aws cloudformation delete-stack --stack-name aws-sam-cli-managed-default --region ap-northeast-1
その後に再度デプロイを実行します。
sam deploy --guided
この手順で、SAM管理用S3バケットを含む管理リソースが再生成され、通常の sam deploy に復帰できます。
- 2回目以降のデプロイ
sam deploy
samconfig.toml に保存された設定を利用するため、--guided は不要です。
- SAMデプロイ完了時の出力例:
CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Key ApiBaseUrl
Description Base URL for the deployed API
Value https://自動生成箇所.XXXXXXXX.ap-northeast-1.amazonaws.com/Prod
Key RequestsTableName
Description DynamoDB table name
Value requests
Key UploadBucketName
Description S3 bucket name
Value 作成したS3バケット名(既存)
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Successfully created/updated stack - fileupapi-aws in ap-northeast-1
- デプロイ後の疎通確認
curl -i -X POST "<INVOKE_URL>/requests" \
-H "Content-Type: application/json" \
-d '{"userId":"u001","fileName":"a.txt"}'
curl -i "<INVOKE_URL>/requests"
curl -i "<INVOKE_URL>/requests/<requestId>"
結果
-
sam deploy --guidedでApiBaseUrlを払い出し - 払い出しURLで
POST /requests、GET /requests/{id}、GET /requestsの再検証に成功 - GUI構築時の動作を、SAM経由でも再現できることを確認
S3バケットへのPUT結果
DBのステータス更新
6. 工程4: テストを段階分けして設計する
目的:
CIでの「速い検知」と手起動での「実環境での確実性」を両立することで、テストとしての運用性と品質担保を目的。
この節で確認すること:
- なぜ単体/LocalStack/AWS実環境の3層に分けるか
- PR必須CI(
verify)で何を保証するか - AWSでしか確定できない項目をどこで確認するか
全体方針(3層構成):
- 単体/契約テスト: API通信の動作および契約に関して、コード変更で壊れていないかを高速に検知する
-
SAM + LocalStack: API入口からDynamoDB/S3連携までのローカル疎通を確認する - AWS実環境確認: IAM・実エンドポイント・イベント連携を最終確認する
分離した理由は、全てをPR必須CIに載せると実行時間と不安定要素が増え、日常開発のフィードバックが遅くなるためです。
そのため、CIは軽量で再現性の高い検証に絞り、重い確認は段階を分けて実施しています。
6-1. テスト戦略(全体)
- PR必須CI:
mvn verify(高速・安定を優先)- GitHub Actionsで
pull_request(master向け)とpush(master)をトリガーに実行
- GitHub Actionsで
- AWS依存の重い確認: 手動実施(マージ前/リリース前)
-
SAM + LocalStack: 補助的なローカル統合検証
6-2. 単体/契約テスト
RequestValidatorTestRequestS3KeyBuilderTestRequestServiceTestRequestApiHandlerTest
確認観点:
- 入力検証(必須・形式)
- DTOマッピング
- ハンドラー契約(
200/400/404/500)
6-3. SAM + LocalStack 統合検証
目的:
- API Gateway経由のLambda呼び出し(
sam local start-api) - LambdaからDynamoDB/S3への接続
- Presigned URLを使った
Client -> S3 PUT -
GET /requests/{id}で状態確認(ローカルで確認可能な範囲)
注意点(ローカルの限界):
- 分かる: API契約、入力検証、DynamoDB保存取得、Presigned URL発行とPUT疎通
- 分からない: AWS本番のEventBridge連携最終挙動、IAM実権限差分
必要なもの(前提):
- Java 21
- Maven
- Docker Desktop(起動済み)
- SAM CLI
- LocalStack(
s3,dynamodb,events) -
env.local.json(DDB_ENDPOINT/S3_ENDPOINT/AWS_REGION)
env.local.jsonを用意する理由:
-
sam localで起動したLambdaに、ローカル接続先を環境変数で明示注入するため - 本番向けコードを変更せずに、接続先だけをLocalStack向けへ切り替えるため
- 毎回の手動exportを減らし、再実行時の設定ブレを防ぐため
env.local.json例:
{
"RequestApiFunction": {
"DDB_ENDPOINT": "http://host.docker.internal:4566",
"S3_ENDPOINT": "http://s3.localhost.localstack.cloud:4566",
"AWS_REGION": "ap-northeast-1",
"AWS_DEFAULT_REGION": "ap-northeast-1",
"REQUESTS_TABLE_NAME": "requests",
"UPLOAD_BUCKET_NAME": "fileupapi-local-uploads"
},
"RequestStatusUpdateFunction": {
"DDB_ENDPOINT": "http://host.docker.internal:4566",
"AWS_REGION": "ap-northeast-1",
"AWS_DEFAULT_REGION": "ap-northeast-1",
"REQUESTS_TABLE_NAME": "requests"
}
}
env.local.json利用手順:
-
env.local.jsonをプロジェクトルートに配置する -
sam local start-api --env-vars env.local.jsonで起動する -
POST /requests実行後、返却のuploadUrlでPUTする -
GET /requests/{id}とGET /requestsで状態を確認する
手順/設定(最小手順):
- LocalStack起動
- S3バケット作成 + 直後存在確認
- DynamoDBテーブル作成 + 直後存在確認
sam buildsam local start-api --env-vars env.local.json-
POST /requestsでuploadUrl取得 -
uploadUrlへPUT -
GET /requests/{id}とGET /requestsで確認
再現時の注意点(ハマりどころ):
- S3エンドポイントはLocalStack公式に沿う(
s3.localhost.localstack.cloud) - 署名付きURLは毎回再発行(期限・再利用回避)
-
uploadUrlは手打ちせずレスポンスからそのまま利用 - LocalはEventBridge連携の最終確認環境ではない(最終確認はAWS実環境)
結果:
- 契約確認:
404/400/400 - 正常系:
POST /requests -> 201,GET /requests/{id} -> 200,GET /requests -> 200 - API契約とDynamoDB連携のローカル再現を確認
- Presigned URL生成の統合確認
6-4. AWS実環境確認
目的:
ローカルでは確定できない項目(IAM、EventBridge、実S3)を最終確認すること。
この節で確認すること:
- EventBridgeが実際に起動し、状態更新まで到達すること
- IAM権限が実環境で不足しないこと
- 実S3のPUT後に
GET /requests/{id}がCOMPLETEDへ遷移すること
確認シナリオの意味:
-
POST /requestsは受付を作成し、初期状態RECEIVEDを確定する -
uploadUrlへのPUT後、イベント駆動でステータス更新Lambda(RequestStatusUpdateFunction)が起動する -
GET /requests/$REQUEST_IDでCOMPLETEDを確認し、POST -> PUT -> COMPLETEDの因果を確認する
イベント駆動の実行経路:
- 利用者が
uploadUrlにPUT実行 - S3 Object Created を EventBridge が受信
- EventBridge がステータス更新Lambda(
RequestStatusUpdateFunction)を起動 -
RequestStatusUpdateFunctionが DynamoDB の対象レコードをCOMPLETEDに更新
手順/設定(時系列):
-
POST /requestsでrequestIdとuploadUrlを取得 -
uploadUrlへPUT -
GET /requests/$REQUEST_IDでstatus=COMPLETEDを確認 -
GET /requestsで一覧確認
実行コマンド:
RESPONSE_JSON=$(curl -s -X POST "<INVOKE_URL>/requests" \
-H "Content-Type: application/json" \
-d '{"userId":"u001","fileName":"a.txt"}')
echo "$RESPONSE_JSON"
REQUEST_ID=$(echo "$RESPONSE_JSON" | jq -r '.requestId')
UPLOAD_URL=$(echo "$RESPONSE_JSON" | jq -r '.uploadUrl')
curl -i -X PUT "$UPLOAD_URL" \
--upload-file /path/to/upload-file.txt
curl -i "<INVOKE_URL>/requests/$REQUEST_ID"
curl -i "<INVOKE_URL>/requests"
結果:
-
POST /requests、GET /requests/{id}、GET /requestsのスモークが成功 - DynamoDB
requestsへの保存を確認 - S3 PUT後の
status=COMPLETED反映を確認
失敗時対応(再発防止):
- EventBridgeが未起動の場合、まず対象S3バケットの
Amazon EventBridge連携設定を確認する - ルールの
source/detail-type/bucket.nameを実バケット名と突合する - ステータス更新Lambda(
RequestStatusUpdateFunction)のCloudWatch Logsで、requestId抽出失敗やDynamoDB更新エラーを確認する - 署名付きURLは毎回再発行し、レスポンスの
uploadUrlをそのまま使う
6-5. RepositoryテストをCIに置かない理由
RequestRepository はDynamoDBとの境界層です。
この層はSDKモックだけでは実契約を十分に再現しにくいため、PR必須の verify には含めていません。
代替として、次のレイヤで確認しています。
-
SAM + LocalStackで疎通確認(保存/取得/一覧) - 実AWSスモークで最終確認(IAM・実エンドポイント・実データ反映)
方針としては「Repositoryを未検証にする」のではなく、
CIは軽量契約に限定し、永続化契約は統合レイヤで分離確認する運用です。
7. 工程5: GitHub運用とCIを固める
開発ルールは次で固定しました。
- 機能追加やコード改修の場合は、新規ブランチを切ってコミット
- Masterブランチへの直接プッシュ禁止
- PR必須(required status check はverify)
- strict(Require branches to be up to date before merging)有効
-
工夫:
- ジョブを増やしすぎず、壊れたら困る箇所のみCIで常にテスト
- API通信の処理や全体のビルド

一人で開発しているので、ひたすらコミット、プルリク、マージを繰り返しています。
8. 実装・検証で得た学び
8-1. 非同期完了判定の設計判断
Presigned URL方式では、アップロード通信の終端は Client -> S3 です。
そのため、利用者向けの「アップロード完了」は、S3へのPUT成功(HTTP 2xx)を基準に判定します。
一方で、status=COMPLETED 反映は S3 -> EventBridge -> Lambda -> DynamoDB の非同期処理です。
この反映完了を同一レスポンスで待たせる設計は採用せず、バックエンド整合として追従させることにしました。
採用判断:
- 前提AWSハンズオンとしてそこまで厳密な即時通知を求めていない
- クライアント側の完了通知は、S3 PUT成功で即時表示する
- DynamoDB反映は非同期で処理し、必要時のみ
GET /requests/{id}で参照する
8-2. 検証で得た失敗知見と是正
env.local.jsonに記載するS3_ENDPOINTですが、
当初は Docker の一般的な接続先として host.docker.internal を S3_ENDPOINT に設定して検証しました。
しかし、以下のエラーが発生しました。
curl: (6) Could not resolve host: fileupapi-local-uploads.host.docker.internal-
NoSuchBucket(BucketName=uploads)
原因は、Presigned URL の解釈が S3 の Host/Path ルールに依存する点を、LocalStack仕様と照合せずに設定したことです。
LocalStack 公式リファレンスを確認すると、S3では Path-style / Virtual-hosted-style の扱いが明示されており、Virtual-hosted方式でbucketを正しく解釈するには s3. プレフィックスのあるエンドポイント(例: s3.localhost.localstack.cloud)を使うべきと記載されていました。
参照:
- LocalStack S3(Path-style / Virtual-hosted-style)
https://docs.localstack.cloud/aws/services/s3/ - AWS S3 Virtual Hosting
https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html
9. まとめ
今回のAWSハンズオン構築で以下の点を学ぶことができました。
- API 3本の実装とAWS実環境スモーク
- Lambda内部の責務分離
- SAMへの再現可能な反映
- CIと実環境確認の役割分離
Javaを用いてのAPI開発とJava SDKの利用、AWS SAMでの環境再現などをざっくりと学ぶことができたので、トライしてよかったです。
有効だった工夫:
- 方針を先に固定し、工程ごとに判断理由を明示したこと
- 先にGUIで理解し、後でSAMに戻して再現性を確保したこと
- テストを段階分けし、CIの範囲と手作業での範囲を分けたこと。
今後の拡張:
-
SAM + LocalStackの自動化(スクリプトを利用した自動実行) - 認証・監視を含む本番寄り運用への拡張
以上、ここまでお付き合いいただきありがとうございました。


