안녕하세요. 올리브영에서 Back-end 개발 업무를 담당하고 있는 불광동꿀주먹🥊, 길만자 입니다.
올리브영에서는 여러 신규프로젝트들을 진행 중입니다. 최근 상품프로젝트에서는 새로운 상품에 대한 이미지 업로드 기능을 개발했습니다.
이 프로젝트에서는 상품의 기본 이미지, 썸네일 이미지, 전시 이미지 등, 각각의 용도에 맞는 이미지 리사이징 기능이 필요했습니다.
기존의 리사이징 방식은 서버에 임시파일을 생성하여 리사이징하는 방법이었지만, 서버부하, 속도, 요금 측면에서 더 나은 대안을 찾고자 고민하게 되었습니다.
그래서 저희는 AWS Lambda를 활용하여 이미지 리사이징 기능을 구현하기로 결정했습니다.
이를 통해 이미지 저장소인 S3를 최대한 효율적으로 이용할 수 있게 되었습니다.
이 글에서는 어떻게 AWS Lambda를 활용하여 이미지 리사이징을 구현했는지에 대해 자세히 설명해드리고자 합니다!
Lambda를 사용하여 S3 이미지 리사이징하기
이미지 파일이 AWS SDK를 통해 S3에 업로드 된 순간, 트리거를 사용하여 이미지 리사이징을 하는 방식을 사용했습니다.
리사이징에 필요한 코드나 전체적인 과정은 AWS 공식 가이드 를 참고하여 진행했습니다.
이미지를 업로드하고 리사이징 된 파일을 저장하는 S3 버킷을 생성합니다
![001 001](/static/304f7eb35691924aa271cd9048c95b8f/617a2/001.png)
실습을 이렇게 진행하면 S3에 업로드 된 파일을 URL로 바로 확인이 불가능하고, 다운로드 과정이 필요하기 때문에 간단하게 실습하실 예정이시라면 퍼블릭으로 설정하시는 것을 추천드립니다!
이미지가 업로드 될 때마다 실행 될 함수를 정의하기 위해 Lambda를 생성합니다
![002 002](/static/31b838d57b7e0a7ede99f1c1eaee5731/617a2/002.png)
S3 업로드를 위한 IAM 정책을 생성합니다
저는 Lambda에서 S3를 읽고 쓰는 권한이 필요한 작업을 수행하기 때문에 별도로 역할을 생성했습니다.
해당 정책은 실습을 위해 생성한 S3에 대한 GET 및 PUT 권한을 포함하고 있습니다.
만약 S3의 전체 권한을 부여하고 싶다면 AmazonS3FullAccess를 추가하시면 됩니다.
![003 003](/static/5416470381edc1a107450d25e45436cf/a85d4/003.png)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::{버킷이름}/*"
}
]
}
생성한 Lambda 함수에 해당 S3 정책을 연결하고 설정을 수정합니다
업로드 함수가 동작하는 데에 시간이 다소 소요될 수 있으므로, 일반 구성 → 편집의 제한 시간을 50초로 늘려보았습니다. 또한, Lambda 함수를 생성할 때 함께 생성되는 기본 역할이 아닌 기존의 역할을 사용하시려면 아래에서 역할을 변경해주시면 됩니다!
저는 기본 역할에 S3 역할을 추가할 예정이므로 해당 부분은 생략하겠습니다.
![004 004](/static/5365c23ce18b9feef108ae98b5a4a3b2/e7d8e/004.png)
구성 → 권한의 편집을 통해서 역할에 S3 권한을 추가합니다
![005 005](/static/8e744a96fa08592a66487ab3689a82d5/134d7/005.png)
![006 006](/static/e38dcab997f92b4b9b4f31b5d502f9e7/ed887/006.png)
정책 연결을 클릭하여 이 전에 생성한 S3 권한을 추가합니다
이제 Lambda 함수가 S3에 접근하고 업로드할 수 있는 권한이 부여되었습니다.
S3에 이미지가 업로드될 때 함수를 실행시키기 위한 트리거를 생성합니다
함수 개요에 있는 트리거 추가를 클릭합니다.
![008 008](/static/457225649f12241ae19cc8579e2a4520/0600e/008.png)
트리거 이벤트를 추가할 버킷을 선택하고 Event type을 선택합니다. 이미지가 업로드될 때 트리거로 동작하길 원하므로 PUT으로 설정하였습니다.
![009 009](/static/f31b38d79c37f05d4553f92c8a2ed9b5/51f47/009.png)
함수 코드를 작성합니다
AWS 링크에 첨부된 예제 파일을 사용했습니다.
![010 010](/static/e2befe2d0b1c5f820dbd35ea99641665/07220/010.png)
aws 이미지 리사이징 코드
// dependencies
const AWS = require('aws-sdk');
const util = require('util');
const sharp = require('sharp');
// get reference to S3 client
const s3 = new AWS.S3();
exports.handler = async (event, context, callback) => {
// Read options from the event parameter.
console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
const srcBucket = event.Records[0].s3.bucket.name;
// Object key may have spaces or unicode non-ASCII characters.
const srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
const dstBucket = {사용할 버킷명};
const dstKey = {파일 이름}; // 리사이징 된 이미지가 저장될 파일 이름. 폴더를 사용한다면 폴더명도 추가해준다 ex."{폴더명}/" + fileName;
// Infer the image type from the file suffix.
const typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
console.log("Could not determine the image type.");
return;
}
// Check that the image type is supported
const imageType = typeMatch[1].toLowerCase();
if (imageType != "jpg" && imageType != "png") {
console.log(`Unsupported image type: ${imageType}`);
return;
}
// srcBucket에 업로드된 이미지를 가져온다
try {
const params = {
Bucket: srcBucket,
Key: srcKey
};
var origimage = await s3.getObject(params).promise();
} catch (error) {
console.log(error);
return;
}
// set thumbnail width. Resize will set the height automatically to maintain aspect ratio.
const width = 200;
// Use the sharp module to resize the image and save in a buffer.
try {
var buffer = await sharp(origimage.Body).resize(width).toBuffer();
} catch (error) {
console.log(error);
return;
}
// 다시 dstBucket, dstKey에 지정한 경로로 업로드를 진행한다
try {
const destparams = {
Bucket: dstBucket,
Key: dstKey,
Body: buffer,
ContentType: "image"
};
const putResult = await s3.putObject(destparams).promise();
} catch (error) {
console.log(error);
return;
}
console.log('Successfully resized ' + srcBucket + '/' + srcKey +
' and uploaded to ' + dstBucket + '/' + dstKey);
};
로컬에서 이미지 리사이징 코드를 실행하고, npm init
을 통해 package.json
파일을 생성한 후 npm install
을 실행하여 필요한 모듈을 설치해야 합니다.
그러나 개별적으로 코드를 넣고 모듈을 추가하는 대신, 코드와 모듈을 하나의 zip 파일로 묶어서 코드 업로드를 진행해도 됩니다.
{
"name": "lambda-s3-trigger",
"version": "1.0.0",
"description": "Lambda 이미지 리사이징",
"main": "index.js",
"author": "inpa",
"license": "ISC",
"dependencies": {
"aws-sdk": "^2.1299.0",
"sharp": "^0.31.3"
}
}
zip 파일로 함께 node_modules을 올리지 않는 경우에는 Layer에 필요한 모듈들을 업로드하는 과정이 필요합니다.
Lambda의 Layer는 추가 코드 또는 기타 콘텐츠를 포함할 수 있는 .zip 파일입니다. 계층에는 라이브러리, 사용자 정의 런타임, 데이터, 구성 파일이 포함될 수 있습니다.
버전별로 관리되며 변화 가능성이 높은 경우에는 버전을 관리할 수 있는 Layer를 사용하는 것이 편리합니다. 계층을 사용하면 업로드된 배포 아카이브의 크기가 줄어들고 코드를 빠르게 배포할 수 있는 장점이 있습니다. 따라서, 저는 이러한 이점을 고려하여 Layer 방식을 선택하여 작업을 진행했습니다.
node_modules 계층 양식을 참고하여 로컬에서 폴더 계층 구조를 만들어서 zip 파일로 생성하면 준비가 완료됩니다 🎉 AWS의 설명에 따르면 Node Js의 경우에는 nodes/node_modules의 구성이 필요하므로 해당 구조를 그대로 유지하여 zip파일로 압축하여 Layer에 업로드했습니다.
계층 추가로 들어가서 layer 생성을 클릭하여 계층을 생성합니다
![011 011](/static/3114c2cb487163469e000abc74cedb81/51b9f/011.png)
![012 012](/static/22af50392c4d403ef38bbfc08b15b512/8d021/012.png)
![013 013](/static/a005f4b5b45cf49813d15c9d41ebc86f/9104d/013.png)
계층을 생성 후 Lambda 함수에 계층을 추가합니다
이렇게 Lambda 함수 설정과 코드 실행에 대한 설정이 끝났습니다 👏👏👏
![014 014](/static/ed8fa17d39bbe823ca7bff856185f26a/51f47/014.png)
mac에서 압축하는 법 : mac을 사용한 압축방식은 파일이 누락될 확률이 있어서 명령어로 진행
zip -r nodejs nodejs
만일 sharp 버전 문제가 나온다면 해당 명령어를 실행합니다.
rm -rf node_modules/sharp
npm install –arch=x64 –platform=linux –target=16x sharp
함수 설정이 끝났으니 테스트를 진행해보겠습니다.
S3에 직접 이미지를 업로드 하여 테스트할 수 있지만 Lambda 함수에서 제공하는 테스트를 사용하여 진행할 수도 있습니다.
테스트 샘플에서 제공하는 S3 Put을 기본적으로 설정합니다
![015 015](/static/2acf4c5e2991e14cdcc8d1ae68ceaf46/789f4/015.png)
{
"Records": [
{
"eventVersion": "2.0",
"eventSource": "aws:s3",
"awsRegion": {Region 명},
"eventTime": "2023-01-21T00:00:00.000Z",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "test001"
},
"requestParameters": {
"sourceIPAddress": "127.0.0.1"
},
"responseElements": {
"x-amz-request-id": "~~~",
"x-amz-id-2": "~~~"
},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "~~~",
"bucket": {
"name": {버킷명},
"ownerIdentity": {
"principalId": "test001"
},
"arn": {버킷_arn}
},
"object": {
"key": {테스트할 파일명},
"size": "6767104",
"eTag": "~~~",
"sequencer": "~~~"
}
}
}
]
}
CloudWatch에서 로그를 확인합니다
해당 페이지에서 간단한 테스트 로그를 확인 할 수 있지만 연결된 CloudWatch를 사용한다면 더 자세한 로그를 확인할 수 있습니다.
⭐️실행 결과🌟
코드 상에서 S3 버킷에 이미지가 업로드되면, Thumbnail 폴더에 리사이징이 완료된 이미지를 업로드하도록 설정하여 실행 값을 확인할 수 있습니다.
💡S3의 특정 폴더에 이벤트를 지정하고 싶다면 S3 버킷 Prefix를 설정하면 가능합니다
이렇게 설정하면, s3-product-resizes의 images 폴더에 업로드되면 Lambda 함수가 호출됩니다.
정상적으로 업로드 된 것을 확인할 수 있습니다.
⚠️주의사항⚠️
-
무한업로드 실패 사례
source 버킷과 destination 버킷이 같은경우 이미지 리사이징 후 업로드를 실행할 경우 S3에 이미지가 무한 업로드 되는 현상 발생=
- Client가 ‘test.jpg’ 업로드
- Application은 S3의 특정 path에 test.jpg 업로드
- 트리거를 통해 S3의 같은경로에 ‘test.jpg’를 업로드
- 3번을 통해 생성된 파일들에 대해 업로드를 시작하며 Lambda가 무한 호출
💡해결 방법💡
- aws 공식 예제에서는 버킷을 분리하여 저장하는 것을 권고하고 있지만, 현재의 올리브영 온라인몰 구조상 같은 버킷을 사용해야 했습니다.
- 따라서, 같은 버킷의 트리거에 서로 다른 prefix를 설정하여 폴더를 나누어, 각 용도에 맞는 폴더 경로로 이미지를 업로드했습니다.
참고자료 : lambda s3 무한 업로드 실패 사례
💡해결 방법💡
이제 이미지를 업로드하면 자동으로 썸네일 버전의 이미지가 생성되어 서버에서 따로 처리할 필요가 없어졌습니다.
특정 이벤트에 처리되어야 하는 간단한 함수들은 이렇게 Lambda 함수를 생성하는 방식으로도 고려해볼 수 있을 것 같습니다.
감사합니다🫒