올리브영 테크블로그 포스팅 AWS Lambda Image Resize 도입기
AWS Lambda Resize

AWS Lambda Image Resize 도입기

신규 상품 프로젝트에서 AWS Lambda 이미지 리사이징 적용하기

2023.05.19

안녕하세요. 올리브영에서 Back-end 개발 업무를 담당하고 있는 불광동꿀주먹🥊, 길만자 입니다.

올리브영에서는 여러 신규프로젝트들을 진행 중입니다. 최근 상품프로젝트에서는 새로운 상품에 대한 이미지 업로드 기능을 개발했습니다.

이 프로젝트에서는 상품의 기본 이미지, 썸네일 이미지, 전시 이미지 등, 각각의 용도에 맞는 이미지 리사이징 기능이 필요했습니다.

기존의 리사이징 방식은 서버에 임시파일을 생성하여 리사이징하는 방법이었지만, 서버부하, 속도, 요금 측면에서 더 나은 대안을 찾고자 고민하게 되었습니다.

그래서 저희는 AWS Lambda를 활용하여 이미지 리사이징 기능을 구현하기로 결정했습니다.

이를 통해 이미지 저장소인 S3를 최대한 효율적으로 이용할 수 있게 되었습니다.

이 글에서는 어떻게 AWS Lambda를 활용하여 이미지 리사이징을 구현했는지에 대해 자세히 설명해드리고자 합니다!


Lambda를 사용하여 S3 이미지 리사이징하기

이미지 파일이 AWS SDK를 통해 S3에 업로드 된 순간, 트리거를 사용하여 이미지 리사이징을 하는 방식을 사용했습니다.

리사이징에 필요한 코드나 전체적인 과정은 AWS 공식 가이드 를 참고하여 진행했습니다.

이미지를 업로드하고 리사이징 된 파일을 저장하는 S3 버킷을 생성합니다

001 외부 URL로 이미지를 접근하는 방식이 아니기 때문에 퍼블릭 액세스 차단으로 버킷을 설정했습니다. 필요한 권한은 추후에 늘려가도록 하려합니다.

실습을 이렇게 진행하면 S3에 업로드 된 파일을 URL로 바로 확인이 불가능하고, 다운로드 과정이 필요하기 때문에 간단하게 실습하실 예정이시라면 퍼블릭으로 설정하시는 것을 추천드립니다!

이미지가 업로드 될 때마다 실행 될 함수를 정의하기 위해 Lambda를 생성합니다

002 기본 Lambda 권한을 가진 새로운 역할을 선택하여 Lambda를 생성했습니다. 기존의 역할을 사용하실 분들은 CloudWatch 그룹 생성 권한과 Log 읽고 쓰기 권한을 추가하시면 됩니다.

S3 업로드를 위한 IAM 정책을 생성합니다

저는 Lambda에서 S3를 읽고 쓰는 권한이 필요한 작업을 수행하기 때문에 별도로 역할을 생성했습니다.

해당 정책은 실습을 위해 생성한 S3에 대한 GET 및 PUT 권한을 포함하고 있습니다.

만약 S3의 전체 권한을 부여하고 싶다면 AmazonS3FullAccess를 추가하시면 됩니다.

003
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::{버킷이름}/*"
        }
    ]
}

생성한 Lambda 함수에 해당 S3 정책을 연결하고 설정을 수정합니다

업로드 함수가 동작하는 데에 시간이 다소 소요될 수 있으므로, 일반 구성 → 편집의 제한 시간을 50초로 늘려보았습니다. 또한, Lambda 함수를 생성할 때 함께 생성되는 기본 역할이 아닌 기존의 역할을 사용하시려면 아래에서 역할을 변경해주시면 됩니다!

저는 기본 역할에 S3 역할을 추가할 예정이므로 해당 부분은 생략하겠습니다.

004

구성 → 권한의 편집을 통해서 역할에 S3 권한을 추가합니다

005 006

정책 연결을 클릭하여 이 전에 생성한 S3 권한을 추가합니다

007
이제 Lambda 함수가 S3에 접근하고 업로드할 수 있는 권한이 부여되었습니다.


S3에 이미지가 업로드될 때 함수를 실행시키기 위한 트리거를 생성합니다

함수 개요에 있는 트리거 추가를 클릭합니다.

008

트리거 이벤트를 추가할 버킷을 선택하고 Event type을 선택합니다. 이미지가 업로드될 때 트리거로 동작하길 원하므로 PUT으로 설정하였습니다.

009

함수 코드를 작성합니다

AWS 링크에 첨부된 예제 파일을 사용했습니다.

010

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 012 013

계층을 생성 후 Lambda 함수에 계층을 추가합니다

이렇게 Lambda 함수 설정과 코드 실행에 대한 설정이 끝났습니다 👏👏👏

014

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
{
  "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를 사용한다면 더 자세한 로그를 확인할 수 있습니다. 016

⭐️실행 결과🌟

코드 상에서 S3 버킷에 이미지가 업로드되면, Thumbnail 폴더에 리사이징이 완료된 이미지를 업로드하도록 설정하여 실행 값을 확인할 수 있습니다. 017 018

💡S3의 특정 폴더에 이벤트를 지정하고 싶다면 S3 버킷 Prefix를 설정하면 가능합니다

이렇게 설정하면, s3-product-resizes의 images 폴더에 업로드되면 Lambda 함수가 호출됩니다. 019

결과 값을 확인해보면 020 021

정상적으로 업로드 된 것을 확인할 수 있습니다.


⚠️주의사항⚠️

  1. 무한업로드 실패 사례

    source 버킷과 destination 버킷이 같은경우 이미지 리사이징 후 업로드를 실행할 경우 S3에 이미지가 무한 업로드 되는 현상 발생=

    1. Client가 ‘test.jpg’ 업로드
    2. Application은 S3의 특정 path에 test.jpg 업로드
    3. 트리거를 통해 S3의 같은경로에 ‘test.jpg’를 업로드
    4. 3번을 통해 생성된 파일들에 대해 업로드를 시작하며 Lambda가 무한 호출

💡해결 방법💡

  • aws 공식 예제에서는 버킷을 분리하여 저장하는 것을 권고하고 있지만, 현재의 올리브영 온라인몰 구조상 같은 버킷을 사용해야 했습니다.
  • 따라서, 같은 버킷의 트리거에 서로 다른 prefix를 설정하여 폴더를 나누어, 각 용도에 맞는 폴더 경로로 이미지를 업로드했습니다.

참고자료 : lambda s3 무한 업로드 실패 사례

  1. Lambda 메모리 조정

    이미지 파일 용량(4.1MB) 초과로 Lambda 리사이징 작업중 메모리 과부하로 리사이징 실패현상이 발생

💡해결 방법💡

  • 함수에 할당된 메모리 및 CPU 전력 수정(128MB → 512MB). 25MB까지 업로드 가능 022 023

이제 이미지를 업로드하면 자동으로 썸네일 버전의 이미지가 생성되어 서버에서 따로 처리할 필요가 없어졌습니다.

특정 이벤트에 처리되어야 하는 간단한 함수들은 이렇게 Lambda 함수를 생성하는 방식으로도 고려해볼 수 있을 것 같습니다.

감사합니다🫒

이미지 리사이징AWS Lambdas3
올리브영 테크 블로그 작성 AWS Lambda Image Resize 도입기
🙈
불광동꿀주먹 |
Back-end Engineer
버티면 다 되는거야😵