[Reactjs] AWS Lambda@Edge 이미지 리사이징

[Reactjs] AWS Lambda@Edge 이미지 리사이징

안녕하세요? 정리하는 개발자 워니즈입니다. 이번 시간에는 필자가 토이 프로젝트로 진행하는 프로젝트에서 있었던 이슈에 대해서 이야기를 하고, 어떻게 해결을 했는지 정리해보도록 하겠습니다.

지난 글들은 아래를 참고 해주시면 됩니다.

필자가 진행하는 프로젝트는 전세계 가구 브랜드를 전시하는 웹사이트 입니다. 이미지 기반의 Masonry 형식의 Grid View를 활용하다 보니, 첫 홈페이지 로딩시 수십개의 이미지들이 로딩되면서, Grid View가 살짝 깨지는 현상이 있었습니다.

좌측이 실제 화면이고, 실제 첫 페이지 로딩시에 리소스가 7.3mb를 호출하여 사용하는 만큼 네트워크 비용도 많이 들고, 사용자 경험 입장에서는 최악의 로딩을 제공하고 있었습니다.

그래서 생각했던것들은 다음과 같습니다.

  • 크롤서버에서 직접 Resizing을 수행한 뒤, S3에 이미지를 적재 한다.
  • 람다를 두고, 적재된 이미지를 Resizing하고 특정 경로에 다싱 업로드를 수행한다.

위의 방법들은 어찌 됐든, Backend에서 직접 처리 후, 원본 이미지를 변경해주는 작업이였습니다.

조금더 구글링을 하다보니, 필자와 같은 고민을 했던 여러 스타트업 회사들이 있었고, 그중에서도 가장 적합하게 찾은것이 당근마켓에서 게재한 블로그 내용입니다.

AWS Lambda@Edge에서 실시간 이미지 리사이즈 & WebP 형식으로 변환

1. AWS Lambda@Edge가 무엇인가요?

Lambda@Edge는 AWS CloudFront의 기능입니다. 이름에서도 느껴지듯이 람다이지만, 엣지쪽에 배치를 하여 사용자 경험을 최상으로 끌어내겠다는 것입니다. 별도의 API Gateway없이, CloudFront에 의해 생성된 이벤트를 트리거로 하여 함수를 실행할 수 있습니다.

CloudFront+Lambda@Edge를 사용하여 구현하기 위해서는 Origin response(원본 응답)에 대해서 이벤트 트리거를 사용하였습니다. CloudFront에 최초 접속했을때, 캐시된 내용이 있으면 응답하면 되고, 없으면 Origin에게 요청을 수행하여 돌아오는 응답에 대해서 람다 연산을 수행하고, 그 내용을 캐싱하고 사용자에게 응답하는 것입니다.

2. AWS Lambda@Edge 구현

필자가 최초 구현했을 당시에는, S3를 직접 호출하여 이미지를 사용자에게 내려주고 있었습니다. 그러다보니 Origin으로 요청이 계속해서 들어가게 되어있는 구조였습니다. (빨강색 표시)

하지만, CloudFront를 중간에 넣었고, 이미지에 대한 Resiznig 구현으로 AWS Lambda@Edge를 넣게 되었습니다.

따라서, 구현하고자 하는 내용은 아래와 같습니다.

Lambda@Edge를 이용해 다음과 같이 쿼리스트링을 옵션으로 요청에 따라 실시간으로 이미지의 크기(w, h), 품질(q), 파일 형식(포맷, f)을 변경할 수 있도록 구성하고자 합니다.

2-1. IAM 정책/역할/신뢰관계 생성

  • 정책생성
  1. IAM으로 접속합니다.
  2. 정책생성을 선택합니다.
  3. JSON을 선택하여 아래 정책을 입력합니다.
  4. 정책의 이름(ResizingImages)과 설명을 작성합니다.
  5. 정책 생성버튼을 클릭합니다.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole",
                "lambda:GetFunction",
                "lambda:EnableReplication",
                "cloudfront:UpdateDistribution",
                "s3:GetObject",
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ],
            "Resource": "*"
        }
    ]
}
  • 역할 생성
  1. 역할 만들기를 선택합니다.
  2. ‘이 역할을 사용할 서비스’로 Lambda를 선택합니다.
  3. 목록 중 위에서 생성한 정책 ResizingImages를 선택합니다.
  4. 역할의 이름(ResizingImages)과 설명을 작성합니다.
  5. 역할 만들기버튼을 클릭합니다.
  • 신뢰 관계(정책) 수정

