본문 바로가기
개발/AWS

AWS + Lambda@Edge + CloudFront 이미지 리사이징

by 설이주인 2023. 8. 18.

회사에서 진행하는 프로젝트에 페이지 로딩 이슈가 올라왔다.

현재 AWS - CloudFront를 통해서 캐싱을 진행하고 있는데도 이슈가 올라온것이다

 

슬프지만 확인은 해야 하는 법...!

 

라이트 하우스로 1차 확인

상세 로딩 속도 확인

이미지가 짤렸지만.... 대략 8초가 걸리는것으로 확인 된다. 제일 늦게 로딩 되는 부분이 favicon이지만, 메인페이지에서 보여지는 컨텐츠의 썸네일 또한 로딩 속도가 매우 느린 상황임을 할 수 있다.

 

도데체... 왜? 라는 생각으로 컨텐츠들의 용량을 확인해봤다.

용량이... ? 눈을 의심했다.

 

웹 사이트의 로딩에 3초 이상이 걸리면 느리다고 인식 되는데.. 이래서는 안된다.

 

우선 생각 할 수 있는 방안은 두가지였다.

1. 현재 사이즈가 큰 썸네일을 가진 이미지들에 대해서 수정을 통한 리사이징을 진행하는가

2. 사용자에게 로딩 되기전에 따로 리사이징을 강제로 진행하는가

 

일괄 업로드/수정 기능이 구현되지 않은 상태에서의 단건 수정만으로 대용량 썸네일들을 바꾸는 것은 공수가 너무 많이 들며, 앞으로도 S3에 다이렉트로 bulk 업로드를 진행하는 일이 있을것이라는 상황이 존재했기에 1번은 선택지에서 제외했다.

 

그럼 답은 2번이다. 한가지 고민거리가 더있었다.

프론트에서 해당 작업을 진행 할지 고민했지만, 이것 또한 모든 요소를 하나하나 작업해야하기에 패스다.

 

결국은 백엔드에서 작업 + Lambda를 사용해서 처리하는게 제일 수월하겠다고 판단하여 진행했다.

 


Lambda를 제작하기에 앞서 사전 작업들이 존재한다.

(해당 순서는 저의 작업 순서이며 정답이 아닙니다. 버킷은 이미 존재하기에 생성은 생략했습니다.)

IAM 정책 및 역할 생성

  • 정책 선택
  • 정책 생성 선택
  • JSON 선택 및 아래 정책 입력
  • 정책 검토 선택
  • 정책 이름 ResizingImagePolicy 또는 원하는 이름 선택 (설명 옵션)
  • 정책 생성 선택

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "logs:CreateLogStream",
                "iam:CreateServiceLinkedRole*",
                "logs:DescribeLogStreams",
                "lambda:GetFunction",
                "cloudfront:UpdateDistribution",
                "logs:CreateLogGroup",
                "logs:PutLogEvents",
                "lambda:EnableReplication"
            ],
            "Resource": "*"
        }
    ]
}

 

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html#lambda-edge-permissions-required

 

Setting IAM permissions and roles for Lambda@Edge - Amazon CloudFront

Setting IAM permissions and roles for Lambda@Edge To configure Lambda@Edge, you must set up specific IAM permissions and an IAM execution role. Lambda@Edge also creates service-linked roles to replicate Lambda functions to CloudFront Regions and to enable

docs.aws.amazon.com

 

IAM 역할 생성

  • 역할 선택
  • 역할 만들기 선택
  • 신뢰 할 수 있는 유형의 개체 선택 - AWS 서비스
  • 사용 사례 선택 - Lambda
  • 다음:권한 선택
  • 권한 정책 설정
  • 정책 필터에 1. 정책 생성을 통해 생성된 정책 검색 (ResizingImagePolicy) 후 선택
  • 다음:태그 선택 (skip)
  • 다음:검토 선택
  • 역할 이름 ResizingImageRole 또는 원하는 이름 선택 (설명 옵션)
  • 역할 만들기

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Lambda 함수 생성

Lambda@Edge 로 생성

중요! 반드시 버지니아 북부(us-east-1)에서 생성할 것!!

  • 왼쪽 상단 지역 - 버지니아 북부 선택
  • Lambda - 함수 생성 선택
  • 새로 작성 선택
  • 함수이름 resize-image 또는 원하는 함수명 입력
  • 런타임 Node.js 12.x 선택
  • 권한 아래 실행 역할을 선택하거나 생성하여 클릭
  • 기존 역할 사용 선택
  • 전에 만든 ResizingImageRole 검색 후 선택
  • 함수 생성

\

함수 제한 시간 수정

