こんにちは、シンクロ・フードの大久保です。
今回は実際に弊社で運用しているAWS Lambdaを使ったリアルタイム画像変換APIについてご紹介したいと思います。 リアルタイム画像変換APIについては、あまり詳しく説明しませんが、画像のサイズ変換等をURLパラメータで指定してリアルタイムに変換することです。 弊社の場合はリサイズしか行わないため、画像変換APIというよりは画像リサイズAPI、というほうが適切かもしれません。
リアルタイム画像変換の方法について
弊社の方法をご紹介する前に、リアルタイム画像変換の一般的な実現手法を挙げてみます。
- Nginx, ApacheなどのWebサーバのプラグインを用いる
- CDNとして提供されている機能を用いる
1は、クックパッド社のmod_tofuが有名で、色々企業で実現されている手法です。サーバを用意しなければならない点や、冗長化を考えると弊社ではコスト的に取り組めませんでした。 2は、Akamaiやfastlyなど、高機能なCDNサービスを使う方法です。このあたりのサービスは以前は高額なものが多かったのですが、fastlyなどはとてもお得だと思います。
APIドキュメント
https://docs.fastly.com/api/imageopto/
料金
https://www.fastly.com/pricing/
こんなブログポストを書いておいてなんですが、リアルタイム画像変換は自作するよりも、fastlyの利用を検討することをオススメします。弊社も、このリアルタイム変換のシステムを作る前にfastlyで実現できることを知っていたら、fastlyを利用していたのではないかと思います。
ということで、以下はそれでもLambdaを使って自分たちで構築したい、という方向けの記事です。
設計
ベースとなる設計は、以下のAWSブログの記事をベースとしています。
弊社はこの設計に、さらにCloudFrontをかませており、以下のようなリクエストの流れとなります。元記事よりリクエストの流れを丁寧に説明していきます。
前提として、S3のバケットは2つ用意する必要があります。1つはリサイズ元画像を格納するOriginalBucket、もう1つはリサイズ後の画像を格納するResizedBucketです。
- ユーザーがCloudFrontにリクエストを送ります。初回リクエストはCloudFront上にキャッシュが無いため、CloudFrontはオリジンとして設定しているS3のResizedBucketにリクエストを流します。
- CloudFrontから流れてきたリクエストを受け、S3はBucket内を探しますが、Resizeされた画像が存在しません
- 画像が存在しない場合、内部用の画像変換APIへ307レスポンスが投げられます
- CloudFrontはS3からの307のレスポンスをそのままユーザーへ戻します
- ユーザーは307レスポンスを受け、次は内部用画像変換APIのURLを待ち受ける、APIGatewayにリクエストを投げます
- API GatewayよりLambdaの画像変換関数がキックされます
- LambdaがS3のOriginalBucketより、画像リサイズ元となる画像を取得します
- 7で取得した画像をパラメータに従ってリサイズし、ResizedBucketに保存します
- 8で保存した画像へのURLへの301レスポンスをLambdaにて返します
- 301レスポンスをそのままユーザーに返します
- ユーザーは301レスポンスに従い、もう一度リサイズ画像のリクエストを送ります
- CloudFrontからResizedBucketにリクエストが流れ、変換後画像をレスポンスとしてキャッシュしつつ、結果をキャッシュする
大きな流れは以上です。最初に画像を閲覧したユーザーは3回リクエストが飛ぶことになりますが、2回目以降はCloudFrontがキャッシュしているため、高速に戻すことができます。 リクエストが3回も飛ぶ動きなどが一見気持ち悪い手法ですが、この方法で半年以上運用し、特に大きな問題は起きていません。
以降、少し細かい設定方法を説明していきます。
Lambda関数を用意する
流れの図にある、7(S3からリサイズ元画像を取得する)と8(画像をリサイズしResizedBucketに入れる)、という2つの処理を行うLambda関数を作ります。 コードは、元記事も紹介している、以下のコードを改造していくのが良いと思います。
https://github.com/awslabs/serverless-image-resizing
弊社はこのコードを元に、以下の修正を加えて運用しています。
- 不正なリサイズリクエストを除外するためのハッシュチェック
- 細かいバリデーション、ログ埋め込みなど
- serverlessフレームワーク化(まったく必須ではありません)
ハッシュチェックについては、誰かれかまわずリサイズできると困るため、確実に自分たちのサービスからのリクエストであることをチェックするために、アプリケーション側で作成したハッシュ値をLambda側でチェックしています。
API Gatewayの設定
こちらは特に特殊なことをやっているわけではないのですが、API Gatewayは分かりにくいため、記載しておきます。
リソースの作成
まず、API Gatewayにある「APIを作成」というボタンから新しいAPIを作成したあと、「アクション」→「リソースの作成」を選択します。
その後、上記画面のような入力画面が表示されるのですが、ここで「プロキシリソースとして設定」にチェックを入れて、リソースを作成してください。進むと以下のような画面が出ます。
こちらでは統合タイプを「Lambda関数プロキシ」、Lambdaリージョンは、後述するLambda関数のリージョン、Lambda関数は、作成したLambda関数を指定してください。
デプロイ
「アクション」→「APIのデプロイ」を選択し、ステージを選択します。 最初はステージがないため、「新しいステージ」を選択し、ステージ名を入力してください。 とりあえず、prod、という名称にすることが多いように思います。
デプロイが完了すると、APIGatewayのURLが生成されます。
https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod
これでAPIGatewayの準備は完了です。
S3の設定
上記設計図におけるResizedBucketは、StaticWebHostingを有効にしておく必要があります。それ自体は簡単なのですが、同時にリダイレクトルールを以下のように設定しておく必要があります。
<RoutingRules> <RoutingRule> <Condition> <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals> </Condition> <Redirect> <Protocol>https</Protocol> <HostName>${API Gatewayのホスト名}</HostName> <ReplaceKeyPrefixWith>${API Gatewayのステージ名}/resize?key=</ReplaceKeyPrefixWith> <HttpRedirectCode>307</HttpRedirectCode> </Redirect> </RoutingRule> </RoutingRules>
もう一つ、Lambdaが作成した画像ファイルを都度閲覧可能にする必要があるため、バケットポリシーの設定も必要です。この設定がないと、Lambdaが作成したリサイズ後画像がPrivate権限となっていて、画像参照ができません。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::${S3のバケット名}/*" } ] }
CloudFrontの設定
こちらは通常通り作成し、オリジンにはResizedBucketを指定します。
キャッシュ問題について
ただし、このままですと、CloudFrontは1回目の307リクエストの結果をキャッシュしてしまい、変換後の画像を表示してくれません。
この問題は、CloudFrontがキャッシュするかどうかは、S3のオブジェクトが持つCacheControlヘッダを使うようにすれば、解決します。
具体的な操作は以下の通りです。
- CloudFrontの「Behaviors」→「Edit」→「Object Caching」を、「Use Origin Cache Headers」から「Customize」にする。
- Lambda側のコードで、リサイズ後画像をS3にPutする際に、CacheControlヘッダを追加する。
.then(buffer => S3.putObject({ Body: buffer, Bucket: BUCKET, ContentType: 'image/png', CacheControl: 'max-age=2592000, s-maxage=259200', Key: key, }).promise() )
これでCloudFrontが適切にCacheしてくれるようになります。
基本的な設定の流れは以上です。 この設定を行うことで、以下のようなURLを渡すと、そのサイズにリサイズされた画像が戻ってくるようになります。 呼び出しイメージは以下のような雰囲気です。
https://xxxxxxxxxxx.cloudfront.net/hash/300x100/xxxx.jpg
実際に運用した感想
実際に上記のような設計でリアルタイム画像変換APIを作成し、半年ほど運用をしましたので、所感を列挙します。
- 安定しています。CloudFront,S3はもちろんですが、lambdaでのトラブルは一切ありません。
- 画像以外のリクエストが多い。faviconや画像以外のファイルリクエストが沢山きます。リリース後、余計なリクエストを除外する仕組みを入れました。
- エラー検知は、とりあえずCloudWatchにて、Lambdaの実行エラーをアラームにしています。変換に失敗したときにメールが飛んできますが、エラー内容がメールに記載されていないので、少し面倒です。
- lambdaのメモリ設定やtimeout設定などは、最初大きめに取り、ログを見ながら最適なメモリサイズやtimeoutに落としていく、という方法が良さそうです。
- (これはリクエスト数次第ですが)費用は安いです。一度変換した画像は二度と変換が実行されないため、lambdaの呼び出し回数も少ないです。
まとめ
特に手法自体は新規性がないと思いますが、実際にプロダクション環境で運用している事を公開することで、誰かの参考になるかなと思い、ご紹介しました。 リアルタイム変換を今から導入しようと思うのであれば、まずはFastlyを第一に検討すべきかと思いますが、なんらかの問題があった場合などにご検討ください。
シンクロ・フードでは常にエンジニアを募集しています。 ご興味のある方は以下よりエントリーしてみてください!応募ではなく、話を聞いてみたい的なものでも結構ですので、お気軽にお問い合わせください。