2017. 7. 26. 01:46ㆍ서버 프로그래밍
처음에는 AWS API Gateway와 Lambda를 같이 사용하려고 했으나, 마지막 연결하는 부분에 있어서 이해가 안되는 부분이 있어서 중단했다.
그래서 AWS Lambda의 트리거를 이용하여, 새로운 이미지가 업로드 되면 무조건 썸네일 이미지를 생성하는 방식으로 진행하였고, 완성할 수 있었다. 일부 코드를 수정하기는 했지만, 앞의 방법보다는 빠르고 쉽게 만들 수 있다.
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 파일을 업로드하면됩니다.