• 람다를 이용한 이미지 리사이징 - 1 :: 마이구미
    AWS 2019. 5. 16. 23:00
    반응형
    이 글은 AWS 람다 서비스를 이용해 이미지 리사이징에 대한 다룬다.
    우선 제목에서도 1편이라고 한 이유는 다음과 같다.
    람다를 이용한 이미지 리사이징은 여러 방식이 있고, 계속해서 새로운 방식이 나오고 있기 때문이다.
    여기서는 가장 기본적이고, 간단한 예제를 통해 다룬다.
    Lambda@Edge 를 활용한 이미지 리사이징 2편 - https://mygumi.tistory.com/377
    AWS 문서 기반 - https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/with-s3-example.html

     

    크게 S3 와 Lambda 를 이용한 예제를 다루려고 한다.

    S3 에 이미지를 업로드하면, 자동적으로 원하는 크기로 리사이징되어 서버에 저장되길 원한다.

    즉, 여러 디바이스 및 환경에 맞게 사용할 수 있는 썸네일을 원하는 시나리오라고 생각하면 된다.

    하나의 원본 이미지가 있으면, 그에 따른 리사이징된 썸네일들도 존재하는 것이다.

    여기서는 이것을 Lambda 와 S3 를 통해 만들어본다.

     

    참고할 사항은 하나의 이미지를 기준으로 필요한 리사이징 이미지가 4개라면, 결과적으로 1000개의 이미지는 5000개의 이미지를 저장하게 된다.

    이미지가 많이 사용되는 서비스에서는 적합하지 않아보일 수 있다.

    아무튼 많은 대안과 방식이 존재하기에, 여기서 다루는 내용은 너무 깊게 고민할 필요는 없고, "이런 방식이 있구나" 라고 바라보자. 

     

    다음과 같은 기술 스택으로 진행한다.

     

    • AWS - S3, Lambda, IAM, CloudWatch
    • aws-cli (optional)
    • Node.js - sharp

     

    원본 이미지에 대해 width 가 200px, 400px, 600px 을 가지는 리사이징된 이미지들이 존재하길 원한다.

    예를 들어, images 폴더 안에는 origin, w_200, w_400, w_600 이라는 이름을 가진 이름이 존재할 것이다.

    우리가 원하는 것은 images/origin 폴더에 원본 이미지를 저장하면, 자동적으로 리사이징된 이미지를 목적에 맞는 폴더에 저장하는 것이다.

    origin, w_200, w_400, w_600 이라는 폴더에는 똑같은 이미지들이 존재하지만, 크기는 서로 다를 것이다.

     

    다음과 같이 그림으로 표현할 수 있다.

     

     

    위처럼 images/origin 폴더에 이미지를 업로드 했을 경우, 자동적으로 w_200, w_400, w_600 폴더에도 이미지를 생성한다.

    이 과정을 본격적으로 알아보자.

     

    1. S3 버킷 생성 

     

    우선 이미지 리소스를 저장하는 S3 버킷을 만든다.

    'mygumi-resource' 라는 버킷을 만들고, 위 그림처럼 images/origin, w_200, w_400, w_600 구조를 만든다.

     

     

    2. Lambda 함수 생성

     

    콘솔 페이지에서 단순히 create 버튼 클릭만으로 생성한 후, 코드를 작성하면 된다.

    이 부분은 편의를 위해 aws-cli 을 사용하도록 하겠다. 

    aws-cli 을 설치하고, aws configure 명령어를 통해 관련 변수를 셋팅해주면 끝이다. (https://github.com/aws/aws-cli README.md 참고)

     

    우선 index.js 파일을 생성한 후, 이미지 리사이징 코드를 작성한다.

    리사이징 관련 모듈은 sharp 을 사용한다.

    원하는 시나리오를 위한 코드의 순서는 다음과 같다.

     

    1. s3 에서 업로드된 원본 이미지를 가져온다. s3.getObject()
    2. 가져온 원본 이미지의 데이터를 통해 이미지 리사이징한다. sharp.resize()
    3. 리사이징된 이미지를 s3 에 업로드한다. s3.putObject()

    각 메소드에 대한 자세한 사항은 문서를 참고하자.

    전체 코드는 다음과 같다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    const sharp = require("sharp");
    const aws = require("aws-sdk");
    const s3 = new aws.S3();
     
    const Bucket = "mygumi-resource";
    const transforms = [
      { name"w_200", width: 200 },
      { name"w_400", width: 400 },
      { name"w_600", width: 600 }
    ];
     
    exports.handler = async (event, context, callback) => {
      const key = event.Records[0].s3.object.key;
      const sanitizedKey = key.replace(/\+/g, " ");
      const parts = sanitizedKey.split("/");
      const filename = parts[parts.length - 1];
     
      try {
        const image = await s3.getObject({ Bucket, Key: sanitizedKey }).promise();
     
        await Promise.all(
          transforms.map(async item => {
            const resizedImg = await sharp(image.Body)
              .resize({ width: item.width })
              .toBuffer();
            return await s3
              .putObject({
                Bucket,
                Body: resizedImg,
                Key: `images/${item.name}/${filename}`
              })
              .promise();
          })
        );
        callback(null, `Success: ${filename}`);
      } catch (err) {
        callback(`Error resizing files: ${err}`);
      }
    };
     
    cs

     

    참고로 sharp.resize().toBuffer() 를 설명하면 다음과 같다.

    sharp.resize() 파라미터 값으로 width 만 지정한 이유는 이미지의 비율을 깨고 싶지 않아서이다.

    결과적으로 width 를 기준으로 height 는 자동적으로 비율에 맞게 조정된다.

    s3 업로드를 위한 메소드 putObject() 에서의 파라미터 Body 를 위해 이미지의 버퍼 형태로 가져오기 위해 toBuffer() 를 사용한다.

     

    이제 작성된 코드를 aws-cli 를 통해 람다를 생성해본다.

    작성된 index.js 파일을 가지는 폴더에 sharp 와 같은 모듈은 설치해주어야한다.

     

    $ npm install sharp

     

    결과적으로 다음과 같은 디렉토리 구조를 가질 것이다.

     

    lambda-image-resize
        |- index.js
        |- node_modules/sharp

     

    함수 코드(index.js) 및 종속 모듈들의 대한 배포 패키지를 zip 명령어를 통해 생성한다.

     

    $ zip -r function.zip .

     

    폴더에 function.zip 파일이 존재할 것이다.

    생성된 zip 파일을 기반으로 관련 aws-cli 명령어를 통해 람다 함수를 생성한다.

     

    $ aws lambda create-function --function-name imageResizing \
    --zip-file fileb://function.zip --handler index.handler --runtime nodejs8.10 \
    --role arn:aws:iam::123456789012:role/lambda-s3-resizing

     

    aws lambda create-function 명령어는 요약하면 다음과 같다.

     

    • imageResizing 이라는 이름을 갖는 람다 함수.
    • node.js 8.10 버전을 기반.
    • arn:aws:iam::123456789012:role/lambda-s3-resizing IAM 역할을 기반.

     

    성공적으로 완료했다면, 콘솔 페이지를 통해 확인하면 람다 함수가 생성된 모습을 볼 수 있다.

    그렇다면, 생성된 람다 함수가 잘 동작하는지 확인해보자.

    콘솔 페이지에서 아래 입력값을 통해 Test 버튼을 통해 확인할 수도 있고, aws lambda invoke 명령어를 통해 확인할 수 있다.

    * 테스트는 S3에 접근해야하기 때문에, imageResizing 람다 함수가 가지는 Role(arn:aws:iam::123456789012:role/lambda-s3-resizing) 에 S3 접근 정책을 추가해야한다.

     

    input.txt

    {
      "Records":[  
        {  
          "eventVersion":"2.0",
          "eventSource":"aws:s3",
          "awsRegion":"us-west-2",
          "eventTime":"1970-01-01T00:00:00.000Z",
          "eventName":"ObjectCreated:Put",
          "userIdentity":{  
            "principalId":"AIDAJDPLRKLG7UEXAMPLE"
          },
          "requestParameters":{  
            "sourceIPAddress":"127.0.0.1"
          },
          "responseElements":{  
            "x-amz-request-id":"C3D13FE58DE4C810",
            "x-amz-id-2":"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
          },
          "s3":{  
            "s3SchemaVersion":"1.0",
            "configurationId":"testConfigRule",
            "bucket":{  
              "name":"mygumi-resource",
              "ownerIdentity":{  
                "principalId":"A3NL1KOZZKExample"
              },
              "arn":"arn:aws:s3:::mygumi-resource"
            },
            "object":{  
              "key":"images/origin/1.jpg",
              "size":1024,
              "eTag":"d41d8cd98f00b204e9800998ecf8427e",
              "versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko"
            }
          }
        }
      ]
    }
    cs

     

    $ aws lambda invoke --function-name imageResizing --invocation-type Event --payload file://input.txt outputfile.txt

     

    이 테스트를 위해서는 images/origin/ 경로에 1.jpg 파일이 존재해야한다.

    그렇지 않으면, "NoSuchKey: The specified key does not exist" 관련 에러가 발생할 것이다.

    정상적으로 동작했다면, 각 폴더에는 너비가 200px, 400px, 600px 인 이미지가 저장되어있을 것이다.

    만약, 원하는 결과를 얻지 못했다면, 람다 함수 실행에 의한 연동되어있는 CloudWatch Logs 를 보고 확인하면 된다.

     

    추가적으로, 이 과정에서 코드를 계속해서 수정해야할 때는 간단한 예로 aws-cli 명령어를 통해 쉽게 업데이트 할 수 있다.

    (배포 패키지 생성 => 람다 함수 업데이트 => 람다 함수 실행)

     

    $ zip -r function.zip .
    $ aws lambda update-function-code --function-name imageResizing --zip-file fileb://function.zip --publish
    $ aws lambda invoke --function-name imageResizing --invocation-type Event --payload file://input.txt outputfile.txt

     

    3. S3 - Lambda 연동

     

    현재까지는 람다 함수를 직접 실행해서 동작을 확인했다.

    결과적으로 우리가 원하는 건 S3 에 이미지가 업로드 되었을 경우, 람다 함수가 실행되어 이미지 리사이징이 일어나는 것이다.

    이를 위해 S3 는 관련 이벤트가 발생했다는 것을 람다 함수에게 알림을 줘야한다.

    이 기능은 간단하게 S3 버킷에서 Properties->Advanced settings->Events 에 존재한다.

     

     

    우리가 원하는 조건을 위와 같이 줄 수 있다.

    Prefix 를 통해 images/origin 내부에서의 이벤트 발생을 발생하게 한다.

    Suffix 를 통해 jpg 파일만을 감지하게 한다.

    Send to, Lambda 를 통해 람다 함수 imageResizing 를 실행한다.

     

    이벤트 알림을 생성하면, 람다 콘솔 페이지에서도 연동된 것을 확인할 수 있다.

     

     

    결과적으로, 간단하게 람다 함수를 이용해서 이미지 리사이징을 구현할 수 있다.

    처음에도 언급했지만, 더 효율적이고 좋은 방법은 존재한다.

    더 효율적이고 좋은 방식은 이러한 방식이 존재해서 대안으로 나온 것이기에, 참고하면 좋을 것이다.

    다음에는 다른 방식을 다루려고 한다.

     

    Lambda@Edge 를 활용한 이미지 리사이징 2편 - https://mygumi.tistory.com/377

    반응형

    댓글 2

    • minho 2021.01.04 17:58

      좋은 강좌 감사합니다.
      다만 s3구성시에 같은 버켓으로 구성하면 재귀호출로인한 리커시브가 발생할 수 있는데... 그부분 잘못하면 비용이 과다하게 청구되더라구요
      그부분 업데이트 해주시면 더 좋지 않을까 싶습니다!

      추가적으로 강제 함수의 동시성을 강제초기화 하는방법도 간단하게 올려주시면 더 좋을것 같아요...

      (오늘 고생을 좀 했네요 ㅠㅠ)

Designed by Tistory.