サーバレスにサムネイル画像を配信する試み

AWS流行ってますね。

先月末の[re:Invent 2016](https://reinvent.awsevents.com/)では既存のサービスの拡充や、意欲的な新サービスが発表されたようです。

ところで、最近API GatewayはBinary Support, Lambdaでは環境変数のサポートなど、サーバレスな仕組みをお手軽に実現するためのサポートも増えました。[^1]
[^1]: [Binary Support](https://aws.amazon.com/jp/about-aws/whats-new/2016/11/binary-data-now-supported-by-api-gateway/), [Environment Variables](http://docs.aws.amazon.com/lambda/latest/dg/env_variables.html)

今回は、API GatewayのBinary SupportとLambdaを使って既存の構成をサーバレスな仕組みに置き換えられそうか、試してみました。
対象は、サムネイル画像の配信システムです。

**注意**: 資料を元にいくつかの既存システムを紹介していますが、私の解釈の間違いや現状のシステムとの差異がある可能性に注意してください。

## 要件
- ユーザに投稿してもらった画像は、オリジナル画像としてS3に保存
    - オリジナル画像は配信しない (private)
- サムネイル画像が要求されたらオリジナル画像から動的変換[^2] し、CDNで配信
    - サムネイル画像は保存しない
    - CDNにヒットしなかったらもう一度動的変換

[^2]: サムネイル画像が実際に要求された時に初めて変換することを動的変換と呼ぶことにする

### 動的変換は便利
サービスの要求によりますが、よくある事前にサムネイル画像生成は将来的にもアプリが事前に決められたサイズしか要求しない場合には、シンプルで都合が良い構成です。

しかし、既存の画像を丸々新しいサイズにリサイズしなければならないので、新たにレスポンシブルなデザインに対応するなど新しいデバイスの展開やプロトタイピング的な利用が難しくなります。

今回はよりチャレンジングそうな動的変換を行うことにします。

## 既存の構成

動的変換でよくある構成を次に示します。[TOFU](http://www.slideshare.net/mirakui/ss-8150494), [pixivのサムネイル事情](http://inside.pixiv.net/entry/2014/12/10/222735) などを参考にしています。

リクエスト数やCDNへのヒット率に応じて、EC2のインスタンスの増減をすればお手軽にスケールできそうです。

<img width="695" alt="スクリーンショット 2016-12-17 15.33.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/146404/dd448fdb-7d73-447c-e6c0-fcaef4c562b0.png">

### サムネイル画像へのアクセス
- `http://example.com/images/[width]x[height]/[hash]/[image].jpg`
    - `hash`は悪意のあるユーザが `width, height`をちょっとづつずらしてリサイズ処理を多数走らせないように用意

## サーバレスな構成
API GatewayとLambdaを使って次のように構成してみました。シンプルですね。
アクセス方法は既存の構成と同じにできます。

<img width="731" alt="スクリーンショット 2016-12-17 16.47.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/146404/ac584830-cbbe-0f75-24e1-b32f5a6029f0.png">

### スケールすることに関して
- API GatewayはLambdaのプロキシとして十分にリクエストを処理できる
    - AWSがよしなにやってくれる
    - ボトルネックになるとしたらLambda
- Lambdaもスケールすることに関して何も気にしなくて良い
    - AWSがよしなにやってくれる
- Lambdaなのでスケールすることに関して気にする必要がある
    - **リージョン毎に** 同時実行数制限がある (デフォルト: 100)
        - サムネイル処理に1000msかかるとすると、100 rps程度まで処理できる
            - [Lambda 関数の同時実行](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/concurrent-executions.html)
        - AWSに要求すれば引き上げられる

構築手順の概要だけ説明します。

### Lambdaの設定
API Gatewayのバックエンドとして構築し、パス名を処理する必要があります (API Gatewayの設定で`event`にセットできる)。
また、Lambdaから対象バケットのオブジェクトに対して`GetObject` を行える設定が必要です。

変換はLambdaの環境でサポートされているImageMagickが手軽ですね (ImageMagickがベストかどうかについては議論の余地があります)。

```js
const aws = require('aws-sdk');
exports.handler = (event, context, callback) => {
  const s3Client = aws.S3();
  const size = parseSize(event.size);
  const hash = event.hash;
  const image = event.image;

  // hashを使った検査
  ...
  // サムネイル生成
  s3Client.getObject({ Bucket: "...", Key: image }).promise()
  .then(response => resize(response.Body, size))
  .then(thumbnail => callback(null, thumbnail))
  .catch(err => callback(err))
}
```

### API Gatewayの設定
1. `/{size}/{hash}/{image}` というリソースを作成
    - 全てパスパラメータ
2. `/{size}/{hash}/{image}` にGETメソッドを用意
    - 先ほどのLambdaと統合
    - Integrationでマッピングパターンを次のように設定
        - この設定でLambdaのハンドラの`event`にパスが渡せる
3. Binary Supportを利用し、`image/*`のときバイナリを返すように設定

```json
{
  "size": "$input.params('size')",
  "hash": "$input.params('hash')",
  "image": "$input.params('image')"
}
```

### CloudFront
1. Custom OriginをAPI Gatewayのエンドポイントに設定
2. Origin (API Gateway)がサムネイル画像を返せない時は、`no_img.png` のような画像を短い時間キャッシュするように設定
    - 高い負荷の時は、一部この画像が表示されることになる

## 性能
[siege](https://github.com/JoeDog/siege) を使って簡単に測定して得られた知見。

- 800x800 (500KB) のサンプル画像で、100x100のサムネイル生成の平均変換時間は500ms程度
    - いわゆるLambdaのコンテナがHotなとき [参考](http://stackoverflow.com/questions/34216248/aws-lambda-hot-and-cold-start)
- API Gateway + Lambdaは100rpsくらいならErrorなくさばけていた
- Lambdaの同時実行数は100を超えても短い時間なら問題無い

## まとめ
- サーバ管理が不要に
    - AWSの仕様や変更をウォッチし続ける必要はありそうですが...
- CloudFrontのヒット率が高ければ (90%以上)、1000rps程度のサムネイル画像要求ならさばける可能性が十分ある
- 全ての仕組みが従量課金制なので、発展段階の小規模なサービスにはコストを低く抑えやすい
    - Route 53を使えば途中で配信方法を切り替えるなど柔軟な対応もできそう

## 参考文献
### 動的変換
- [TOFU](http://www.slideshare.net/mirakui/ss-8150494), [2015年時点](http://techlife.cookpad.com/entry/2015/03/04/124153)
- [CloudFront+S3の画像配信にリサイズ機能を追加する](http://oretachino.hatenablog.com/entry/2014/12/20/235441)
- [nginxの画像キャッシュサーバにリサイズ機能を付けた](http://qiita.com/zaru/items/1c21d418d69c3505a91a)
- [pixivのサムネイル事情](http://inside.pixiv.net/entry/2014/12/10/222735), [サムネイルマスタとgo-thumber](https://speakerdeck.com/harukasan/use-thumbnail-master-with-go-thumber)
- [とあるECサイトに動的画像変換を導入した話](https://speakerdeck.com/yano3/calamel-okara-beta)
- [簡単！リアルタイム画像変換をNginxだけで行う方法](http://cloudrop.jp/labs/nginx_image_filter)

### 事前に変換
- [紙面ビューアーを支えるサーバーレスアーキテクチャ](http://sssslide.com/speakerdeck.com/ikait/serverless-architecture-supports-nikkeis-paper-viewer)
- [サーバーレスアーキテクチャで画像処理](http://blog.serverworks.co.jp/tech/2016/10/19/serverless_framework/)
- [Serverless image resizing at any scale](https://cloudonaut.io/serverless-image-resizing-at-any-scale/)

### クラウドサービス
- [ImageFlux](https://www.sakura.ad.jp/services/imageflux/)
- [Image Manager](https://www.akamai.com/jp/ja/solutions/products/web-performance/image-manager.jsp)
- [Cloudinary](http://cloudinary.com/)
- [Imagix](https://www.imgix.com/)

### その他
- [画像変換Nightに行ってきた](http://yudoufu.hatenablog.jp/entry/2015/02/18/233921)
- [AWS 導入事例：株式会社スクウェア・エニックス](https://aws.amazon.com/jp/solutions/case-studies/square-enix/)