【3時間でできる？】AWSでS3+Lambda+API Gateway+CloudFrontのCDN構築2018

少しづつ広まってきているServerless技術を使って何かやろうとするとちょうどいい題材なのがS3に上げた画像を動的に縮小表示するというアプリです。
すなわち
https://xxx.cloudfront.net/somefile.jpeg?w=600
のようなURLでアクセスするとS3にある画像を動的に縮小して幅600pxにして表示してくれるというものです。

作るきっかけになったのは、自分のサイトで画像クチコミを載せていたのですが、圧縮すること無くそのまま上げてしまっていたので1ページ30MBみたいなサイズになって読み込み障害を起こすようになってしまったことでした。アップロード時に圧縮するのがセオリーだと思いますが、作った当時はそんなことは二の次としてたらあっという間に数が増えてしまったようです。今だとPietとかそういった選択肢もあるのかもしれません。お金があればAKAMAIやFastlyのようなCDNもアリでしょう。
それも厳しい人向けにAWSのLambdaなどのサーバレステクノロジーでやったときの内容を公開します。
以下設定画面とLambdaのソースになります。
構成はこんな感じですが、面倒そうに一瞬見えるかもしれませんが、全体通してS3の画像を圧縮して表示するシステムと思えばそれほど大変でもないです。以下の設定内容に沿ってまずはやってみることをオススメします。気合い入れたら3時間位で出来るかもです。