Cloud9을 활용한 Lambda 함수 작성

  • 왼쪽 상단 지역 - 버지니아 북부 선택
  • Cloud9 - Create environment 선택
  • Name에 resize-image 또는 원하는 이름 입력 (Description 은 옵션)
  • Next step 선택
  • Environment type - Create a new EC2 instance for environment 선택
  • Instance type - t2.micro 선택
  • Platform - Amazon Linux 선택
  • Cost-saving setting - After 30 minutes 선택
  • Next step 선택
  • Create environment 선택

생성된 인스턴스를 통해 lambda 함수 수정

  • AWS 클릭 > 생성한 람다가 존재하는 리전 선택(US EAST N.Virginia)

  • Remote Functions - resize-image 더블 클릭

Error : User is not authorized to perform action on resource

동시성 미설정으로 함수를 불러오지 못하는 상황 발생

  • import 선택

  • 아래 터미널에서 명령어를 통한 npm init, 라이브러리 설치
  • cd resize-image
  • npm init -y
  • npm i sharp

 

'use strict';

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

// http://sharp.pixelplumbing.com/en/stable/api-resize/
const Sharp = require('sharp');

const S3 = new AWS.S3({
  region: 'ap-northeast-2'  // 버킷을 생성한 리전 입력(여기선 서울)
});

const BUCKET = 'bucket-name' // Input your bucket

// Image types that can be handled by Sharp
const supportImageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

exports.handler = async(event, context, callback) => {
  const { request, response } = event.Records[0].cf;

  console.log("request: ", request)
  console.log("response: ", response)

  // Parameters are w, h, f, q and indicate width, height, format and quality.
  const { uri } = request;

  const ObjectKey = decodeURIComponent(uri).substring(1);
  console.log("ObjectKey : ", ObjectKey)
  const params = querystring.parse(request.querystring);
  console.log("params : ", params)
  const { w, h, q, f } = params

  /**
   * ex) https://dilgv5hokpawv.cloudfront.net/dev/thumbnail.png?w=200&h=150&f=webp&q=90
   * - ObjectKey: 'dev/thumbnail.png'
   * - w: '200'
   * - h: '150'
   * - f: 'webp'
   * - q: '90'
   */

  // 크기 조절이 없는 경우 원본 반환.
  if (!(w || h)) {
    return callback(null, response);
  }


  const extension = uri.match(/\/?(.*)\.(.*)/)[2].toLowerCase();
  console.log('extension : ', extension);

  const width = parseInt(w, 10) || null;
  const height = parseInt(h, 10) || null;
  const quality = parseInt(q, 10) || 100; // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
  let format = (f || extension).toLowerCase();
  let s3Object;
  let resizedImage;

  // 포맷 변환이 없는 GIF 포맷 요청은 원본 반환.
  if (extension === 'gif' && !f) {
    return callback(null, response);
  }

  // Init format.
  format = format === 'jpg' ? 'jpeg' : format;

  if (!supportImageTypes.some(type => type === extension )) {
    responseHandler(
      403,
      'Forbidden',
      'Unsupported image type', [{
        key: 'Content-Type',
        value: 'text/plain'
      }],
    );
    return callback(null, response);
  }


  // Verify For AWS CloudWatch.
  console.log(`parmas: ${JSON.stringify(params)}`); // Cannot convert object to primitive value.\
  console.log('S3 Object key:', ObjectKey)
  console.log('Bucket name : ', BUCKET);

  try {
    s3Object = await S3.getObject({
      Bucket: BUCKET,
      Key: ObjectKey
    }).promise();

    console.log('S3 Object:', s3Object);
  }
  catch (error) {
    console.log('S3.getObject error : ', error);
    responseHandler(
      404,
      'Not Found',
      'OMG... The image does not exist.', [{ key: 'Content-Type', value: 'text/plain' }],
    );
    return callback(null, response);
  }

  try {
    resizedImage = await Sharp(s3Object.Body)
      .rotate()
      .resize(width, height, {fit : 'contain'}) //.resize(width, height, {fit : 'contain'}) 
      .toFormat(format, { //format도 강제 가능하다{.toFormat('jpeg', {quality : 90})}
        quality
      })
      .toBuffer();
  }
  catch (error) {
    console.log('Sharp error : ', error);
    responseHandler(
      500,
      'Internal Server Error',
      'Fail to resize image.', [{
        key: 'Content-Type',
        value: 'text/plain'
      }],
    );
    return callback(null, response);
  }

  // 응답 이미지 용량이 1MB 이상일 경우 원본 반환.
  if (Buffer.byteLength(resizedImage, 'base64') >= 1048576) {
    return callback(null, response);
  }

  responseHandler(
    200,
    'OK',
    resizedImage.toString('base64'), [{
      key: 'Content-Type',
      value: `image/${format}`
    }],
    'base64'
  );

  /**
   * @summary response 객체 수정을 위한 wrapping 함수
   */
  function responseHandler(status, statusDescription, body, contentHeader, bodyEncoding) {
    response.status = status;
    response.statusDescription = statusDescription;
    response.body = body;
    response.headers['content-type'] = contentHeader;
    if (bodyEncoding) {
      response.bodyEncoding = bodyEncoding;
    }
  }

  console.log('Success resizing image');

  return callback(null, response);
};

 

 

