S3+Lambdaで画像保存時にリサイズされた画像も同時に保存する

Line Messaging APIを使ってユーザーに画像を送信する処理を実装したんですが、Line Messaging APIを使って画像を送る際にはoriginalサイズの画像をresizeされた画像両方のURLが必要になります。そのために、originalサイズの画像を保存するバケットとresizeサイズの画像を保存するバケットを作成して、originalバケットに画像が入った時に、lambdaを実行させてresizeバケットにresizeされた画像を保存するという一連の処理を実装したので、手順を書き記します。

全体図確認

全体的な流れは以下の図のような感じです。今回の記事ではoriginalバケットに画像を保存する過程は省き、「originalバケットに画像が保存されたことをトリガーとしてlambda実行=>resizeバケットにリサイズ された保存される」流れについて解説していきます。

S3でバケットを二つ作成する

originalサイズの画像を保存するようのバケットと、resize画像を保存するようのバケットの二つを作成します。命名は以下のようにしておくと、後々楽になります。

originalバケット「hoge」

previewバケット「hoge-review」

Lambda関数を作成して、トリガーにS3を指定

Lambbda関数を作成します。一から作成するのではなくて、設計図を使用して作成しましょう。s3-get-objectという設計図を利用します。

そのあとに実行ロールを作成して、Lambda関数がS3にアクセスする権利を持たせます。

そしてトリガーとして、S3に作成したoriginalバケットを指定します。イベントタイプを「すべてのオブジェクト作成イベント」にすることで、S3に画像が追加された瞬間にLambdaが実行されるという設定が完了します。

Lambdaに処理を記述する

実は似たようなチュートリアルをAWSがすでに用意してくれているので、それを参考にしながらlambda処理を記述していきます。以下のようにLambda関数をかけば、originalバケットに画像が追加された時に、リサイズされた画像がresizeバケットに入るはずです。

AWSチュートリアル: https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html

// dependencies
var async = require('async');
var AWS = require('aws-sdk');
var gm = require('gm')
.subClass({ imageMagick: true }); // Enable ImageMagick integration.
var util = require('util');

// constants
// => ここに画像をリサイズした後の画像サイズを記述する!
var MAX_WIDTH = 100;
var MAX_HEIGHT = 100;

// get reference to S3 client
var s3 = new AWS.S3();

exports.handler = function(event, context, callback) {
// Read options from the event.
console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
var srcBucket = event.Records[0].s3.bucket.name;
// Object key may have spaces or unicode non-ASCII characters.
var srcKey =
decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
// destination bucketという意味、srcBucket == hogeである。dstBucketにはresize画像を保存したい先のバケットを記述する。
var dstBucket = srcBucket + "resize";
// destination keyという意味、srcKey == 画像のファイル名である。dstKeyにはresize画像の名前を記述する。
var dstKey = "preview-" + srcKey;

// Sanity check: validate that source and destination are different buckets.
if (srcBucket == dstBucket) {
callback("Source and destination buckets are the same.");
return;
}

// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
callback("Could not determine the image type.");
return;
}
var imageType = typeMatch[1].toLowerCase();
if (imageType != "jpg" && imageType != "png") {
callback(`Unsupported image type: ${imageType}`);
return;
}

// Download the image from S3, transform, and upload to a different S3 bucket.
async.waterfall([
function download(next) {
// Download the image from S3 into a buffer.
s3.getObject({
Bucket: srcBucket,
Key: srcKey
},
next);
},
function transform(response, next) {
gm(response.Body).size(function(err, size) {
// Infer the scaling factor to avoid stretching the image unnaturally.
var scalingFactor = Math.min(
MAX_WIDTH / size.width,
MAX_HEIGHT / size.height
);
var width = scalingFactor * size.width;
var height = scalingFactor * size.height;

// Transform the image buffer in memory.
this.resize(width, height)
.toBuffer(imageType, function(err, buffer) {
if (err) {
next(err);
} else {
next(null, response.ContentType, buffer);
}
});
});
},
function upload(contentType, data, next) {
// Stream the transformed image to a different S3 bucket.
s3.putObject({
Bucket: dstBucket,
Key: dstKey,
Body: data,
ContentType: contentType
},
next);
}
], function (err) {
if (err) {
console.error(
'Unable to resize ' + srcBucket + '/' + srcKey +
' and upload to ' + dstBucket + '/' + dstKey +
' due to an error: ' + err
);
} else {
console.log(
'Successfully resized ' + srcBucket + '/' + srcKey +
' and uploaded to ' + dstBucket + '/' + dstKey
);
}
callback(null, "message");
}
);
};

ここまでかけたらlambda関数を保存して、orignalのS3のバケットにドラックアンドドロップで任意の画像を追加してみましょう。追加した後に、resizeバケットにリサイズされた画像が挿入されていれば、実装完了です!