ほぼ無料でAWSサーバーレスアプリ構築(バックエンド編)
学習目的で天気予報Webアプリを作ってみた。
YouTubeでライブ配信されている街並みの映像から、服装や傘の有無を検出し、その情報を組み合わせて天気情報として表示するというものです。
今回は、学習目的であったため、使ってみたいものを組み合わせて開発することにしました。こんな構成で作る意味はあまりないかもしれない。
- インフラ
- AWSサーバーレス(CloudFront、Lambda、DynamoDB、SQS、SES、ECR、S3、EventBridge)
- Terraform
- バックエンド
- Python FastAPI
- Lambda + Mangum
- フロントエンド
- TypeScript Next.js
- Lambda Web Adapter
- 物体検出
- EfficientNetV2、SAHI、YOLO
主にAWSのサーバーレスとTerraformを使ってみたかったということもあり、結果的に複雑な作りになってしまった気もします。
あとは、出来るだけコストをかけたくなかったので、Lambdaをフル活用して、AWSの無料枠で動かせる範囲で作ることにしました。
https://github.com/pontago/otenki-live
バックエンド
映像から物体検出と画像分類をおこなう必要があったので、バックエンドはPythonで構築することにしました。というより、Pythonが使いたかったというのが大きいです。
最初は、Djangoを検討しましたが、システムの規模感的にここまでのフレームワークを導入するのは、学習コストも高く、些か大げさすぎると思ったので、FastAPIを採用しました。
最近主流のuv、ruffを用いて環境構築をおこないました。ついでに型チェックもできるmypyも導入しました。
ディレクトリ構成に悩む
クリーンアーキテクチャーで作っていこうと思いましたが、ディレクトリ構成をどのようにするかで悩んだ結果、各層ごとにディレクトリを作って、その下に機能別にディレクトリを作成する方法を選択しました。
├── adapter
│ ├── api
│ └── handler
├── core
│ ├── di
│ └── translations
├── domain
│ ├── entities
│ ├── repositories
│ └── services
├── infrastructure
│ ├── dto
│ ├── mappers
│ ├── repositories
│ └── seed
└── usecases
├── area
├── contact
├── live_channel
├── services
└── weather_forecastこのディレクトリ構成で作ってみた結果は、各層ごとにファイルが分散してしまうため、ファイルの移動がとても面倒になったということです…。例えば、エリア機能を編集する際にusecase、repository、domainと各層に移動しながら目的のファイルを探すことになります。
この規模ならそれほど問題になりませんが、システムが大きくなるほど、この影響が大きくなります。
この教訓から下記のようなディレクトリ構成にしたほうが良いと思いました。
機能別に作ったディレクトリ以下に各層のディレクトリを配置します。冗長的な感じで微妙だと思っていましたが、「関連するファイルは近くに配置する」ことが重要だと分かりました。
同じようにユニットテストも各ファイルと同一階層に配置することで、テストコードが書きやすくなり、管理もしやすくなります。
├── adpter/
│ ├── api/
│ └── handler/
├── core/
│ ├── di/
│ └── translations/
└── features/
└── area/
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── services/
├── infrastructure/
│ ├── dto/
│ ├── mappers/
│ ├── repositories/
│ └── seed/
└── usecases/
├── list_area_interactor.py
└── list_area_interactor.test.pyDynamoDBに苦戦する
使い慣れているのは、RDBMSのMySQLですが、AWSの無料枠で使えるのは、DynamoDBというNoSQLのkey-valueデータベースしかありません。
似たようなものでは、FirebaseのFirestoreを使ったことはありますが、DynamoDBのほうがさらに制約が多く、RDBとはまったくの別物といった感じです。
スキーマレスで柔軟に利用できますが、設定できるインデックス数に上限(サポートに連絡すると増やせるらしい?)があり、最初の設計を慎重におこなわないと思わぬトラブルが起こりそうです。
- シングルテーブル設計
- 一つのテーブルにデータセットを集約する方法
- マルチテーブル設計
- テーブルごとに正規化したデータセットを配置する方法
JOINなどが使えないため、非正規化してデータを冗長的に持たせる必要が出てきます。下記サイトのファセットを使ってデータを識別する方法が参考になりました。
https://note.shiftinc.jp/n/nec348e98df2d
2025年11月にGSIの複合インデックスがサポートされたようです。PKやSKに属性結合を使う必要がなくなるので、かなり使い勝手がよくなったようです。
今回は、そこまで複雑なデータを扱わないため、マルチテーブル設計である程度正規化した状態で複数テーブルを作成することにしました。
大まかな理由は、
- そこまで複雑なテーブルを扱わない
- インデックス数の上限が気になった
- クエリ操作が非常に高速なため、複数テーブルにまたがった操作でも気にならないと思った
- データはTTLの設定で1ヶ月後に自動削除する
下記のように今回作成するテーブルは、主に取得結果のキャッシュが目的であったため、そこまで念入りに設計する必要もないというのもあります。
- APIから取得した天気情報のキャッシュ
- YouTubeストリームの情報
- ライブ映像から取得した物体検出結果
AWSサーバーレスの環境構築
モックデータである程度、基本的な部分の実装を進めた後、DynamoDBと実際に連携させて開発しました。LocalStackというAWS環境のエミュレーターを使えばお手軽に開発環境を整えることができます。
無料だと機能は限られますが、今回の構成なら最低限動かすことができます。
dynamodb-adminを使えば、GUI上でデータの確認も可能です。
https://github.com/aaronshaf/dynamodb-admin
ライブ映像から物体検出
メインの処理となるのは、ライブ映像から傘や服装を検出する処理です。
検出フローは、SAHI+YOLOで大まかな人物と傘を検出した後、服装分類処理をかけることで服装を判定します。
傘については高精度な検出モデルがすでに存在するため、特に苦労はありませんでしたが、服装については機械学習させたモデルを作成する必要がありました。当初は、ジャケットやコートなど細かく分類したかったのですが、思うように精度が出なかっっため、半袖と長袖の分類にとどまりました。この2つの精度も正直いまいち…。マルチモーダルのLLMを使った方がまだいいかもしれません。(ただしコストがかかる)
コストを抑えてLambda上で物体検出をする必要があったため、大きなモデルを使うことが出来ず、精度が微妙になったというのもあります。(メモリ1024MB以内で動かしたい)
Lambda上で動かす工夫としては、
- ResNetを使った分類モデルのサイズが大きく、EfficientNetV2で作り直した
- YOLOのモデルを小さいものへ変更
- LambdaのZIPデプロイサイズ上限の250MBがあるため、コンテナイメージで動かす
- モデルファイルは大きいのでS3からダウンロードして利用する
コンテナイメージで動かすため、ECRの料金がかかってしまうのがネックです。(月10円ほど)
流れとしては、
- EventBridgeで一定時間ごとにLambdaを動かし、未処理のライブストリームを検出タスク用のSQSに渡す
- SQSから起動したLambdaで物体検出し、結果をDynamoDBに保存
Step Functionsを使えば、もう少しスマートにできそうですが、無料枠を超えてしまいそうだったので、少し面倒な工程を挟んでいます。
Lambdaで物体検出させるという無理やりな感じになりましたが、小規模なものなら何とか動かせるといった感じでしょうか…。
フロントエンド編に続く。