AWS Lambda를 이용한 썸네일 이미지 생성 기능 구현

2017. 7. 26. 01:46서버 프로그래밍


처음에는 AWS API Gateway와 Lambda를 같이 사용하려고 했으나, 마지막 연결하는 부분에 있어서 이해가 안되는 부분이 있어서 중단했다.

http://how2marry.com/2016/06/21/%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4-%EC%8D%B8%EB%84%A4%EC%9D%BC-%EC%83%9D%EC%84%B1-api-gateway-lambda-s3-cloudfront/

imgResize


그래서 AWS Lambda의 트리거를 이용하여, 새로운 이미지가 업로드 되면 무조건 썸네일 이미지를 생성하는 방식으로 진행하였고, 완성할 수 있었다. 일부 코드를 수정하기는 했지만, 앞의 방법보다는 빠르고 쉽게 만들 수 있다.

https://medium.com/n42-corp/aws-lambda%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%8D%B8%EB%84%A4%EC%9D%BC-%EC%83%9D%EC%84%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-acc278d49980

AWS Lambda를 이용한 이미지 리사이즈는 AWS에서도 예제로 제공해줍니다 ~ AWS 에서는 원본과 썸네일 S3 버킷을 별도로 나누라고 했었는데 저희는 리사이즈 하지 않는 경우도 있고해서 S3 버킷 하나에 원본과 썸네일을 저장하기로 했습니다.(근데 이거 실수하면 엄청 무시무시합니다…. 무한 루프가 돌 수도 있어서 조심해야 됩니다)

AWS에서 제공하는 예제는 실제 서비스에서 사용하기에는 부족한 면도 있고 Node.js 최신버전도 아니라서 다른 분들에게 도움이 될까해서 소스코드를 첨부합니다. 내용이 길지만, 중간에 잘리면 도움이 안될것 같아서 쭉 붙여넣습니다. 아래 코드를`index.js` 파일로 저장합니다.

'use strict';

let aws = require('aws-sdk');
let s3 = new aws.S3({ apiVersion: '2006-03-01' });
let async = require('async');
let gm = require('gm')
.subClass({ imageMagick: true });

const supportImageTypes = ["jpg", "jpeg", "png", "gif"];
const ThumbnailSizes = {
PROFILE: [
{size: 80, alias: 's', type: 'crop'},
{size: 256, alias: 'm', type: 'crop'},
{size: 640, alias: 'l', type: 'crop'}
],
ARTICLE: [
{size: 192, alias: 's'},
{size: 1280, alias: 'l'}
],
MESSAGE: [
{size: 1280, alias: 'l'}
],
BUSINESS_ARTICLE_THUMB: [
{size: 192, alias: 's', type: 'crop'}
],
sizeFromKey: function(key) {
const type = key.split('/')[1];
if (type === 'article') {
return ThumbnailSizes.ARTICLE;
} else if (type === 'profile') {
return ThumbnailSizes.PROFILE;
} else if (type === 'message') {
return ThumbnailSizes.MESSAGE;
} else if (type === 'business_article_thumb') {
return ThumbnailSizes.BUSINESS_ARTICLE_THUMB;
}
return null;
}
}

function destKeyFromSrcKey(key, suffix) {
return key.replace('origin/', `resize/${suffix}/`)
}

function resizeAndUpload(response, size, srcKey, srcBucket, imageType, callback) {
const pixelSize = size["size"];
const resizeType = size["type"];

function resizeWithAspectRatio(resizeCallback) {
gm(response.Body)
.autoOrient()
.resize(pixelSize, pixelSize, '>')
.noProfile()
.quality(95)
.toBuffer(imageType, function(err, buffer) {
if (err) {
resizeCallback(err);
} else {
resizeCallback(null, response.ContentType, buffer);
}
});
}

function resizeWithCrop(resizeCallback) {
gm(response.Body)
.autoOrient()
.resize(pixelSize, pixelSize, '^')
.gravity('Center')
.extent(pixelSize, pixelSize)
.noProfile()
.quality(95)
.toBuffer(imageType, function(err, buffer) {
if (err) {
resizeCallback(err);
} else {
resizeCallback(null, response.ContentType, buffer);
}
});
}

async.waterfall(
[
function resize(next) {
if (resizeType == "crop") {
resizeWithCrop(next)
} else {
resizeWithAspectRatio(next)
}
},
function upload(contentType, data, next) {
const destKey = destKeyFromSrcKey(srcKey, size["alias"]);
s3.putObject(
{
Bucket: srcBucket,
Key: destKey,
ACL: 'public-read',
Body: data,
ContentType: contentType
},
next
);
}
], (err) => {
if (err) {
callback(new Error(`resize to ${pixelSize} from ${srcKey} : ${err}`));
} else {
callback(null);
}
}
)
}

exports.handler = (event, context, callback) => {
const bucket = event.Records[0].s3.bucket.name;
const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
// Lambda 타임아웃 에러는 로그에 자세한 정보가 안남아서 S3 파일 이름으로 나중에 에러처리하기위해 에러를 출력하는 코드
const timeout = setTimeout(() => {
callback(new Error(`[FAIL]:${bucket}/${key}:TIMEOUT`));
}, context.getRemainingTimeInMillis() - 500);

if (!key.startsWith('origin/')) {
clearTimeout(timeout);
callback(new Error(`[FAIL]:${bucket}/${key}:Unsupported image path`));
return;
}

const params = {
Bucket: bucket,
Key: key
};
const keys = key.split('.');
const imageType = keys.pop().toLowerCase();
if (!supportImageTypes.some((type) => { return type == imageType })) {
clearTimeout(timeout);
callback(new Error(`[FAIL]:${bucket}/${key}:Unsupported image type`));
return;
}

async.waterfall(
[
function download(next) {
s3.getObject(params, next);
},
function transform(response, next) {
let sizes = ThumbnailSizes.sizeFromKey(key);
if (sizes == null) {
next(new Error(`thumbnail type is undefined(allow articles or profiles), ${key}`));
return;
}
async.eachSeries(sizes, function (size, seriesCallback) {
resizeAndUpload(response, size, key, bucket, imageType, seriesCallback);
}, next);
}
], (err) => {
if (err) {
clearTimeout(timeout);
callback(new Error(`[FAIL]:${bucket}/${key}:resize task ${err}`));
} else {
clearTimeout(timeout);
callback(null, "complete resize");
}
}
);
};

위 코드는 ‘origin/profile’ 폴더 밑에 이미지가 올라오면 지정된 사이즈에 따라 `resize/l/profile`, `resize/m/profile`, `resize/s/profile` 폴더에 썸네일을 생성후 저장합니다. 썸네일 사이즈, 경로, 크롭 옵션은 코드 첫부분에서 정의해두었고 이것을 사용합니다.(profile 뿐만 아니라 article, message 등 여러 가지 타입이 있습니다)

AWS Lambda 런타임은 Node.js 4.3 이며 코드를 실행하기 위해서는 async, gm 모듈을 설치해야 합니다. `npm install async`, `npm install gm` 명령어를 입력하면 현재 폴더의 `node_modules` 폴더에 설치됩니다.

위 코드를 저장한 파일과 node_modules 디렉토리를 하나로 압축해서 Lambda에 업로드 하면 되는데요. 이것을 쉽게 하기 위해 `zip.sh` 파일을 만들었습니다

#!/bin/sh
zip -r lambda index.js node_modules

zip.sh 파일을 실행하면 lambda.zip 파일이 생성되고 zip 파일을 업로드하면됩니다.