[2018/03/08追記]API Gatewayでサーバレスな画像リサイズAPIを作る


_<2018/03/08追記>_
2018年3月、[Lambda@Edge](https://aws.amazon.com/jp/lambda/edge/)を使った画像リサイズのソリューションがAWSの公式ブログにて紹介されました。
- _[Amazon CloudFront & Lambda@Edge で画像をリサイズする](https://aws.amazon.com/jp/blogs/news/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/) - Amazon Web Services ブログ_
今後サーバレスな画像リサイズ処理について検討する場合は、上のブログ記事を参照されることをお勧めいたします。
_</追記>_

----


2016年11月に [Amazon API Gatewayがバイナリデータに対応した](https://aws.amazon.com/jp/about-aws/whats-new/2016/11/binary-data-now-supported-by-api-gateway/)ので、Amazon CloudFront、AWS Lambda、Amazon S3を組み合わせて、フルマネージドな画像変換（リサイズ）APIを作ってみる。

# やりたいこと
URL中のパスで指定したファイルを、クエリパラメータで指定したサイズ（今回は幅のみ指定）に変換して返す。

例えば HTML中にimgタグで

```html
<img src="https://resizer.example.com/sample.png?w=50">
```
とか書くだけで望んだ画像を望んだサイズで（sample.pngを幅50で）表示したい。

こうすれば数あるサイズのデバイスに対応するのにいちいちサイズ別画像を生成しておかなくてもよくてハッピー。

## 例

### 元画像
![元画像](https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/sample.png)
`https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/sample.png`

### 幅50に変換
![幅50](https://d3rdcsp6kyxueg.cloudfront.net/sample.png?w=50)
`https://d3rdcsp6kyxueg.cloudfront.net/sample.png?w=50`

### 幅200に変換
![幅200](https://d3rdcsp6kyxueg.cloudfront.net/sample.png?w=200)
`https://d3rdcsp6kyxueg.cloudfront.net/sample.png?w=200`

いい感じ


# ざっくり処理概要

* Lambda
	* S3から画像をとってきて変換してBase64で返す
* S3
	* 元画像置き場
* API Gateway
	* Content-Type, Acceptヘッダがimage/*なときにバイナリを扱えるよう設定
	* Lambdaから返ってきたBase64 stringをバイナリにして返す
* CloudFront
	* 今回はキャッシュ目的でなく、 **API GatewayへのリクエストにContent-Type, Acceptヘッダを付加する** ために使う
	* もちろんキャッシュしてもいい

# アーキテクチャ
![アーキテクチャ](https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/architecture_med.jpg)

# 作り方

※ CloudFront の Distribution 作成後、有効になるまで数十分かかる可能性があるので注意。

## Amazon S3

特に複雑なことはせず、読み込み可能な元画像を置いておく。今回は
https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/sample.png
を用意。

<details><summary>Step-by-step instruction</summary>
1. ↑の sample.png をダウンロード
2.  s3のマネジメントコンソールでバケットを作成 `yourname-image-resizer` 的な名前で
3.  ダウンロードした sample.png をアップロード
4.  アップロードしたファイルを `make public` あるいは `公開する`
5. ![公開するボタン](https://s3-ap-northeast-1.amazonaws.com/tsukada-aws/demos/ImageResizer/img/make_public.png)
6. ↑の `Link` をブラウザで開ける（＝パブリックアクセスできる）ことを確認する
</details>


## AWS Lambda

パラメータ `w` と `filename` を受け取り、 S3の元画像を取得し、imagemagickで変換して、Base64にして返す。
LambdaのBlueprint `s3-get-object` を元にして作った。


<details><summary>Step-by-step instruction</summary>
1.  Lambdaのマネジメントコンソールで `Create a Lambda Function` または `Lambda関数の作成` ボタンをクリック
2.  `Select blueprint(設計図の選択)` で `s3-get-object` と入力し、Node.jsのものを選択
3. `Configure triggers(トリガーの設定)` で S3 っぽいものが出ているかも知れないが、 `Remove(削除)` ボタンで消す（今回のトリガーはあとでAPI Gatewayを設定するので）
4. Lambdaファンクション名は好きな名前でよいが `ImageResizer` あたりにしておく。ソースコードのエディタ部分に ↓の `ImageResizer.js` をコピペする
5. コード中の `key` と `bucket` を自分が作ったバケット名とアップロードしたファイルのパスに合わせる
6. エディタの下にある `Lambda function handler and role(Lambda 関数ハンドラおよびロール)` で、 `Role name(ロール名)` に `ImageResizerLambdaRole` とかそれっぽい名前をつける
7. `次へ` 
8. `Create function(関数の作成)` ボタンをクリック
</details>


### コード

このサンプルは最低限の処理になっていますが、
誰でもどんなサイズでも好き勝手に画像生成されてはたまらん、という方は
**パラメータのValidationなどを入れることを考慮** するとよいです。

```Javascript:ImageResizer.js
'use strict';

const im = require('imagemagick');
const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });

exports.handler = (event, context, callback) => {

    const key = "demos/ImageResizer/img/" + event.filename;
    const bucket = "tsukada-aws";
    const params = {
        Bucket: bucket,
        Key: key,
    };

    s3.getObject(params, (err, data) => {
        if (err) {
            console.log(err);
            const message = `Error getting object ${key} from bucket ${bucket}. Make sure they exist and your bucket is in the same region as this function.`;
            console.log(message);
            callback(message);
        } else {
            console.log(data.Body);
            console.log('CONTENT TYPE:', data.ContentType);
            
            im.resize({
                srcData: data.Body,
                format: "png",
                width: event.w
            }, function(err, stdout, stderr) {
                if (err) {
                    callback('resize failed', err);
                } else {
                    console.log("resized!");
                    callback(null, new Buffer(stdout, 'binary').toString('base64'))
                }
            });
        }
    });
};

```

今回は最低限 `width` を変更しているだけだが、画像品質やその他もろもろに対応しようと思えばできる。



### IAM Roleの設定

LambdaにはS3にアクセス可能（read only）なIAM Roleを設定する。
`Create new role from template(s)` で `S3 object read-only permissions` を選ぶ。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::*"
        }
    ]
}
```

↑これと、デフォルトのLog出力用ポリシー↓の二つが付いている状態になる。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-1:************:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-1:************:log-group:/aws/lambda/ImageResizer:*"
            ]
        }
    ]
}
```

### テスト
![Kobito.aL6374.png](https://qiita-image-store.s3.amazonaws.com/0/1490/97a9ad78-82ba-dc64-2df6-0cd8a21144bf.png "Kobito.aL6374.png")


こんな感じで `w` と `filename` を渡すようにテストイベントを設定して Save and Testし、

![Kobito.sJse6F.png](https://qiita-image-store.s3.amazonaws.com/0/1490/e7f61636-93a6-f4f4-5408-9c9fb09c4730.png "Kobito.sJse6F.png")

succeededでBase64な文字列が返ってきていればたぶん大丈夫。

## API Gateway

<details><summary>Step-by-step Instruction</summary>
1. API Gatewayのマネジメントコンソールで`Create API(APIの作成)` をクリック
2. `API Name` に `Image Resizer` と入れ `Create API(APIの作成)` ボタンをクリック
3. 画面左の `Binary Support(バイナリサポート)` を開き、 `Edit(編集)` ボタンをクリックし `image/*` と入力、 `Save(保存` する
4. 画面左の `Resources(リソース)` を開き、 `Actions(アクション)` ボタンを押して `Create resource(リソースの作成)` を選択
5. `Configure as proxy resource(プロキシリソースとして設定)` にチェックを入れ、 `Resource Name(リソース名)` に `filename`、 `Resource Path(リソースパス)` に `{filename+}` と入力し、 `Enable API Gateway CORS(API Gateway CORS を有効にする)` にチェックを入れ `Create Resource(リソースの作成)` ボタンをクリック
6. 勝手にできた `ANY` メソッドは今回必要ないので `Actions` > `Delete Method`
7. 代わりに `GET` メソッドを作りたいので　`Actions` > `Create Method` > `GET` を選択しチェックマークをクリック
8. `Integration Type(統合タイプ)` は `Lambda Function Proxy(Lambda関数プロキシ)` を選択、 `Lambda Region(Lambda リージョン)` と `Lambda Function(Lambda関数)` で自分が作成した Lambda ファンクションを選んで `Save(保存)` ボタンをクリック
9. `Lambda 関数に権限を追加する` ダイアログが表示されたら `OK` 的なボタンをクリック
10. `Integration Request(統合リクエスト)` を開き、 `Use Lambda Proxy Integration(Lambdaプロキシ統合の使用)` のチェックを外す（ホントはLambda Proxy Integrationにして、レスポンスヘッダなどもLambda関数内で生成したほうが手堅い。。）
11. `Body Mapping Templates(本文マッピングテンプレート)` を開き、 `When there are no templates defined (recommended)(テンプレートが定義されていない場合 (推奨))` を選択、 `Add mapping template(マッピングテンプレートを追加)` をクリック、 `Content-Type` に `image/png` と入力してチェックマークをクリック
12. [↓の記述](http://qiita.com/akitsukada/items/e6d8fe68c49973d1edf6#lambda%E3%81%AB%E6%B8%A1%E3%81%99%E5%BC%95%E6%95%B0%E3%81%AE%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90) を参考にマッピングテンプレート内にJSONを記入、保存する
13. `Actions(アクション)` > `Deploy API(API のデプロイ)` を選択
14. `Deployment Stage(デプロイされるステージ)` に `demo` と入力し `Deploy(デプロイ)` ボタンクリック
15. 表示される `Invoke URL(URLの呼び出し)` をメモ
</details>

### バイナリサポートの設定
`image/*` を設定

![Kobito.dHLqPf.png](https://qiita-image-store.s3.amazonaws.com/0/1490/1fc35e0c-88fc-0786-31db-c30521b1cbe7.png "Kobito.dHLqPf.png")

詳しい仕様は[API Gateway公式ドキュメントを参照](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-payload-encodings.html)。

### プロキシリソースとGETメソッドの作成

今回はパスパラメータとして自由にファイル名を受け取りたいので、`/{filename+}` としてAPIにリソースを作成する。
また、最終的には `s3-ap-northeast-1.amazonaws.com` なドメインからAjaxで叩くので、CORSも有効にする。
![Kobito.A1pSGo.png](https://qiita-image-store.s3.amazonaws.com/0/1490/f68c36b2-fa72-1fc7-6181-ed6f3862daa1.png "Kobito.A1pSGo.png")

プロキシリソースとして作成すると `ANY` メソッドが勝手にできるが、今回は `GET` だけできればいいので `ANY` は消して `GET` を作ることにする。

![Kobito.eEeiz2.png](https://qiita-image-store.s3.amazonaws.com/0/1490/0731431d-9e48-e1de-0e2a-d2d349100611.png "Kobito.eEeiz2.png")

`GET` はさっき作ったLambdaファンクションにつなげておく。

![Kobito.Efybdx.png](https://qiita-image-store.s3.amazonaws.com/0/1490/dd883a51-96eb-5a7f-720f-ccdc875769a7.png "Kobito.Efybdx.png")


### Lambdaに渡す引数のマッピングテンプレートを作成

「統合リクエスト > 本文マッピングテンプレート」からこんな感じで設定する。
![Kobito.p6Sv53.png](https://qiita-image-store.s3.amazonaws.com/0/1490/9731860c-5365-d2c3-9bac-717cf5dabd14.png "Kobito.p6Sv53.png")

テンプレート本文はこう。

```json
{
 "w": "$input.params('w')",
 "filename": "$input.params('filename')"
}
```
今回、無駄にpng, jpg, gifに対応したのでそれぞれ設定。
この辺はマネジメントコンソールだけでやると冗長な感じですが、 [Swagger](http://swagger.io/) とか使ってうまいこと重複排除するといいと思います。

### APIのテスト（デプロイ前）
`GET` > メソッドリクエストの設定で、「URL クエリ文字列パラメータ」に `w`、「HTTP リクエストヘッダー」に `Content-Type` `Accept` を設定すると、API Gatewayのテスト機能で API Gateway ↔ Lambda の結合テスト的なことができる。
（が、設定していないので割愛）

### APIのデプロイ
今回は `demo` ステージにデプロイしました。
`https://kmdc2eae46.execute-api.ap-northeast-1.amazonaws.com/demo` というInvoke URL=エンドポイントが作られます。

### APIのテスト（デプロイ後）

`curl -v --request GET -H "Accept: image/*" -H "Content-Type: image/png"  'https://kmdc2eae46.execute-api.ap-northeast-1.amazonaws.com/demo/sample.png?w=150' > sample.png`

これで指定したサイズのsample.pngがローカルに落ちてくればOK.

## ここまでの問題点と解決のアプローチ

これで API Gateway ↔ Lambda ↔ S3 な画像リサイズAPIができた。
しかし、このままHTML中に 

```html
<img src="https://kmdc2eae46.execute-api.ap-northeast-1.amazonaws.com/demo/sample.png?w=150">
```
とか書いても画像は表示されない。
なぜなら **imgタグからのHTTPリクエストには `Accept` ヘッダと `Content-Type` ヘッダが含まれず、API Gatewayのバイナリサポート機能が使えないから**。

そこで今回はCloudFrontを使って強引に（？）解決してみる。

## CloudFront

問題点は `Accept` ヘッダと `Content-Type` ヘッダが API Gateway アクセス時のHTTPリクエストに含まれていないこと。
CloudFrontには [`Custom Origin Header`という機能がある](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/forward-custom-headers.html)ので、今回はそれを使ってAPI GatewayへのHTTPリクエストに欲しいヘッダを追加してみる。

<details><summary>Step-by-step Instruction</summary>
1. CloudFrontのマネジメントコンソールから、 `Create Distribution` をクリック
2. `Web` の方の `Get Started` をクリック
3. `Origin Domain Name` に API Gateway のデプロイ後にメモした `Invoke URL` のドメイン（`xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com` みたいなやつ）を入力
4. `Origin Path` に `/demo` と入力
5. `Origin Protocol Policy` を `HTTPS Only` に
6. `Origin Custom Headers` に Header Name `Accept` : Value `image/jpeg,image/png,image/gif`, Header Name `Content-Type` : Value `image/jpeg,image/png,image/gif` の2行を入力
7. `Object Caching` を `Customize` にして、 `Default TTL` を `0` にして キャッシュを無効化（テストしやすくするため）
8. `Query String Forwarding and Caching` を `Forward all, cache based on all` に（ `?w=50` のクエリストリングをOriginへの転送対象に含める）
9. `Create Distribution` ボタンをクリック
10. 待つ
</details>

### CloudFrontディストリビューションとOriginの作成
こんな感じの設定で作りました。

#### Distribution
![Kobito.KK1PQp.png](https://qiita-image-store.s3.amazonaws.com/0/1490/3317ba28-bafa-dc91-5605-9eb2cf910031.png "Kobito.KK1PQp.png")



#### Origin
![Kobito.EOuJqN.png](https://qiita-image-store.s3.amazonaws.com/0/1490/1d73d32f-a09b-b9a7-7980-4c490a8a46d7.png "Kobito.EOuJqN.png")



* Origin Domain Name に API Gateway でデプロイ済みAPIのドメイン名
* Origin Path に `/` + API Gateway でデプロイしたステージ名
* Origin SSL Protocol は HTTPS Only
* **Origin Custom Headersに以下二点をエントリ**
	* `Accept` = `image/png,image/jpeg,image/gif`
	* `Content-Type` = `image/png,image/jpeg,image/gif`

### 動作確認
これで、CloudFront を通して API Gateway にアクセスすれば `Accept` ヘッダと `Content-Type` ヘッダが付加された状態でHTTPリクエストが送られ、API Gateway がバイナリを扱ってくれるはず。

CloudFrontの作成・設定後は State が In Progress から Enabled になるまでしばらくかかるので待つ。

Enabled になったら、ブラウザで

`https://**************.cloudfront.net/sample.png?w=150`

にアクセスしてみて画像が落ちてくることを確認する。
（ドメイン名部分はさっき作ったディストリビューション情報で確認）

画像が落ちてくるんじゃなくてHTML上で確認したい場合は

```html
<!DOCTYPE html>
<html>
<head>
    <title>Test</title>
</head>
<body>
    <img src="https://**************.cloudfront.net/sample.png?w=150">
</body>
</html>
```
とか書いてローカルで開いてみて画像が表示されるか見てみる。
表示されれば成功、便利API完成。


# 注意点 & 補足

## パラメータのValidationについて

Lambdaのサンプルのところで少し書きましたが、実運用時に誰でも好きなサイズの画像を生成し放題では困る、という場合はパラメータのValidationを考慮するのがよいです。

* 参考:
	* [料理を楽しくする画像配信システム](http://www.slideshare.net/mirakui/ss-8150494/47)
	* [楽しく! 美味しく! 美しく! 画像加工サーバ Rudess（仮）](http://dev.classmethod.jp/cloud/aws/rudess-image-server/)

## API Gatewayのペイロードサイズ制限

[API Gatewayがリクエストあたりで処理できるペイロードサイズは最大10 MB](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/limits.html) なので、それを超える大きな画像はこのAPIでは扱えません。
（ただ、多くのWebサイトではそうそう10MBの画像をほいほい表示したりしてないと思います）

## CloudFront カスタムオリジンヘッダの使い方

[カスタムオリジンヘッダには様々な使い道が考えられます](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/forward-custom-headers.html)が、典型的には[オリジンへのアクセスを制限する](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/forward-custom-headers.html#forward-custom-headers-restrict-access)ことなどがあります。
利用前には公式ドキュメントをご参照ください。

## API Gatewayの前にCloudFrontを置くことについて

今回、CloudFrontの提供する機能を使いたいために、API Gatewayの前段にCloudFrontを通しています。
しかし、当然ながらCloudFrontを置くことによってPOPが増えるため、レイテンシの増加を考慮の上で利用する必要があります。

[![Kobito.0htJIz.png](https://qiita-image-store.s3.amazonaws.com/0/1490/d18d39c9-5dc5-5c1f-dbfb-b8e9fea9c863.png "Kobito.0htJIz.png")](http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-online-seminar-2016-amazon-api-gateway/76)
[AWS Black Belt Online Seminar 2016 Amazon API Gateway](http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-online-seminar-2016-amazon-api-gateway/74)


## CloudFrontのOriginとBehaviour

今回はCloudFrontを 1 Distribution - 1 Origin で設定していますが、実運用時はもう少し複雑な設定をしたくなることが多いと思います。
例えば `/img` `/css` `/js` 以下は S3 に向けて、それらへのリクエストにはカスタムオリジンヘッダーは付けず、 `/api` または `api.example.com` だけを API Gateway に向ける、など。
そういった要件を満たしたい場合、CloudFrontディストリビューションをマルチオリジンで設定し、ビヘイビアで振り分け先と条件を指定することで実現できます。

[Amazon CloudFront » 開発者ガイド » ウェブディストリビューションの使用 » キャッシュ動作の設定](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesCacheBehavior)


[AWS Black Belt Tech シリーズ 2016 - Amazon CloudFront](http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-tech-2016-amazon-cloudfront/23)


## デバッグ

### ログ

Lambda、API Gateway、CloudFrontはそれぞれログを出すことができます。（API Gateway、CloudFrontは要設定）

[AWS Lambda » Developer Guide » Lambda 関数 » 概要: Lambda 関数のビルド » プログラミングモデル » プログラミングモデル (Node.js) » ログ作成 (Node.js)](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-logging.html)

[Amazon API Gateway » 開発者ガイド » Amazon API Gateway での API のデプロイ » Amazon API Gateway でステージに API をデプロイする » ステージの設定](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-stage-settings.html)

[Amazon CloudFront » 開発者ガイド » アクセスログ](http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html)

特に、開発作業中はLambdaとAPI Gatewayのログが必要になることが多いかと思います。
その二つのログはCloudWatch Logsで参照できます。


### AWS X-Ray

re:Invent 2016で発表された[AWS X-Ray](https://aws.amazon.com/jp/xray/)も、デバッグには有用です。2016年12月15日現在、[まだプレビュー中で、誰でもサインアップすることができます](https://pages.awscloud.com/aws-xray-preview.html)。

[Amazon Web Services ブログ AWS X-Ray – 分散アプリケーションの内部を見る](https://aws.amazon.com/jp/blogs/news/aws-x-ray-see-inside-of-your-distributed-application/)


# 免責

この記事は個人の見解であり、所属する企業や団体とは関係ございません。