![AWS Networking.png](https://qiita-image-store.s3.amazonaws.com/0/1593/e9b4d005-fa95-dd14-6f21-9c51b59bad6c.png)

動作イメージを簡単に言語化すると
1. 上記URLにアクセス
2. CloudFrontが最初の受け口になってAPI Gatewayに転送
3. API GatewayからLambdaをコールしてファイル名とクエリパラメータを渡す
4. Lambdaが起動してS3の画像を取得してImageMagickを使って圧縮後、Base64形式にして出力
5. 出力した画像を再度API Gatewayの出力として書き出し
6. CloudFrontがAPI Gatewayの書き出した画像情報にContent-typeなどのヘッダ情報をつけてHTMLで表示出来るように修正
となります。要はS3の画像をそのままURLにして表示するのでなく、AWSのAPI機能を使って自動化機能であるLambdaで圧縮かけて表示しているというところでしょうか。

まずはS3の設定からです。ここにBucketを用意して画像を置きます。

## S3の設定
S3を開きます。Bucketの作成から行います。
![s3-1.png](https://qiita-image-store.s3.amazonaws.com/0/1593/4a0bec0b-0e95-f7c0-7e8d-110448a80847.png)
画像の置き場所となるfolderを作成します。ここではimagesとしています。以下参考にしてください。
このimages配下に適当な画像を置いておいてください。
![s3-2.png](https://qiita-image-store.s3.amazonaws.com/0/1593/bae696ec-a9b9-e615-b9a6-e5d43bc789b6.png)

## Lambdaの設定
次はLambdaです。functionを作成します。以下参照してください。
![lambda1.png](https://qiita-image-store.s3.amazonaws.com/0/1593/b0873682-bcdb-726b-2593-14c77f9acbad.png)

![lambda1.5.png](https://qiita-image-store.s3.amazonaws.com/0/1593/03dd2162-2711-d14c-3d49-4e4be2b8a634.png)
今回は触れませんがIAMでS3にアクセスに行くためのアカウントとRoleも作成します。S3ReadOnly権限でいいかなと思います。その情報をLambdaでも設定します。
![lambda2.png](https://qiita-image-store.s3.amazonaws.com/0/1593/8fcc6a55-bc1e-39b2-4242-491407461556.png)
![lambda3.png](https://qiita-image-store.s3.amazonaws.com/0/1593/87d4bfd1-d6ac-5ffe-eb56-9bb10694a721.png)

### Lambdaのコード
こちらはLambdaのコードです。Node.jsを想定しています。
が、ここで注意があります。Nodeのバージョンは設定可能なのですが、折角なので最新（2018年4月時点）の8.10に対応しようと思います。そうするとライブラリとして使おうとしているimagemagickをNode8.10バージョンでビルドしたものを取り込む必要があります。
その手順を書いてもいいですが、結構面倒で挫折してしまうと思われるので、私の方でビルドしたものをGithubにあげておきました。
https://github.com/hardreggaecafe/resizeimage_im_node8.10
使い方は簡単です。 `git clone` してindex.jsを書き換えた後にindex.jsとnode_moduleディレクトリを `zip -r` で固めます。これをLambdaのFunction code内のCode Entry TypeからUploadするだけです。zipファイルが10MB以内ならここから直接あげられますが、10MB超えるときには一度S3にzipファイルを置いて、そのパスを指定する必要があります。今回は恐らく10MBにギリギリ収まるかと思います。

![lambda4.png](https://qiita-image-store.s3.amazonaws.com/0/1593/87267497-885d-db6b-9aab-1699a0587dc3.png)

こちらがindex.jsのコードです。適宜必要な箇所は書き換えてください。

```node.js:index.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) => {
    //console.log('Received event:', JSON.stringify(event, null, 2));

    // Get the object from the event and show its content type
    const filename = event.path.replace(/^\//, "");
    var bucket = 'some-bucket';

    const width = event.queryStringParameters.w;
    console.log("file="+filename+" bucket="+bucket);
    const params = {
        Bucket: bucket,
        Key: 'images/' +filename
    };
    s3.getObject(params, (err, data) => {
        if (err) {
            console.log(err);
            var message = "Error getting object " + filename + " from bucket " + bucket +
                ". Make sure they exist and your bucket is in the same region as this function.";
            context.fail(message);
        } else {
            var contentType = data.ContentType;
            var extension = contentType.split('/').pop();
            
            im.resize({
                srcData: data.Body,
                format: extension,
                width: width
            }, function(err, stdout, stderr) {
                if(err) {
                    context.fail(err);
                    return;
                }
                var contentType = filename.endsWith('jpg') ? "image/jpeg" : "image/png";
                callback(null, {
                    "isBase64Encoded": true,
                    "statusCode": 200,
                    "headers": { "Content-Type": contentType },
                    "body": new Buffer(stdout, 'binary').toString('base64')
                });
            });
        }
    });
};
```

## API Gateway の設定
次はAPI Gatewayの設定です。
手順の大まかなイメージは
1. filenameというresourceを作成する
2. GETのmethodを作成する
3. Method RequestのところにPath, query parameterとheaderの設定を入れる
4. Integration RequestでProxy設定やLambdaの設定を入れる
5. StageにAPIを上げる
6. その他の細かい設定（パラメータを受け渡し方法やCloudWatchへのログ吐き出しなど）
7. API GatewayとLambdaをまとめてテスト（※）
となります。

早速APIを作成します。
![api_gateway1.png](https://qiita-image-store.s3.amazonaws.com/0/1593/04ac8503-be1f-c060-98ab-2f1065ee8f3c.png)

Resource、MethodそしてStageはここから作成。
![api_gateway2.png](https://qiita-image-store.s3.amazonaws.com/0/1593/bce27964-02b4-86f1-b45a-84000d584b2d.png)

Resourceの作成方法はこちら。
![api_gateway3.png](https://qiita-image-store.s3.amazonaws.com/0/1593/c7383441-62fc-3381-bb34-51e4671e6673.png)
MethodはGETを指定してMethod Requestの箇所とIntegration Requestをそれぞれ以下のように設定します。
![api_gateway4.png](https://qiita-image-store.s3.amazonaws.com/0/1593/678c9b15-9545-1d5c-1de6-b7018a62e49e.png)

![api_gateway5.png](https://qiita-image-store.s3.amazonaws.com/0/1593/0e7246a9-778e-52f2-48f4-ca84be129fdb.png)

![api_gateway6.png](https://qiita-image-store.s3.amazonaws.com/0/1593/bf116be0-5934-3656-d64e-858633653026.png)

ここからはStageの設定。
![api_gateway7.png](https://qiita-image-store.s3.amazonaws.com/0/1593/fa054719-88d0-655a-e42c-e61a6dfefcac.png)
StageのLogの設定
![api_gateway8.png](https://qiita-image-store.s3.amazonaws.com/0/1593/18534646-82e8-17e8-b334-b981c3a9603c.png)
最後にAPIGateway全体の設定。
![api_gateway9.png](https://qiita-image-store.s3.amazonaws.com/0/1593/607291f5-6b45-5114-ed0a-753e78c442fc.png)

でここまで作ったら本当に動くか確認したくなるのが人情。APIGatewayとLambdaをまとめてテストします。
### API Gateway+Lambda+S3のテスト
GETの設定でこういうのがあります。
![api_gateway9.5.png](https://qiita-image-store.s3.amazonaws.com/0/1593/92732d21-2958-57d9-b4bd-d71a0fc902a7.png)
![api_gateway10.png](https://qiita-image-store.s3.amazonaws.com/0/1593/c4ee3061-5f86-325a-61bf-bb3568567444.png)

こんな風にfilenameと幅指定(w)とヘッダ情報を入力してテストします。２００とバイナリが返ってくれば成功です。ダメな場合何処かで失敗しています。CloudWatchのログの見方も下につけたのでこちらも参照しながら調査してみてください。

## CloudFrontの設定
最後はCloudFrontです。Distributionｗ作成します。
![cloud_front1.png](https://qiita-image-store.s3.amazonaws.com/0/1593/2ffd4f4e-970e-49fa-69d4-8c6bbf1e3c12.png)
General情報とOrigin情報を以下のように設定します。ここにAPIGatewayのURLが必要になってきます。
![cloud_front2.png](https://qiita-image-store.s3.amazonaws.com/0/1593/f4774b3f-8e63-7767-ab2e-5685128973c6.png)

![cloud_front3.png](https://qiita-image-store.s3.amazonaws.com/0/1593/fb81ccbf-3254-2389-2783-d29c6a8d632e.png)
![cloud_front4.png](https://qiita-image-store.s3.amazonaws.com/0/1593/cdc3eb20-6104-b2d7-b0c7-97d578a3fef1.png)

Behaviorではキャッシュ情報とQueryStringの設定を行います。
![cloud_front5.png](https://qiita-image-store.s3.amazonaws.com/0/1593/21cde0e1-5b16-f718-007c-74c656c8c1b9.png)
![cloud_front6.png](https://qiita-image-store.s3.amazonaws.com/0/1593/ac90dbea-3245-4152-f3f7-e652842ac5e2.png)

ここまで設定したらGeneralの設定情報にDomainNameという項目があるので
https://xxx.cloudfront.net/somefile.jpeg?w=600
のようなURLをブラウザから入力してみてください。上手く行けばS3の置いている画像が圧縮されて表示されるはずです。
上手く行かない場合は以下のCloudWatchのログを見て何が問題かを調べてみてください。
ここに吐き出されない場合は今までの設定の何処かが間違っています。ログ出力の設定を見直してみてください。

## CloudWatchでログを見る方法
動作ログ＆デバッグログを確認する方法としてはCloudWatchのログ欄を見るということになります。
以下を参照ください。
![clowdwatch1.png](https://qiita-image-store.s3.amazonaws.com/0/1593/6e66b661-c0e0-f6f4-660c-6f1731392a89.png)
![clowdwatch2.png](https://qiita-image-store.s3.amazonaws.com/0/1593/886bd472-1f84-8bd5-d3e7-7cf6611025db.png)
![clowdwatch3.png](https://qiita-image-store.s3.amazonaws.com/0/1593/585119b9-0efd-5f61-ec56-5902a9319629.png)

## 結果
![resize.jpg](https://qiita-image-store.s3.amazonaws.com/0/1593/6d7bef8e-a2af-158e-338f-10bde2d30507.jpeg)
劇的に縮小できました。が、スマホで上げた画像はExifという画像の属性情報の読み取りが上手く行かない関係で上下を正しく認識しないままになってしまいます。これはとりあえず次回の課題ですが縮小には成功しました。

気になる費用ですが、20日間ほど運用して
- API Gateway $48（23万アクセスで、1.5GBのキャッシュ利用）
- CloudFront $6.6
- S3 $1
- Lambda $0

なので占めて月間想定で$83ほど。Fastlyに月額20万も払うことを考えたら遥かに安いです。あっちはExifを認識して自動で回転（AutoOrient）機能とか色々ついているから一概に比較できないですが・・・。
といいつつ実はImageMagickよりも5倍早く圧縮し、AutoOrient機能もついているSharpというNodeModuleを組み込んだバージョンも作りました。その話は次回に書きます。お役に立てれば幸いです。

## 続きはこちらで
AWS・LambdaにSharpモジュールで画像圧縮を高機能に
https://qiita.com/hardreggaecafe/items/5acd30d42cc59534d72c