Blog

フィルター中: Django
HobbyHubの画像アップロードを非同期化する ― S3 + Lambda + Djangoで詰まった4つのポイント

2026年7月8日

HobbyHubの画像アップロードを非同期化する ― S3 + Lambda + Djangoで詰まった4つのポイント

はじめに HobbyHubは、趣味のレッスンをマッチングするマーケットプレイス型のWebアプリケーションです([プロジェクト詳細ページへのリンク])。本記事では、その中でも特に苦労したプロフィール画像アップロード機能について取り上げます。画像処理をサーバーと切り離すために、S3・Lambda・Djangoを連携させた非同期パイプラインを構築しました。詳しい設計判断は後述しますが、まずは全体像についてお話します。 全体のアーキテクチャ HobbyHubのプロフィール画像アップロード機能は、S3・Lambda・Djangoの3者が連携する非同期パイプラインとして構築しています。全体の流れは以下の図の通りです。 処理の流れを簡単に説明します。 ブラウザはPresigned URLを使い、画像を直接S3のstaging/プレフィックスへアップロードします。 ユーザーがフォームを送信すると、Djangoが該当ファイルをprocessing/プレフィックスへコピーし、staging/プレフィックスから削除します。 processing/への書き込みイベントをトリガーに、Lambdaが起動します。 Lambdaは以下の処理を行います。 ファイルサイズの検証(S3イベントのContentLengthが6MB超、または0バイトの場合は処理を中断) 用途別の複数サイズ生成(アバター用のsmall、一覧表示用のmedium、フルサイズ表示用のoriginal) WebP形式への変換によるファイルサイズ削減 プライバシー保護のためのEXIFメタデータ(位置情報など)の削除 変換後の画像をprocessed/プレフィックスへ保存し、元のprocessing/内のファイルを削除 処理が完了すると、LambdaはDjangoへWebhook経由で通知します。この際、共有シークレットを使ったヘッダー認証(X-Webhook-Secret)で、正規のLambdaからの通知であることを検証しています。 通知を受け取ったDjangoは、データベースのprofile_photoフィールドに処理済み画像のパスを保存します。 ブラウザ側のアップロード完了通知に頼らず、Lambda自身がDjangoに完了を伝える設計にしたことで、「アップロードは成功したが処理が失敗した」という状態を防ぎ、DBには常に処理済みの画像だけを参照させる構成にしています。 なぜ同期処理ではなく非同期処理にしたか プロフィール画像のアップロードは、当初はDjango側でアップロードと同時にリサイズ・変換処理を行う同期的な実装も選択肢にありました。しかし、以下の理由から非同期処理を選びました。 1つ目は、リクエスト/レスポンスを画像処理でブロックしたくなかったことです。 画像の検証やリサイズは決して軽い処理ではなく、これをリクエストの中で同期的に行うと、ユーザーの待ち時間が伸びるだけでなく、Djangoのワーカーが画像処理の間占有されてしまいます。 2つ目は、サーバーの役割を明確に分離したかったことです。 Djangoは「リクエストに応答すること」に専念させ、画像の検証・変換という重い処理はAWS Lambdaへ切り出すことで、アプリケーションサーバーへの負荷を減らす設計にしました。 3つ目は、将来的なスケーラビリティです。 Lambdaは呼び出し数に応じて自動的にスケールするため、仮にアップロード数が急増しても、Djangoサーバー側のリソースを圧迫することなく処理をさばける構成にできます。 またS3のプレフィックスもstaging/(仮置き)→processing/(処理待ち)→processed/(処理済み)の3段階に分け、処理途中や不要な画像を溜め込まない設計にしました。この一手間により、トラブルシューティングが必要になった時も「今どの段階で失敗しているか」を追いやすくなります。 実装で詰まった4つのポイント 1. Windows環境でビルドしたPillowがLambda上で動かない Lambda関数の実装は普段通りWindowsのローカル環境で行っていましたが、デプロイ後に以下のエラーで動作しませんでした。 Unable to import …

AWS Django HobbyHub