こちらの記事は「MEDLEY Summer Tech Blog Relay」の3日目の記事です🪼
はじめに
こんにちは。株式会社メドレーのSREエンジニアの森川です⚙️
突然ですが、Webサービスにおいて配信する画像のサイズを調整できる機能は重要性が高いと言えるのではないでしょうか。様々なデバイスやレイアウトに対応するため、同一の画像を複数のサイズで配信できることが望まれます。
本記事では、AWS CloudFront と Lambda@Edge を活用して画像リサイズ配信システムを構築した過程でのアーキテクチャの変遷について記録します。
技術的制約や運用上の課題により、当初想定していた構成では解決できない問題が次々と発生しました。その結果、最初に構築を行った後、さらに2回にわたる大幅なアーキテクチャの見直しを行うことになってしまったのです....
要件と課題
要件
- オンデマンドかつ高速な画像リサイズ配信
- すでに大量の画像が様々なサイズで保存されている状況
- 保存時のサイズ変換ではなく、配信時のサイズ変換を実装する
- URLパラメータによるサイズ指定
直面した課題
- 管理が困難なS3のオブジェクトパス構造
- Lambda@Edgeのレスポンスサイズ制限
アーキテクチャの変遷(第1〜3まで)
第1段階:基本実装
AWS公式のブログ*1でアーキテクチャとLambdaのコードが紹介されていました。
*1 Amazon CloudFront & Lambda@Edge で画像をリサイズする
紹介されていた構成の通りに実装を行いました。
画像は上記ブログから引用
処理概要
Lambda@Edge:Viewer-Request
1. クエリパラメータ `d` (dimension) を解析
例:/foo/bar/test.jpg?d=300x300
2. 許可されたサイズリストをチェック
定義例:[{w:100,h:100}, {w:200,h:200}, {w:300,h:300}, {w:400,h:400}]
3. 指定サイズが許可範囲内(variance: 20%)かを判定
範囲内 → 許可サイズに補正
範囲外 → デフォルトサイズ(200x200)に設定
4. Accept ヘッダーでWebP対応をチェック
5. パスを変換してOriginに転送
元:/foo/bar/test.jpg?d=300x300
↓
新:/foo/bar/100x100/webp/test.jpg (WebP対応時)
/foo/bar/100x100/jpg/test.jpg (非対応時)
Lambda@Edge:Origin-Response
1. S3からのレスポンスステータスをチェック
2. 200(リサイズ済み画像が存在):
そのまま返す
3. 404(リサイズ済み画像が未存在):
パスを解析して元画像のキーを取得
例:foo/bar/300x300/webp/test.jpg → foo/bar/test.jpg
↓
S3.getObject() で元画像を取得
↓
Sharp ライブラリでリサイズ処理
↓
S3.putObject() でリサイズ済み画像を保存
↓
リサイズ済み画像をbase64エンコードしてレスポンス
課題
構築が終わりアプリケーション開発チームに試用いただいているうちに、いくつかの改善点が見えてきました。
ストレージ管理の複雑さ
- 元画像とリサイズ後画像が第一階層レベルから分かれていない
- リサイズ後画像のみを一括で操作したい場合は
*/WxH/{拡張子}/*
のようなパターンマッチングが必要になり、かなり厳しい
パラメータ仕様の制約
- 現在の
d=400x600
形式よりも、幅・高さを別々で指定できる形式の方がアプリ側の開発実装上、利便性が高い - 画像の用途に応じて、許可サイズのバリエーションを変えたい
- 例)プロフィール画像は正方形のみ、ヘッダー画像は横長の複数サイズなど
第2段階:アーキテクチャの大幅見直し
第1段階で明らかになった課題を解決するため、アーキテクチャを大幅に見直しました。
変更点
ストレージ戦略の変更
- リサイズ後の画像をS3に保存せず、すべて Lambda@Edge から配信
- S3バケット内は元画像のみとなり、管理が大幅に簡略化
パラメータ仕様の柔軟化
- URLパラメータを
w=400 & h=600 & type=profile
という形式に変更 - 幅・高さを個別のパラメータで指定
- 画像タイプごとに許容するサイズパターンの制御が可能に
- 例)profile: 200x300, 400x600 / header: 800x200, 1200x300
Lambda@Edge:Viewer-Request を削除
- リサイズ後の画像をS3に保存しない
- → リサイズ後の画像はS3から探さない
- → ビュアーリクエスト時点でのパス変換が不要
キャッシュ戦略の最適化
- CloudFrontでURLパラメータ込みでキャッシュ
- 同一パラメータのリクエストは高速なキャッシュヒットで配信される
課題
この構成で本番運用まで行ったのですが、新しい問題が発覚しました。
画像リサイズにおいて散発的なエラーを検知したため調査したところ、Lambda@Edgeのレスポンスサイズ制限(1MB) に起因するエラーでした。
- Lambda@Edge 関数がリクエスト本文を置き換える場合、関数が返す本文には、以下のサイズ制限が適用されます。
- Lambda@Edge 関数が本文をプレーンテキストとして返す場合:
- ビュアーリクエストイベントでは、本文が 40 KB に制限されます。
- オリジンリクエストイベントでは、本文が 1 MB に制限されます
暫定対応としてリサイズ後の画像が 1MB を超えるサイズとなってしまった場合は元画像を返すようにしたのですが、この問題の根本解決のためには、またもやアーキテクチャの大工事が必要となってしまうのです😭
第3段階:1MB制限対応のための根本的な再設計
第2段階では、S3にリサイズ画像を保存しないようにし、ビュアーリクエスト Lambda@Edge の削除まで行いました。
しかし 1MBを超える画像を返すためには、S3にリサイズ画像を保存しておく必要があるという結論に行き着きました。
最終的なアーキテクチャは以下の通りになります。
変更点
Lambda@Edge:Viewer-Request の復活
- リサイズ後の画像をS3に保存するため、リサイズ後の画像を探すためのパス変換
- 変換前:
/foo/bar/test.jpg?w=400&h=600&type=profile
- 変換後:
/resized_profile/foo/bar/test_400x600.jpg
- 変換前:
リサイズ後の画像のS3パス管理
- リサイズ後の画像の保存先のパスには
/resized_{type}/
プリフィックスをつけるよう統一 - 元画像とリサイズ後画像の分離により管理性が向上
- タイプ別の一括操作も容易に
1MB以上の画像は301リダイレクト経由で取得させる
- リサイズ後の画像のサイズに応じて配信方法を振り分ける
- 1MB未満の画像:Lambda@Edgeから直接配信
- 1MB以上の画像:S3保存後、301リダイレクトで配信
- リダイレクトURL:
https://cdn.example.com/resized_profile/foo/bar/test_400x600.jpg
- 301リダイレクトをレスポンスとして返す際は、リダイレクトループ回避のためにCloudFrontでキャッシュさせないようにヘッダーを調整(以下参照)
// リサイズ後の画像サイズが1MB以上の場合は301リダイレクト
if (resizedImageSize >= MAX_RESPONSE_SIZE) {
// リダイレクトURLを生成
const redirectUrl = generateRedirectUrl(request, resizedObjectKey);
// 301レスポンスを構築
response.status = "301";
response.statusDescription = "Moved Permanently";
response.headers["location"] = [
{ key: "Location", value: redirectUrl },
];
// CloudFrontにキャッシュさせないためのヘッダーを追加
response.headers["cache-control"] = [
{
key: "Cache-Control",
value: "no-cache, no-store, must-revalidate",
},
];
response.headers["pragma"] = [{ key: "Pragma", value: "no-cache" }];
response.headers["expires"] = [{ key: "Expires", value: "0" }];
delete response.body;
return response;
}
パフォーマンス
測定条件
- サイズ指定:400x400px
- 測定地点:東京リージョンからのアクセス
測定結果
元画像サイズ | リサイズ後配信方法 | 初回処理時間 | キャッシュヒット時 |
---|---|---|---|
1.2MB (JPG) | Lambda@Edge直接配信 | 891ms | 20〜50ms |
2.6MB (JPG) | Lambda@Edge直接配信 | 965ms | 20〜50ms |
3.3MB (JPG) | 301リダイレクト | 1.9s | 20〜80ms |
初回のリサイズ処理では処理時間がかかりますが、CloudFrontのキャッシュ効果により実用的なパフォーマンスが達成できています。
まとめ
CloudFrontとLambda@Edgeを使った画像リサイズ配信システムの構築において、3段階にわたるアーキテクチャの進化を通じて多くの学びを得ることができました。
URL・S3のパス設計については初期段階から考えて尽くしておくことが必要でした。第1段階では元画像とリサイズ後画像が混在してしまい管理が煩雑でしたが、最終的に /resized_{type}/
プリフィックスを活用した階層化により、今後の運用がずっと楽になりそうです。
また、Lambda@Edgeの制限への対処では、1MBというレスポンスサイズ制限に直面しましたが、画像サイズによって配信方法を振り分けることによりうまく回避することができました。ただし、301リダイレクト時のCDNキャッシュ設定には注意が必要でした。
今後も継続的な運用を通じて、さらなる最適化を図っていきたいと考えています!
明日は、メドレー中村さんの記事です!お楽しみに!✨
We’re hiring!
メドレーでは各種エンジニアを絶賛募集中です!
カジュアル面談いつでもWelcomeですので、どうぞお気軽にお問い合わせください🍉🌻