シンクロ・フード エンジニアブログ

飲食店ドットコムを運営する、株式会社シンクロ・フードの技術ブログです

AWS Lambda+API Gateway + S3で格安リアルタイム画像リサイズAPIを作成する

こんにちは、シンクロ・フードの大久保です。

今回は実際に弊社で運用しているAWS Lambdaを使ったリアルタイム画像変換APIについてご紹介したいと思います。 リアルタイム画像変換APIについては、あまり詳しく説明しませんが、画像のサイズ変換等をURLパラメータで指定してリアルタイムに変換することです。 弊社の場合はリサイズしか行わないため、画像変換APIというよりは画像リサイズAPI、というほうが適切かもしれません。

リアルタイム画像変換の方法について

弊社の方法をご紹介する前に、リアルタイム画像変換の一般的な実現手法を挙げてみます。

  1. Nginx, ApacheなどのWebサーバのプラグインを用いる
  2. 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ブログの記事をベースとしています。

https://aws.amazon.com/blogs/compute/resize-images-on-the-fly-with-amazon-s3-aws-lambda-and-amazon-api-gateway/

弊社はこの設計に、さらにCloudFrontをかませており、以下のようなリクエストの流れとなります。元記事よりリクエストの流れを丁寧に説明していきます。

f:id:synchro-food:20180114145809p:plain

前提として、S3のバケットは2つ用意する必要があります。1つはリサイズ元画像を格納するOriginalBucket、もう1つはリサイズ後の画像を格納するResizedBucketです。

  1. ユーザーがCloudFrontにリクエストを送ります。初回リクエストはCloudFront上にキャッシュが無いため、CloudFrontはオリジンとして設定しているS3のResizedBucketにリクエストを流します。
  2. CloudFrontから流れてきたリクエストを受け、S3はBucket内を探しますが、Resizeされた画像が存在しません
  3. 画像が存在しない場合、内部用の画像変換APIへ307レスポンスが投げられます
  4. CloudFrontはS3からの307のレスポンスをそのままユーザーへ戻します
  5. ユーザーは307レスポンスを受け、次は内部用画像変換APIのURLを待ち受ける、APIGatewayにリクエストを投げます
  6. API GatewayよりLambdaの画像変換関数がキックされます
  7. LambdaがS3のOriginalBucketより、画像リサイズ元となる画像を取得します
  8. 7で取得した画像をパラメータに従ってリサイズし、ResizedBucketに保存します
  9. 8で保存した画像へのURLへの301レスポンスをLambdaにて返します
  10. 301レスポンスをそのままユーザーに返します
  11. ユーザーは301レスポンスに従い、もう一度リサイズ画像のリクエストを送ります
  12. 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を作成したあと、「アクション」→「リソースの作成」を選択します。

f:id:synchro-food:20180114161809p:plain

その後、上記画面のような入力画面が表示されるのですが、ここで「プロキシリソースとして設定」にチェックを入れて、リソースを作成してください。進むと以下のような画面が出ます。

f:id:synchro-food:20180118155859p:plain

こちらでは統合タイプを「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を第一に検討すべきかと思いますが、なんらかの問題があった場合などにご検討ください。

シンクロ・フードでは常にエンジニアを募集しています。 ご興味のある方は以下よりエントリーしてみてください!応募ではなく、話を聞いてみたい的なものでも結構ですので、お気軽にお問い合わせください。