CloudFront와 Lambda@Edge를 이용한 이미지 리사이징

su-mmer·2021년 12월 9일
8

AWS

목록 보기
2/4
post-thumbnail

목차

  1. 이미지 리사이징을 하는 이유
  2. 기능 설명
  3. S3 버킷 만들기
  4. Cloud Front 생성(배포)
  5. IAM 생성
  6. Lambda 함수 생성
  7. Cloud9 생성 및 설정
  8. Lambda@Edge 배포
  9. 추가적으로 해 볼만한 것
  10. 참고한 자료

이미지 리사이징을 하는 이유

사이트가 크고 이미지가 많아질수록 원본 크기대로 이미지를 불러오게 되면 고화질 이미지일수록 이미지 처리가 오래 걸려 로딩 시간이 길어진다.
또한 이미지 생성과 삭제가 많아질수록 이미지 작업에 따른 비용도 늘어난다.따라서 원본 이미지보다 리사이징 된 작은 용량의 이미지를 불러오는 것이 더 효율적이다. 따라서 이미지를 리사이징하는 것을 CloudFront와 Lambda 함수를 사용하여 실습해보고자 한다.

기능 설명

개요

  1. 클라이언트가 썸네일 이미지를 요청한다.
  2. 캐싱되지 않은 이미지라면 CloudFront가 S3로 이미지를 요청하고 S3에서 응답을 받는다. 여기서 Origin Response를 조작할 수 있다.
  3. 요청 받은 이미지가 S3에 존재한다면 Lambda 함수에 작성한 코드를 통해 원본 이미지를 리사이징한다.
  4. 리사이징한 이미지를 CloudFront에 전달하여 최종적으로 클라이언트에 썸네일을 제공한다.

S3

이미지를 저장하는 스토리지

Cloud Front

이미지 캐싱 및 Lambda 함수 배포 역할
정적 및 동적 웹 콘텐츠를 사용자에게 더 빨리 배포하도록 지원하는 웹서비스
엣지 로케이션의 전 세계 네트워크를 통해 콘텐츠 제공

IAM

Lambda 함수가 각 요소를 실행하기 위한 권한을 주는 역할 생성

Cloud9

Lambda 함수를 작성하기 위한 AWS에서 사용가능한 브라우저 IDE
Lambda 함수와 연결하여 코드 편집

Lambda

리사이징 할 때 캐시를 받고 S3에서 요청 받은 이미지를 불러오고 이미지를 리사이징하는 함수 작성

Lambda@Edge

Aws CloudFront의 기능 중 하나
Lambda 함수를 복제하여 CloudFront의 각 Edge Locatio에 전역으로 배포하는 역할
클라이언트가 어떤 리전에서 요청하던지간에 Lambda@Edge 함수를 거쳐 응답받게 된다.
따라서 코드를 사용자와 가까운 위치에서 실행하여 성능을 개선하고 시간을 단축시킬 수 있다.

S3 버킷 만들기

S3에서 버킷을 생성한다. 이름과 리전을 선택하고

CloudFront에서 S3에 접근하는 로그를 관측하기 위해 ACL 설정을 활성화해주어야 한다.

참고
1. AWS CloudFront Docs
2. 표준 로그 구성 및 사용
3. ACL 구성

외부(링크)에서 접근해야 하기 때문에 액세스 차단을 열어준다.

버전 관리로 S3 버킷에 저장된 모든 객체의 버전을 보존, 검색, 복원 할 수 있다.

객체(폴더)를 생성하고 내부에 테스트용 사진을 넣어둔다.

CloudFront 생성(배포)

CloudFront 생성 -> 원본 도메인으로 아까 만든 버킷 선택
이름은 자동 생성된다.
OAI를 사용하여 CloudFront를 통해서만 내부 사진에 접근할 수 있도록 한다.
OAI는 새로 생성해준다.

프로토콜은 Redirect로 설정해준다.

Legacy ache settings로 설정해 주소창에서 크기를 변경할 수 있도록 한다.

로그를 관측하기 위해 켜주고 s3 버킷을 설정하고 쿠키 로깅도 켜준다.

CloudFront 동작 생성