서비스 보안 주체 lambda.amazonaws.comedgelambda.amazonaws.com에 권한을 위임하기 위해 IAM 역할을 수정합니다.

  1. 나의 역할 목록에서 생성했던 ResizingImages를 선택합니다
  2. 신뢰 관계 탭의 신뢰 관계 편집을 선택합니다.
  3. 아래의 정보를 입력합니다.
  4. 업데이트 버튼을 클릭합니다.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

2-2. AWS Lambda@Edge 생성하기

  1. 리전은 미국 동부(버지니아 북부) /us-east-1 만 허용 됩니다.
  2. 함수 생성 버튼을 클릭합니다.
  3. 함수 이름(ResizingImages) 를 입력합니다.
  4. 런타입(Node.js 10.x)를 선택합니다.
  5. 실행 역할 선택 또는 생성
    • 실행 역할 : 기존 역할 사용
    • 기존 역할 : ResizingImages
  6. 함수 생성 버튼을 클릭합니다.
  7. 제한 시간을 3초로 설정하면 최초 요청(Cache miss)에 연산이 많아지면 연산이 되지 않을 수 있으니, 10초로 설정합니다.
  8. 저장 버튼을 클릭합니다.

2-3. Cloud 9 을 이용한 Lambda 함수 작성

개발의 편의성을 위해서 AWS에서 제공해주는 IDE툴인 Cloud 9 을 사용해보도록 하겠습니다.

람다 코드를 작성하기 위해 새로운 Cloud 9 환경을 설정합니다.

  1. Create environment를 선택합니다.
  2. Name environment의 Name(ResizingImages)과 Description을 입력합니다.
  3. 다음과 같이 Configure settings를 설정합니다.
    • Create a new instance for environment (EC2)
    • t2.micro (1 GiB RAM + 1 vCPU)
    • Amazon Linux
    • After 30 minutes (default)
  4. Create environment를 클릭합니다.

약 5분 정도 기다리게 되면, IDE화면이 노출되고 하단부에는 Terminal까지 켜지게 됩니다. 임시 EC2를 생성하게 된것이고, 기존에 EC2를 생성하게 되면 SG부터 접속까지 이러저러한 불편을 없애준 서비스라고 보시면 됩니다.

  1. 화면 오른쪽 위 메뉴중 AWS Resources를 선택합니다.

  2. mbda(us-east-1)/Remote Functions 목록의 ResizingImagesImport합니다.

  3. 불러온 람다 함수로 접근하기 위해 터미널(Terminal)을 이용합니다.(
    package.json
    

    을 생성하고 Sharp 모듈을 설치합니다.)

    1. $ cd ResizingImages
    2. $ npm init -y
    3. $ npm i sharp
  4. index.js을 아래 코드와 같이 수정합니다.

  5. 작성한 람다 함수를 $LATEST 버전으로 배포합니다.

  • Lambda(us-east-1)/Local Functions 목록의 ResizingImagesDeploy합니다.

[람다 소스]

'use strict';

const querystring = require('querystring'); // Don't install.
const AWS = require('aws-sdk'); // Don't install.
const Sharp = require('sharp');

const S3 = new AWS.S3({
  region: ''
});
const BUCKET = '';

exports.handler = async (event, context, callback) => {
  const { request, response } = event.Records[0].cf;
  // Parameters are w, h, f, q and indicate width, height, format and quality.
  const params = querystring.parse(request.querystring);

  // Required width or height value.
  if (!params.w && !params.h) {
    return callback(null, response);
  }

  // Extract name and format.
  const { uri } = request;
  const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);

  // Init variables
  let width;
  let height;
  let format;
  let quality; // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
  let s3Object;
  let resizedImage;

  // Init sizes.
  width = parseInt(params.w, 10) ? parseInt(params.w, 10) : null;
  height = parseInt(params.h, 10) ? parseInt(params.h, 10) : null;

  // Init quality.
  if (parseInt(params.q, 10)) {
    quality = parseInt(params.q, 10);
  }

  // Init format.
  format = params.f ? params.f : extension;
  format = format === 'jpg' ? 'jpeg' : format;

  // For AWS CloudWatch.
  console.log(`parmas: ${JSON.stringify(params)}`); // Cannot convert object to primitive value.
  console.log(`name: ${imageName}.${extension}`); // Favicon error, if name is `favicon.ico`.

  try {
    s3Object = await S3.getObject({
      Bucket: BUCKET,
      Key: decodeURI(imageName + '.' + extension)
    }).promise();
  } catch (error) {
    console.log('S3.getObject: ', error);
    return callback(error);
  }

  try {
    resizedImage = await Sharp(s3Object.Body)
      .resize(width, height)
      .toFormat(format, {
        quality
      })
      .toBuffer();
  } catch (error) {
    console.log('Sharp: ', error);
    return callback(error);
  }

  const resizedImageByteLength = Buffer.byteLength(resizedImage, 'base64');
  console.log('byteLength: ', resizedImageByteLength);

  // `response.body`가 변경된 경우 1MB까지만 허용됩니다.
  if (resizedImageByteLength >= 1 * 1024 * 1024) {
    return callback(null, response);
  }

  response.status = 200;
  response.body = resizedImage.toString('base64');
  response.bodyEncoding = 'base64';
  response.headers['content-type'] = [
    {
      key: 'Content-Type',
      value: `image/${format}`
    }
  ];
  return callback(null, response);
};

