ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [AWS] Lambda(Edge)와 CloudFront를 이용하여 온디맨드 이미지 리사이징 구현하기
    AWS 2023. 9. 9. 13:21

    때는 몇 주 전, 나는 크래프톤 정글에서 'RoadMaker'라는 로드맵을 생성하고 공유할 수 있는 웹 플랫폼을 개발하고 있었다. 그런데 테스트 과정에서 로드맵 썸네일의 용량이 크면 클수록 메인 페이지의 로딩 속도가 느려지는 문제에 부딪혔다. 예를 들어, 썸네일 하나가 대략 1MB라면 40개의 로드맵 썸네일을 불러오는 데 최대 40MB의 데이터를 다운로드해야 하게 된다.

     

    RoadMaker의 메인 페이지


    이 문제를 해결하기 위해 Lambda와 Cloudfront를 사용한 온디맨드 이미지 리사이징을 구현했다. 오늘은 이 글을 통해 그 과정을 공유하고자 한다.

     

    1. 온디맨드 리사이징이란?

    'On-Demand'는 '요청 시에', '필요할 때' 제공된다는 것을 의미한다. 그런 의미에서 온디맨드 리사이징이란 이미지를 업로드할 때 미리 리사이징해 두는 것이 아니라, 클라이언트가 이미지를 요청할 때 이미지를 리사이징하는 것을 말한다.

     

    온디맨드 이미지 리사이징의 과정은 다음과 같다.

    1. 원본 저장: 이미지 저장 시, 원본을 저장한다.
    2. 이미지 요청: 클라이언트가 이미지와 원하는 사이즈를 요청한다.
    3. 캐싱 확인: CDN에 해당 사이즈로 리사이징 된 이미지가 캐싱되어 있다면 해당 이미지를 반환한다.
    4. 리사이징: 캐싱된 이미지가 없다면 리사이징 한 후 캐싱한다.

    (출처: 본인)

     

    다음은 온디맨드 이미지 리사이징의 장점이다.

    1. 저장 공간 절약: 필요한 이미지만 동적으로 생성하므로, 미리 여러 버전의 이미지를 저장할 필요가 없다.
    2. 유연성: 도중에 디자인이 변경되어 필요한 이미지의 크기가 변경되더라도 대처가 가능하다.

     

    온디맨드 이미지 리사이징도 단점은 존재한다.

    1. 캐시 히트 중요: 캐싱되지 않은 이미지 요청 시 이미지를 리사이징 해야 하므로 로딩이 느리다.
    2. 리사이징 비용: 캐싱되어 있지 않은 크기의 이미지를 리사이징할 시 많은 연산량이 필요하다. 보통 업로드보다 다운로드 비율이 높기 때문에 과부화 발생 가능성이 존재한다.

    2. AWS S3 버킷 생성

    먼저 이미지를 저장할 S3 버킷을 생성해줘야 한다. 만약 기존 사용하던 버킷이 있다면 설정이나, 정책만 잘 입력해주면 된다.

    1. S3 페이지로 이동 후 버킷 만들기 버튼 클릭
    2. 버킷 이름 작성 및 AWS 리전 선택(필자는 아시아 태평양(서울) ap-northeast-2)를 선택했다)
    3. 퍼블릭 엑세스 차단 설정 해제
    4. 버킷 만들기 버튼을 클릭하여 버킷 생성

     

    3. IAM 정책 생성 및 역할 생성

    이미지를 리사이징 할 Lambda에서 S3와 CloudFront에 접근할 수 있어야 하기에 정책과 역할을 생성해야 한다.

    3.1. 정책 생성

    1. AWS IAM 정책 메뉴로 이동
    2. 정책 생성 버튼 클릭
    3. JSON 탭에서 아래 정책을 입력
    4. 정책 이름 설정(필자는 OnDemandImageResizingPolicy으로 했다)
    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": "*"
            }
        ]
    }

     

    3.2. 역할 생성

    1. AWS IAM 역할 메뉴로 이동
    2. 역할 만들기 버튼 클릭
    3. 신뢰할 수 있는 엔티티 유형에서 AWS 서비스 - Lambda 선택
    4. 권한 추가 페이지에서 OnDemandImageResizingPolicy 정책 선택
    5. 역할 이름 설정(필자는 OndemandImageResizingRole로 했다)
    6. 1단계 신뢰할 수 있는 엔티티 선택 - 편집 - 아래 코드 입력
    7. 역할 생성 버튼을 클릭하여 역할 생성
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {
                    "Service": [
                        "edgelambda.amazonaws.com",
                        "lambda.amazonaws.com"
                    ]
                },
                "Action": "sts:AssumeRole"
            }
        ]
    }

     

    4. CloudFront  배포 생성 및 동작 생성

    이제 CloudFront에서 배포를 생성하고, 배포의 동작을 생성해야 한다.

     

    4.1. 배포 생성

    CloudFront 콘솔로 이동하여 '배포 생성' 버튼을 누르고 다음 내용을 입력해주자.

    • 원본 도메인: S3 버킷을 선택한다.
    • 이름: S3 버킷을 선택한다.
    • 원본 엑세스: 원본 엑세스 제어 설정(권장)을 선택하고 Origin access control에 S3 버킷을 선택해주자.

     

    • 뷰어 프로토콜 정책: Redirect HTTP to HTTPS
    • 허용된 HTTP 방법: GET, HEAD
    • 뷰어 엑세스 제한: NO
    • 캐시 키 및 원본 요청: Legacy cache settings
      • 쿼리 문자열: 지정된 쿼리 문자열
        • w: 이미지의 width를 지정
        • h: 이미지의 height을 지정
        • f: 이미지의 format을 지정

    이후 배포 생성 버튼을 눌러 배포를 생성한다.

     

    생성된 배포를 클릭하여 배포 상세 화면으로 넘어간 뒤 '원본 선택 후 편집'을 눌러 정책을 복사한 후 S3에 등록한다(S3 콘솔로 이동 - 버킷 상세 화면 - '권한'탭 - 버킷 정책에 붙여 넣기). S3에 정책을 붙여 넣어줘야 CloudFront에서 S3에 접근할 수 있다.

     

    4.2. 동작 생성

    동작 생성은 배포 생성과 동일하게 작성하면 된다.

     

    1. 생성된 배포를 클릭한 후 동작 탭으로 이동한다.

    2. '동작 생성' 버튼을 누른 후 배포 생성 설정과 동일한 내용을 입력한다.

    3. '동작 경로'의 경우 S3에서 디렉토리 설정을 해주지 않았다면 '/*'로 설정한다.

     

    5. Lambda 함수 생성

    이제 이미지를 리사이징 할 람다 함수를 생성해야 한다. Lambda@Edge가 버지니아 북부에서만 지원되기 때문에 반드시 버지니아 북부에서 생성해주자(use-east-1)

     

    1. AWS Lambda 콘솔로 이동
    2. '함수생성' 버튼 클릭
    3. 함수이름 설정: resize-image
    4. 런타임 설정: Node.js 16.x
    5. 기본 실행 역할 변경 - OnDemandImageResizingRole 선택
    6. 생성된 함수 상세화면에서 '구성' 탭 - '일반 구성' 편집으로 제한 시간을 10초로 변경
    7. Lambda 함수 새 버전 게시 및 Edge 배포

     

    이제 람다 함수에 넣을 코드를 작성해야 한다. AWS Cloud9을 이용하거나 자신이 편한 IDE로 작성해주면 된다. 필자는 이전 회사에서 사용했던 node.js로 작성하기로 했다.

     

    다음은 이미지 리사이징 코드다. 당시 프로젝트 마감 하루 전이여서 https://developjuns.tistory.com/53 게시글의 코드를 복붙하였다(꼭 npm init을 한 후 필요한 패키지를 설치해주자. 로컬 환경에서 작업했다면 코드를 node_modules와 함께 압축한 후 람다에 등록해야 한다)

    '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: 'ap-northeast-2'
    });
    const BUCKET = 's3 name';
    
    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);
    };

     

    람다 함수가 등록이 되었다면 Lambda 콘솔로 이동 후 'resize-image' 함수 상세화면으로 이동한 후 다음과 같이 Lambda@Edge 배포를 하자.

     

    1. 트리거 추가
    2. CloudFront선택
    3. Deploy to Lambda@Edge 버튼 클릭

    설정이 잘 되었다.

     

    6. 결과 확인

    CloudFront의 배포 도메인으로 이미지를 요청해보자. 다음과 같이 'CloudFront 배포 도메인/이미지이름.확장자' URL로 요청하면 된다.

     

    다음은 RoadMaker에서 이미지를 불러오는 URL이다.

    https://d2stpw83oif7ew.cloudfront.net/2ace0c50-2d6d-4fc1-b206-8c7a64419294.webp

    먼저 원본 이미지는 다음과 같다.

     

    이번에는 w=400&h=300 쿼리스트링을 추가하여 리사이징을 요청해보자.

    https://d2stpw83oif7ew.cloudfront.net/2ace0c50-2d6d-4fc1-b206-8c7a64419294.webp?w=400&h=300

     

    이미지가 정상적으로 리사이징 된 것을 확인할 수 있었다.

     

    개발자 도구를 열고 한번 더 요청해보면 304 status code로 응답한다. 이는 정상적으로 캐싱되었다는 의미이다.

     

    1년 전 회사에서 이미지 리사이징을 급하게 구현할 일이 있었는데, 당시에는 이미지를 업로드할 때 이미지를 특정 크기로 미리 리사이징 하는 방식을 사용했었다. 그때 기획이 변경되며 썸네일의 크기와 형식이 매우 다양해져서 당황했던 기억이난다. 이번 프로젝트를 통해 온디맨드 리사이징에 대해 이해하고 직접 구현해봐서 뿌듯하다.

     

    7. 참조

     

    'AWS' 카테고리의 다른 글

    [AWS] Elastic IP란 무엇인가?  (0) 2023.07.17
    IAM  (0) 2023.07.15
Designed by Tistory.