dev/* 패턴을 주어 dev/로 시작하는 모든 경로의 사진들이 해당되게 한다.

CloudFront 만들때와 동일한 옵션들로 구성한다.

로그도 활성화시켜준다.

같은 방법으로 pro/*도 만들어준다

""
여기까지 하고 CloudFront의 도메인 주소를 복사하여 사진이 제대로 뜨는지 확인한다. 이때 반드시 CloudFront가 활성화되어있는지 체크해야 한다. 아직 비활성화 상태라면 활성화가 될 때까지 조금 기다려준다.


xxxxxcloudfront.net/dev/사진이름

IAM 생성

정책 생성

IAM에서 먼저 정책을 생성해준다.

{
  "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": "*"
        }
    ]
}

JSON 붙여넣고 이름 기입하고 생성

역할 생성


내가 만든 정책 선택 하고 이름 쓰고 역할 생성 완료

신뢰관계 편집

Json의 Service부분 수정

"Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]

Lambda 함수 생성

Lambda 함수를 생성해준다.

기존 역할(IAM)을 사용한다.

Lambda함수에서 구성 - 일반구성 - 편집

Cloud9 생성 및 설정

이름 기입하고 큰 코드를 작성하는 것이 아니니 t3.nano로 설정해준다.

완료하여 생성하면 몇 분 기다리면 화면이 바뀐다.
왼쪽 aws 로고(aws-explorer)에서 아까 만든 Lambda 함수를 내가 만든 폴더(Cloud9이랑 같은 이름)에 Download 해준다.

알아서 Resizing_Lambda폴더(내가 만든 Lambda 함수랑 같은 이름)가 생성되고 내부에 index.js 파일을 볼 수 있다

sharp 패키지 설치

ec2-user:~/environment $ cd Resizing_Lambda
ec2-user:~/environment/ResizeImage $ npm init -y
...
ec2-user:~/environment/ResizeImage $ npm install sharp 
ec2-user:~/environment/ResizeImage $ npm install aws-sdk 

npm init -y로 초기화시 node_modules 폴더 및 package.json 자동 생성

index.js 수정

'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({
  signatureVersion: 'v4',
  region: 'ap-northeast-2'  // 버킷을 생성한 리전 입력
});

const BUCKET = '[mybucket]' // Input your bucket

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

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 { uri } = request;
  const ObjectKey = decodeURIComponent(uri).substring(1);
  const params = querystring.parse(request.querystring);
  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();
  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)

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

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

  try {
    resizedImage = await Sharp(s3Object.Body)
      .resize(width, height)
      .withMetadata()
      .toFormat(format, {
        quality
      })
      .toBuffer();
  }
  catch (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);
};

14번째줄 처음에 생성한 버킷 이름 입력 '버킷(S3)이름'

aws cloud9 ide 참고링크
https://docs.aws.amazon.com/ko_kr/ko_kr/cloud9/latest/user-guide/lambda-toolkit.html

작성이 끝났으면 ctrl+s를 눌러 index.js 파일을 저장하고
다시 aws-explorer로 들어가 Lambda함수를 Upload 해준다.
두번째 방법으로 업로드 해주었다.
(업로드 방식을 묻는 메시지에서 Yes를 선택하면 됨)

Lambda@Edge 배포

다시 Lambda 함수로 돌아와서 Lambda@Edge를 배포한다.
dev와 pro 모두 배포해준다.

배포시 함수에 CloudFront가 연결된 것을 볼 수 있다.
배포 후 활성화 될 때까지 5분 정도 기다려야 한다.

이제 사진이 리사이징 되는지 확인해본다.

xxxxxxx.cloudfront.net/dev/test.jpg?h=800

입력하면 높이가 800인 사진을 볼 수 있다.. . !!
h=800

h=400

f=webp&h=400

이미지를 리사이징하고 S3에 저장하지 않는 이유(On-The-Fly 방식)

모든 썸네일을 S3에 저장하면 S3 용량이 증가하여 비용도 증가한다. 또한 클라이언트에서 필요한 이미지 형식이 변경될 경우 모든 썸네일을 다시 재생성해야 한다.
또한 서버의 데이터 전송량이 증가하면 CloudFront의 요금도 증가하게 된다.
이에 대한 해결 방안으로 On-The-Fly 이미지 리사이징 방식을 사용하는데, 클라이언트에서 썸네일을 요청하면 실시간으로 이미지를 리사이징하여 제공하는 방식이다.
이미지가 업로드되고 처음 이미지를 요청하는 클라이언트는 이미지를 리사이징하는 시간을 기다려야하지만 CDN에 이미지가 한번 캐싱되면 다음부터는 썸네일 요청 시 바로 CDN에서 캐싱된 썸네일을 제공하기 때문에 사용자가 빠른 시간내에 이미지를 받아볼 수 있다.
(출처: AWS Lambda@Edge에서 실시간 이미지 리사이즈 & WebP 형식으로 변환)

추가적으로 해 볼만한 것

  1. 웹페이지에 연결시켜서 썸네일은 리사이징해서 띄우기
  2. 사진을 리사이징하여 이미지 크기에 따라 S3에 저장

고찰

오늘도 뿌듯하다 뿌-듯
아쉬웠던 점은 aws Academy(교육용)에서 IAM이 지원되지 않아서 결국 새 계정을 파서 프리티어로 이 과정을 했다.. 과금이 조금.. 무섭다...^^

0개의 댓글