3. CloudFront 옵션 변경

필자는 기존에 CloudFront까지는 생성해두고, Distribution을 수행하여 테스트까지 마친 상태였습니다.

  1. CloudFonrt로 이동해 해당 Distribution을 선택합니다.
  2. Behaviors 탭에서 Create Behavior를 선택합니다.
  3. 다음과 같이 수정합니다.

2번째, 3번째 박스에서 보시면

리소스를 캐싱할때 쿼리스트링을 분리해서 캐싱하는 것이고, 쿼리스트링 내용은(w:너비, h:높이, q:품질, f:포맷) 에 대한 내용입니다.

4. 람다 새 버전 게시

이제 람다엣지를 생성하였고, 클라우드 프론트까지 준비가 되었으니, 실제 엣지로 람다를 배포하고, 클라우드 프론트와 연결하여 사용하면 됩니다.

  1. Lambda 탭으로 이동합니다.
  2. 함수 목록에서 ResizingImages를 선택합니다.
  3. 오른쪽 위 메뉴 중 ‘작업’의 새 버전 게시를 선택합니다.
  4. ‘$LATEST의 새 버전 게시’의 ‘버전 설명’을 입력하고 새로운 버전을 게시합니다.
  5. 게시된 새 버전의 ARN 복사(ARN - arn:aws:lambda~~~:ResizingImages:1, 화면 오른쪽 위)하여 CloudFront에 연결해야 합니다.
  6. CloudFront로 이동해 해당 Distribution을 선택합니다.

  7. Behaviors 탭에서 기본 Behavior를 체크하고 Edit를 선택합니다.
  8. 다음과 같이 수정 후 Yes, Edit 버튼을 클릭합니다.
    • Lambda Function Associations:
      • Lambda Function ARN: Lambda 함수에서 복사한 ARN 입력(arn:aws:lambda~~~:ResizingImages:1)

5. 테스트

최종적인 아키텍처는 다음과 같습니다.

최초 사용자는 Domain을 통해서 요청을 하게 되고 (Route53)에 의해 CloudFront쪽으로 요청이 진행됩니다. 그럼 CloudFront에서는 Cache Layer로서 동작을 하고, 없으면 Origin(S3)에서 원본을 가져옵니다. 이때 Origin Response를 돌아오면서 Lambda@Edge를 거치고, Resizing된 이미지를 Cache Layer에 가져다 둡니다.

5-10분후에 클라우드 프론트 변경내용이 Deployed 상태로 변경이 되면, 모든 작업은 끝이 났습니다. 캐시 되기 전에 S3 오리진에 직접 호출한 내용과 CloudFront를 호출하여 호출한 내용에 대한 차이는 다음과 같습니다.

원본 최초 호출시 : 17.4kb

람다 엣지 최초 호출시 : 1.4kb

6. 마치며…

이번시간에는 람다 엣제를 통한 온디멘드 이미지 리사이징에 대해서 정리해봤습니다. 확실히 아마존에서는 여러가지 고민을 많이 하고 서비스를 만들어 내는것 같습니다. 개발자입장에서는 잘 만들어진 서비스를 가져다가 붙이기만 하면되니 훨씬 편해진 것 같습니다.

만약, 리사이징에 대하여 직접 구현하려면 서버도 생성하고, 개발환경도 셋팅하고 코딩도 해야되고 할 것이 엄청 많은데, 간단하게 코딩에만 집중하여 서버리스로 구현하니, 생산성도 엄청 증가한 것 같습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다