AWS Lambdaを使って敢えて静的に画像をリサイズする

動的リサイズについて世の中にはびっくりするほど沢山のいい記事があります。

<a href="http://tech.speee.jp/entry/2016/07/29/112616" target="_blanck">サーバーレスアーキテクチャで画像の動的リサイズをしたら優勝しました 【SpeeeKaigiレポート#1】</a>

<a href="https://qiita.com/arairyoya/items/8ef3a1653394671a3503" target="_blanck">サーバレスにサムネイル画像を配信する試み</a>

などなど...

`リクエスト <-> CDNでキャッシュ <-> ImageMagickでリサイズ <-> s3`

基本的にこんな感じで実装されているわけですが、そこまでトラフィックがない場合はここまでやらなくてもいいかな―とも思ったりします。
(CDNのヒット率があまり高くないとか、そもそも試験導入したいとか)

でもまぁ、検索画面とかの商品サムネイル画像がオリジナルサイズだとSEO的にもよろしくない...
ということで手っ取り早くサムネイル画像を生成すべく、s3への画像アップロードをトリガーにして複数サイズのサムネイルを生成する処理をサクッと書いてみました。

## Lambdaで関数を作る

ここをポチッと
<img width="1889" alt="Lambda_Management_Console_🔊.png" src="https://qiita-image-store.s3.amazonaws.com/0/217483/b92c196a-a91c-4956-74cb-40f39c9f11e9.png">
<br>
ここのロールはs3へ書き込みできるものを設定しておく必要があります。
(参考: <a href="https://hotchpotchj37.wordpress.com/2017/03/08/aws-sdk-node-js-%E3%81%A7-s3-putobject%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AB%E6%B0%97%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B%E3%81%93%E3%81%A8/">AWS SDK (Node.js) で S3 putObjectするときに気をつけること</a>)
![Lambda_Management_Console_🔊.png](https://qiita-image-store.s3.amazonaws.com/0/217483/c817153d-789f-7b87-b3b8-2e5d5a8d2280.png)
<br>
ポチポチっとトリガーを設定
![Lambda_Management_Console_🔊.png](https://qiita-image-store.s3.amazonaws.com/0/217483/92343721-6000-6ca5-6c2e-ebe3cfd4ba75.png)
<br>
サムネイルの元画像がアップロードされるバケットを選択して、イベントタイプを「オブジェクトの作成(すべて)」に設定し追加します。
![Lambda_Management_Console_🔊.png](https://qiita-image-store.s3.amazonaws.com/0/217483/29cfa481-da91-1b3b-dd98-ba48a97a3be5.png)
<br>
関数を選んで
![Lambda_Management_Console_🔊.png](https://qiita-image-store.s3.amazonaws.com/0/217483/012604ef-dc55-8d26-984c-21bbc0453d35.png)
<br>
コードを書いていきます
![Lambda_Management_Console_🔊.png](https://qiita-image-store.s3.amazonaws.com/0/217483/8eb5ecdf-23e1-7562-936a-7b8d40dbf750.png)


参考: <a href="https://qiita.com/awm-kaeruko/items/00d92cf2484405fb5579">AWS Lambdaを使ってS3にアップロードされた画像をリサイズする</a>
こちらのソースを参考にさせていただきました。

今回は、<strong>幅50~500pxで50px刻みのサムネイルを作る</strong>という要件で始めたので幾つか変更しています。

```javascript:lambda
'use strict';

console.log('Loading function');

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


exports.handler = (event, context, callback) => {
    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
    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('CONTENT TYPE:', data.ContentType);
            callback(null, data.ContentType);
            var contentType = data.ContentType;
            var extension = contentType.split('/').pop();
            console.log(extension);
            console.log("file name:" + key);

            if (key.split('.')[0].indexOf("-thumbnail-") == -1) {
                im.resize({
                    srcData: data.Body,
                    format: extension,
                    width: 500
                }, function(err, stdout, stderr) {
                    if (err) {
                        context.done('resize failed', err);
                    } else {
                        var thumbnailKey = key.split('.')[0] + "-thumbnail-500." + key.split('.')[1];

                        s3.putObject({
                            Bucket: bucket,
                            Key: thumbnailKey,
                            Body: new Buffer(stdout, 'binary'),
                            CacheControl: 'max-age=604800',
                            ContentType: contentType
                        }, function(err, res) {
                            if (err) {
                                console.log("error putting object 500");
                                context.done('error putting object', err);
                            } else {
                                callback(null, "success putting object");
                                console.log("success putting object 500");
                            }
                        });
                    }
                });
            } else {
                var size = parseInt(key.split('.')[0].split('-thumbnail-')[1]);
                console.log("input size: " + size);
                var targetSize = (size - 50);
                console.log("target size: " + targetSize);
                if (targetSize > 0) {
                    im.resize({
                        srcData: data.Body,
                        format: extension,
                        width: targetSize
                    }, function(err, stdout, stderr) {
                        if (err) {
                            context.done('resize failed', err);
                        } else {
                            var thumbnailKey = key.split('.')[0].split('-thumbnail-')[0] + "-thumbnail-" + targetSize + "." + key.split('.')[1];

                            s3.putObject({
                                Bucket: bucket,
                                Key: thumbnailKey,
                                Body: new Buffer(stdout, 'binary'),
                                CacheControl: 'max-age=604800',
                                ContentType: contentType
                            }, function(err, res) {
                                if (err) {
                                    console.log("error putting object " + targetSize);
                                    context.done('error putting object', err);
                                } else {
                                    callback(null, "success putting object");
                                    console.log("success putting object " + targetSize);
                                }
                            });
                        }
                    });
                }
            }
        }
    });
};

```

まず、画像の生成をトリガーにしてLambdaが起動するため、サムネイルの作成自体(`s3.putObject()`)でもこの処理が実行されてしまいます。
そのため、まず`if (key.split('.')[0].indexOf("-thumbnail-") == -1)`で今回がサムネイルの作成によって発火したものかを判定します。


また、初回のサムネイル作成では

```
im.resize({
    srcData: data.Body,
    format: extension,
    width: 500
}
```

と指定して500pxの画像を生成し、その後はファイル名からサイズを判定して50pxずつ小さくする、という方法を取っています。
（Javascriptで10回分べた書きしたりループ回したりではなく、コールバックをs3のオブジェクト生成をトリガーにするイメージでやりました。)

その後はテストを軽く書いておしまいです。

## 実行結果
実際にアップロードしてみるとサムネイルが作成されているのがわかります。
![S3_Management_Console_🔊.png](https://qiita-image-store.s3.amazonaws.com/0/217483/b17099a1-8528-1cc8-c060-a3c1778d4655.png)


## まとめ
動的リサイズも良いのですが、そこまでサムネイルサイズのパターンが多くなく、おためしで導入する場合はこちらでも良いかもしれません。

既存のアップロードされているファイルも
`BUCKET="my-bucket"; aws s3 ls s3://$BUCKET --recursive | grep -v "\-thumbnail\-" | awk -v BUCKET="${BUCKET}" '{system("aws s3api copy-object --bucket " BUCKET " --copy-source " BUCKET "/" $4 " --key " $4 " --metadata-directive REPLACE --cache-control \"max-age=86400\" --content-type \"image/jpeg\"")}'`
こんな感じでaws-cliからコピーし直してあげれば全てサムネイルが生成できます。
(参考: <a href="https://dev.classmethod.jp/cloud/aws/edit-s3-cache-control-onliner/" target="_blank">S3バケットに含まれる全オブジェクトのCache-Controlを書き換えるワンライナー</a>)