Lambda 함수 새 버전 게시

  • 좌측 상단 버지니아 선택
  • Lambda - resize-image 선택
  • 구분자 - 버전 - $LATEST 선택
  • 작업 - 새 버전 발행 선택 (버전 설명은 옵션)
  • 게시 선택
  • 작업 선택
  • 새 버전 발행 선택 (버전 설명은 옵션임)

오리진 응답 선택

 

CloudFront의 Distribution 생성하기

  • CloudFront - Distributions - Create Distribution 선택
  • Web - Get Started 선택
  • Origin Domain Name - 전에 생성한 S3 검색 후 선택 (Origin ID 자동으로 추가됨)
  • Restrict Bucket Access - Yes 선택 (항상 Cloud Front URL 로 S3액세스 하도록)
  • Origin Access Identity - Create a New Identity 선택 (따로 생성해주자)

  • Grant Read Permissions on Bucket - Yes, Update Bucket Policy 선택 (CloudFront 가 S3 버킷 정책에 엑세스하여 업데이트)
  • Cache and origin request settings - Use legacy cache settings 선택
  • Cache Based on Selected Request Headers - None(Improves Caching) 선택
  • Forward Cookies - None(Improves Caching) 선택
  • Query String Forwarding and Caching - Forward all, cache based on whitelist 선택
  • Query String WhiteList - w,h,f,q 를 순서대로 순서대로 적을 것(w, enter, h, enter, f, enter, q, enter)
  • Compress Objects Automatically - Yes 선택
  • Create Distribution 선택 후 생성

 

완성이다.


기존 웹페이지에서 리사이징이 진행 되는지 다시 확인해보자

 

성공이다....!

로딩속도에서 favicon이 아직 느리지만 추후 개선 하는 것으로 결정 됐다.

 


작업 참고 URL

https://developjuns.tistory.com/53

 

CloudFront + Lambda(Edge) + S3 온디맨드 이미지 서버 만들기

이번 포스트는 CloudFront ( AWS CDN )과 Lambda 그리고 S3를 이용해서 온디맨드 이미지 서버를 만드는 과정을 기록하려고 한다. 온디맨드 이미지 서버란? 기존 이미지 서버에서는 이미지를 업로드하는

developjuns.tistory.com

https://velog.io/@dankim/Lambdaedge-cloudFront-S3

 

Lambda@edge + cloudFront + S3

순서대로 해봅시다. 1) IAM 정책 및 역할 생성 1. IAM 정책 생성 정책 선택 정책 생성 선택 JSON 선택 및 아래 정책 입력 정책 검토 선택 정책 이름 ResizingImagePolicy 또는 원하는 이름 선택 (설명 옵션)

velog.io

https://lemontia.tistory.com/1001

 

[aws] lambda@edge 설정 중 파라미터(query string)이 넘어오지 않는 경우(이미지 리사이징)

결론만 말하자면 frontcloud에서 query string을 받을 수 있도록 설정해야한다. lambda@edge 를 사용하는 경우 frontcloud와 함께쓰는 경우가 많은데 파라미터를 통해 크기를 조절하는 경우가 많다. 그런데

lemontia.tistory.com


Lambda에서 사용한 Sharp에 대한 참고 자료이다. (option: fit)

https://sharp.pixelplumbing.com/api-resize

 

sharp - High performance Node.js image processing

 

sharp.pixelplumbing.com

 

https://inpa.tistory.com/entry/NODE-%F0%9F%93%9A-Sharp-%EB%AA%A8%EB%93%88-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-%EC%9B%8C%ED%84%B0%EB%A7%88%ED%81%AC-%EB%84%A3%EA%B8%B0#%EC%9D%B4%EB%AF%B8%EC%A7%80_%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95_api_%EC%84%9C%EB%B9%84%EC%8A%A4_%EB%A7%8C%EB%93%A4%EA%B8%B0

 

[NODE] 📚 Sharp 모듈 사용법 - 이미지 리사이징 / 워터마크 넣기

Sharp 모듈 노드 진영에는 많은 이미지 리사이징 패키지들이 있었지만, 끝까지 살아남은 모듈이 shap 이다. 이미지 리사이징 동작 자체가 cpu와 메모리를 잡아먹는 주범이라, 가끔 out of memory로 node

inpa.tistory.com