Browsing Tag

cloudfront

AWS

[AWS] image resizing failure, trouble shooting

안녕하세요. ManVSCloud 김수현입니다.

오늘은 이전에 실패했던 image resizing 실패 원인을 확인 후
정상적으로 resizing 성공 후기를 남겨보려고 합니다.


First Cause

첫번째 원인을 찾았습니다.
CloudFront 트리거 구성 시 Origin Response가 되어야하는데 Request로 설정을 해두었던 것!

Lambda에서 변경 시 위에서 오리진 응답으로 변경하면 됩니다.

만약 Cloudfront에서 변경을 하실 경우 연결된 cloudfront에서 [Behaviors]-[경로 선택 및 Edit] 후 아래 CloudFront Event에서 Origin Response로 변경해줄 수 있습니다.

이 부분은 리눅서님의 도움을 받았습니다.
해당 부분이 Response가 되어야하는 이유는 인지하고 있었는데 다시 한번 포인트를 잡아주시고 놓치고 지나친 부분 찾아주신 리눅서님에게 감사의 인사 올립니다.


Find hints

위 첫번째 원인을 해결하고도 정상적으로 이미지 리사이징이 되지 않았습니다.
그래서 현재 이미지 리사이징이 어떻게 동작하고있고 특정 오류가 발생하는가 확인해보기로 했습니다.


우선 CloudFront의 Cache statistics와 Monitoring을 체크해보았고
각 오류 코드 및 Header값을 확인해보았습니다.

3xx의 정체는 301, 2xx는 200, 4xx는 403으로 보입니다.

http://주소/경로/tom.png?w=110&h=60&f=webp&q=90 [Viewer Request]
Status Code 로301 Move Permanently를 받으며 리다이렉트 되었습니다.
X-Cache : Redirect from cloudfront

https로 리다이렉트 되고 Status Code로 200을 받았습니다.
x-cache : Miss from cloudfront 엣지 로케이션 캐시에 tom.png?w=110&h=60&f=webp&q=90가 없어 Origin에 요청합니다. 여기서 Origin은 tom.png 이미지가 있는 S3 버킷!
이미지도 잘 데려왔습니다.

그런데 이미지 리사이징이 되지않은 상태로 넘어왔습니다.
User가 Cloudfront로 “Viewer request” 했고 (1. User → Cloudfront)
Miss from cloudfront라 Cloudfront가 Origin에게 “Origin request” 하고 (2. Cloudfront → Origin)
Origin으로부터 “Origin reponse” 후 (3. Origin → Cloudfront)
Cloudfront가 다시 User에게 “Viewer response” (4. Cloudfront → User) 되어
이미지를 받았다라고 했을 때 [1], [2], [4] 과정은 문제가 없었을 것이라 생각됩니다.

[1],[4] 요청과 응답에 대한 통신은 정상적으로 되었으며 Miss from cloudfront 응답에
[2] CloudFront는 Origin에게 요청을 했을 것입니다.

그렇다면 [3] 과정에서 정상적으로 동작하지 않는 것이 있다는 것인데?

Cloudfront 설정은 몇번을 반복하여 보아도 더 이상 잘못 설정한 것이 없었습니다.
그래서 Lambda 설정을 조금 더 검토해보기로 했습니다.

Second Cause

원인을 전혀 못찾다가 사내 상사의 도움으로 “Node.js 버전에 맞는 소스 코드를 사용했는가? 소스 코드가 버전에 맞지않아서 그럴 수 있다”라는 힌트를 얻게 되었습니다.

그런데 아무리 생각해봐도 다른 설정은 잘 되어있고 정말 소스 코드쪽이 크게 의심되어 바로 버전과 소스 코드 변경을 진행하였습니다.

Node.js 버전은 12.x 버전으로 변경하였고 소스 코드는 아래와 같이 수정하였습니다.

'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 = 'mybucket';

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 재배포하여 다시 한 번 리사이징 테스트를 해보았습니다.


Success

이미지 리사이징이 정상적으로 완료되었습니다!
Node.js 버전과 소스 코드 변경 후 정상적으로 리사이징이 되네요

이번 이미지 리사이징을 계기로 개발쪽 공부도 해야겠다는 생각이 드는데 바쁜 일정으로 올해는 무리입니다…

그래도 이미지 리사이징 성공할 수 있어서 뿌듯합니다.

긴 글 읽어주셔서 감사합니다.

AWS

[AWS] CloudFront 캐시 적중률 늘리기 (cache hit ratio)

안녕하세요. ManVSCloud 김수현입니다.

오늘은 ANOS 2기 스터디 후 6주차 CloudFront 과정을 마친 후 예정되어 있던 포스팅으로
CloudFront 캐시 적중률 늘리기에 대한 주제로 글을 써보려합니다.

이 포스팅을 준비하기 위해 생각보다 많은 공부를 하게되어 생각보다 시간이 소요되었지만 평소한 것들보다 만족스럽다는 생각이 들었습니다.

그럼 [AWS] CLOUDFRONT 캐시 적중률 늘리기 (CACHE HIT RATIO)에 대해 알아보겠습니다.


What is Cache?

캐시 메모리, 캐시 저장소, 캐시 서버 등 캐시는 무엇을 말하는 것일까요?
캐시는 쉽게 말하면 임시로 저장해두는 장소라고 말할 수 있겠습니다.

이 캐시로 인해 우리는 평소보다 조금 더 빠르게 접근이 가능하게되는데
Cache Hit 상태라면 우리가 원하는 것을 Cache가 가지고 있는 것이며
Cache Miss는 Cache가 아직 그것을 가지고 있지 않는 상태입니다.

Cache에 대해서는 이 정도로만 간단히 설명하고 다음으로 넘어가겠습니다.


What is CloudFront?

Fast or Slow 사이트에서 제 테스트 웹 페이지 하나에 대해 속도 측정을 해보았습니다.
(위 링크에서 여러분들의 웹 사이트를 속도 측정 할 수 있습니다. (해외 차단 시 불가))

출발한 패킷이 해외 지역에 도달하는데에 걸리는 시간이 각각 다른 것을 알 수 있습니다.
우리는 이 시간을 Latency라고 부르며 출발지와 거리가 멀 수록 Latency가 크게 발생하게 됩니다.

아프리카와 유럽쪽의 응답시간이 다른 지역에 비해 더 늦다는 것을 확인할 수 있습니다.
반면 한국과 가까운 일본이나 홍콩은 다른 지역보다 빠릅니다.

그렇다면 해외 서비스를 운영할 때 발생하는 컨텐츠의 느린 응답 속도를 해결할 수 있는 방법은 없는 것일까요?

그렇지 않습니다. 이를 해결하기 위해 Contents Delivery Network(CDN)을 이용한다면
지리적 또는 물리적으로 떨어진 대상에게 컨텐츠를 빠르게 제공이 가능해집니다.

CDN 사이트 테스트 시 이전 사이트의 Latency에 비해 비교적으로 낮아진 값을 확인할 수 있었습니다.
또한 CDN 업체의 각 CDN Node로 인해 보다 빠른 통신이 가능하다는 것을 알 수 있었습니다.

CDN을 사용한다면 해외 사용자가 컨텐츠를 다운로드 할 때 Origin에서 직접 컨텐츠를 가져오지 않고 주변 CDN 서버에서 컨텐츠를 가져오므로 속도 보장은 물론 국제 회선으로 발생하는 비용 역시 절감할 수 있게 됩니다.

CloudFront는 CDN 서비스를 제공합니다. 또한 CloudFront는 엣지 로케이션 215개 이상, 리전별 중간 티어 캐시 12개의 글로벌 네트워크를 사용하여 캐싱을 통해 짧은 지연 시간과 높은 처리량 및 안정적인 네트워크 연결(부하 분산)이 제공됩니다.

이런 CDN에 대해 관심이 많으시다면 GSLB 기술에 대해 한 번 알아보는 것도 나쁘지 않을 것같아 링크 추가해봅니다.

세계 지도 png에서 kor.pngtree.com

Cloudfront의 전체를 그림 하나로 표현하기에 부족하겠지만 한 번 만들어보았습니다.

캐싱 데이터가 생성된 Edge로 인해 안전성, 비용, 속도를 보장 받을 수 있으니 Cloudfront는 무조건 사용해야한다?, Cloudfront만 사용하면 무조건 빠르다? 는 정답이 될 수 없습니다.

무조건 사용하지 않습니다. 잘 사용해야합니다.
또한 서비스의 속도는 오직 캐싱만으로 결정되지 않습니다.
Rendering, 압축, 개발 소스 등 역시 속도의 원인이 될 수 있으며 최신화를 위해 정적/동적 데이터 구분하여 설정 및 캐싱 예외 처리, TTL 등 고려해야할 사항이 많습니다.

하지만 이 포스팅의 주제는 Cloudfront의 캐시 적중률을 어떻게하면 높일 수 있을까에 대한 주제이므로 캐시 적중률을 높일 수 있는 방법에 대한 내용을 중점으로 다루겠습니다.


How can CloudFront cache hit rate be increased?

CloudFront의 캐시 적중룔, 어떻게하면 높일 수 있을까요?

AWS 가이드에서는 총 7가지가 있다고 합니다.

CloudFront에서 객체를 캐시하는 기간 지정
Origin Shield 사용
쿼리 문자열 매개 변수를 기반으로 한 캐싱
쿠키 값에 따른 캐싱
요청 헤더 기반 캐싱
압축이 필요하지 않은 경우 Accept-Encoding 헤더 제거
HTTP를 사용하여 미디어 콘텐츠 제공

7가지 중 3가지 정도만 자세히 알아보도록 하겠습니다.
(다소 양이 방대하기때문에 나머지는 최하단 Docs 링크 추가해두었으니 참고 부탁드립니다.)


▶ CloudFront에서 객체를 캐시하는 기간 지정

첫번째 방법으로 CloudFront에서 객체를 캐시하는 기간을 지정해주는 것입니다.
Cache-Control max-age의 수치가 길면 길수록 캐시 적중률이 높아집니다.
다만 오리진이 아닌 캐시된 파일을 불러오기에 객체 최신화에 문제가 있습니다.
그렇다면 Cache-Control max-age 수치를 어떻게 주면 좋을까요?

우선 TTL과 Cache-Control에 대해서 알아봅시다.

브라우저 캐시 TTL은 최종 사용자 브라우저가 리소스를 캐시하는 시간입니다.
이 리소스의 경우 TTL이 만료 될 때까지 브라우저 로컬 캐시에서 제공하며 만료 시 브라우저가 재요청을 하게됩니다. 이때 TTL은 해당 컨텐츠가 있는 원본 서버의 Cache-Control을 참고하게 됩니다. 예를들어 원본 서버에서 Cache-Control 설정이 public; max-age=60로 설정되어있을 경우 60초의 시간 동안 브라우저 로컬 캐시에 저장됩니다.

Cache-Control은 응답을 캐시할 수 있는 사용자와 해당 조건 그리고 기간을 제어하는데
즉, 웹 컨텐츠 캐시 정책을 컨트롤 하는 지시문이라 볼 수 있습니다.

Cache-Crontrol에 대한 링크를 추가하였으니 참고하시면 좋을듯합니다.

Cache-Control 설정은 웹 사이트 성능 최적화에 중요한 역할을 합니다.
Cache-Control은 컨텐츠 유형별로 다른 설정을 해준다면 웹 사이트 성능 최적화에 좋은 결과를 기대할 수 있을 것입니다.

컨텐츠 유형은 크게 정적 컨텐츠(Static Contents)와 동적 컨텐츠(Dynamic Contents)로 나뉩니다.

정적 컨텐츠는 css, js, png, jpg, html 파일 등이 대표적이며, 동적 컨텐츠는 업데이트가 잦아 항상 최신화가 필요하거나 요청마다 다른 응답을 주어야하는 리소스들이 되겠습니다.

정적 컨텐츠는 제가 포스팅 시 업로드한 이미지,영상 파일들이나 우리가 흔히 게임을 하기위해 다운로드를 받는 소프트웨어의 경우 TTL이 높아도 상관이 없습니다.
다만 우리가 보는 뉴스 기사들은 TTL이 높을 경우 빠르게 최신 뉴스를 볼 수 없게 될 것입니다. 그러므로 각 컨텐츠 별로 다르게 설정적 컨텐츠는 제가 포스팅 시 업로드한 이미지,영상 파일들이나 우리가 흔히 게임을 하기위해 다운로드를 받는 소프트웨어의 경우 TTL이 높아도 상관이 없습니다.다만 우리가 보는 뉴스 기사들은 TTL이 높을 경우 빠르게 최신 뉴스를 볼 수 없게 될 것입니다. 그러므로 같은 정적 컨텐츠라도 다르게 설정해줄 필요가 있겠습니다.

제 블로그에 올라간 이미지 파일들은 이미지가 변경되어야 할 이유가 크게 없습니다.
이 경우 아래와 같은 예시로 Cache-Control을 사용할 수 있겠습니다.
예시) max-age = 31536000;(브라우저) 또는 s-maxage=86400(프록시와 같은 공유 캐시에만 적용)
max-age의 최대치는 31536000 입니다.

날씨와 같이 1분 주기로 최신화를 하고 싶다면 아래와 같이 할 수 있겠습니다.
예시) public; max-age=60 또는 no-cache; max-age=60 등의 방법

동적 컨텐츠는 항상 최신화가 필요할 것입니다.
항상 최신화를 위해 아래 예시의 방법이 있습니다.
예시) no-cache, max-age=0 또는 private, no-store 등의 방법

제 블로그 역시 Cache-Control이 적용되어있습니다.

Apache를 이용하여 Cache-Control 헤더를 사용한다면 .htaccess를 이용하여
아래와 같은 방법을 사용할 수 있습니다.

<filesMatch ".(ico|jpg|jpeg|png|gif)$">
 Header set Cache-Control "max-age=2592000, public"
</filesMatch>

<filesMatch ".(css|js)$">
 Header set Cache-Control "max-age=86400, public"
</filesMatch>

제 블로그의 이미지 파일들은 AWS S3에 있습니다.
그렇다면 이 S3에 있는 이미지 파일들에게 Cache-Control 설정은 해줄 수 없을까요?

가능합니다.
우선 S3의 해당 버킷을 선택한 뒤Cache-Control을 설정할 파일 또는 디렉토리를 선택합니다.
그리고 [작업]-[작업 편집]-[메타데이터 편집]을 선택 클릭하면 메타데이터 편집을 통해
Cache-Control을 수정 및 설정할 수 있습니다.

자 그럼 이제 CloudFront에서 객체를 캐시하는 기간을 설정하는 방법에 대해 알아 보겠습니다.

Cloudfront의 [Behaviors]에서 설정할 수 있습니다.

또한 아래 사진처럼 Create Behavior로 Path Pattern을 나누어 우선 순위를 설정한 뒤 각 경로나 파일마다 다르게 설정할 수도 있습니다.

수정이 필요한 Behavior를 선택 후 [Edit]을 누르면 아래와 같은 창으로 이동합니다.
Cache and origin request settings을 선택해줄텐데 직접 수정하려면 아래
[Use legacy cache settings]를 선택해주어야합니다.

[Use a cache policy and origin request policy]를 선택하여 Managed-CachingOptimized를 사용할 경우 관리형 캐시 정책에 대한 아래 Docs를 참고하면 되겠습니다.

legacy cache settings를 선택할 경우 아래 추가로 입력할 수 있는 창이 생깁니다.
Object Caching을 Customize로 체크해줄 경우 아래 TTL 값을 원하는 값으로 수정이 가능하게됩니다.

* [참고] Cache Based on Selected Request Header가 요청 헤더 기반 컨텐츠 캐싱을 지정해주는 것이고 Query String Forwarding and Caching은 쿼리 문자열 매개 변수를 기반으로 한 캐싱, Forward Cookies가 쿠키 값에 따른 캐싱에 해당됩니다. 아래에서 Query String Forwarding and Caching에 대해 추가로 다룰 예정입니다.

지금까지 위에서 다룬 TTL 값을 변경하는 방법을 알아보았습니다.
다만 다음과 같은 특이 케이스가 있을 것입니다.

??? : ” 정적 컨텐츠였는데 지금 이미지 하나를 급하게 최신화 해야합니다. 하지만 Cloudfront에 기존 TTL 값이 너무 높게 설정이 되어있습니다. 어떻게 방법이 없을까요? “

있습니다. [Invalidation]을 사용한다면 캐시가 만료되기 전에 파일의 내용을 갱신할 수 있습니다. 이는 캐시 무효화 기능인데 CloudFront Edge 로케이션에 저장된 캐시를 삭제해줍니다.

여기까지 CloudFront에서 객체를 캐시하는 기간을 지정하여 캐시 히트율을 높이는 첫번째 방법에 대해 알아보았습니다.


▶ Origin Shield 사용

두번째 방법으로는 Origin Shield를 사용하는 방법이 있겠습니다.
Origin shield는 CloudFront 캐싱 인프라의 추가 계층으로 Origin 앞에 추가적인 캐싱 계층을 제공해줍니다. 모든 캐싱 계층에서 Origin에 대한 모든 요청이 Origin shield를 통과하기에 CloudFront 배포의 캐시 적중률을 개선하는데 큰 도움이됩니다.

또한 Origin의 부하를 최소화하며 가용성을 높여 운영 비용을 줄이는 데에 큰 도움이 됩니다.
캐시에 없는 컨텐츠에 대한 요청은 동일한 객체에 대한 다른 요청과 통합되어 Origin으로 가는 요청이 하나만 발생하게 되는데 이로 인해 동시 요청 수를 줄일 수 있으며 최대 로드, 트래픽 급증 중에 Origin의 가용성을 유지할 수 있고 동적 패키징, 이미지 변환 및 데이터 전송과 같은 비용을 줄일 수 있습니다.

Feb 20, Origin Shield 사용 전 (21일에 적용)
Feb 23, Origin Shield 사용 2일 후 (21일에 적용)

(포스팅 당일은 캐싱되지 않은 새로운 컨텐츠가 존재하여 cf-cache-status : MISS가 발생할 수 있습니다.)

제 블로그에 적용 당시 Origin shield입니다.
적용 후 그래프의 캐시 적중률(Hits)이 상승한 것을 볼 수 있습니다.

Origin Shield 설정 – 1
Origin Shield 설정 – 2

위와 같은 방법으로 Origin Shield 설정이 가능합니다.

그외에도 Origin Shield는 CloudFront의 리전 엣지 캐시를 활용하며 CloudFront 위치에서 Origin Shield 로 연결할 때 각 요청에 대해 활성 오류 추적을 사용하여 Default Origin Shield 를 사용할 수없는 경우 요청을 보조 Origin Shield 위치로 자동 라우팅하므로 고 가용성까지 갖췄습니다.


▶ 요청 헤더 기반 캐싱

마지막으로 요청 헤더 기반 캐싱에 대해 알아보겠습니다.

위에서 언급했듯 요청 헤더 기반 캐싱은 Behavior을 [Edit] 시Query String Forwarding and Caching를 선택할 수 있습니다.

이는 Query String을 캐시 구분자로 사용할지 물어보는 것입니다.

# None(캐시 향상) : 이를 선택할 경우 모든 쿼리스트링에 대해 캐시합니다.
# Forward all, cache based on whitelist : whitelist를 선택하여 값을 입력할 경우 whitelist에 입력된 쿼리스트링에 대해서만 캐시가 되지 않고 나머지는 캐시가 됩니다.
# Forward all, cache based on all : all을 선택하게 되면 캐시를 하지 않게됩니다.

None과 all은 캐시 된다/안된다이니 whitelist를 잘 써야봐야겠습니다.
아래 사진으로 예를 들어보겠습니다.

제 블로그는 도메인명 뒤에 ?p가 붙습니다.
ex)https://manvscloud.com/?p=612

만약 제가 포스팅되는 글들은 캐시되지 않고 오리진에서 최신화하여 불러왔으면 좋겠다고 했을 때 위와 같이 설정해줄 수 있습니다.
whitelist에 p를 넣어주면 https://manvscloud.com/? 뒤에 p의 쿼리스트링에 대해서 캐시하지 않고 항상 최신화하게 됩니다.

그렇다면 이 쿼리스트링을 여러개 사용해야한다면 어떤 방법이 있을까요?
예를 들어 이미지가 요청할 때마다 항상 다른 사이즈로 리사이징되어야 하는 경우입니다.

첫 요청 시 /?w=200&h=150&f=webp&q=90 으로 요청했는데
이후 /?w=100&h=300&f=webp&q=90으로 요청이 올 경우 바로 리사이징 된 이미지를 보여주어야합니다.

위와 같은 방법을 사용할 수 있습니다.
각 w,h,f,q 쿼리스트링에 대한 whitelist를 입력하여 해당 쿼리스트링 요청 시 캐싱되지 않고 오리진에서 불러올 수 있도록하는 것입니다.

이 방법을 사용한다면 원하는 것만 최신화 하고 나머지는 항상 캐싱되니
캐시를 잘 사용한다고 볼 수 있겠습니다.

캐시 적중률이 높다는 것은 hit율만 높고 최신화가 되지 않는 것을 말하는 것이 아닙니다.
최신화 되어야할 부분은 최신화 되면서 캐싱이 되어야 비로소 캐시가 잘 적중했다라고 생각됩니다.

지금까지 캐시 적중률을 높이는 3가지 방법에 대해 자세히 알아보았습니다.
이 외에도 나머지 4가지 방법이 더 있습니다.
제일 하단 링크에 추가된 링크를 통해 나머지 4가지 방법도 찾아보시길 바랍니다.


Opinion

검색 엔진 최적화(SEO)에는 웹 사이트 속도와 상관 관계가 있다고 합니다.
조사에 따르면 웹 페이지가 출력되는데에 4초 이상이 소요될 경우 75%의 사람들은 이미 해당 웹 사이트를 종료한다고 합니다.

캐시 적중률을 높여 웹 페이지의 속도를 빠르게 해보시는 것은 어떠신가요?
많은 분들에게 도움되는 포스팅이 되었길 바랍니다.

(원래 추가로 Cloudfront + Lambda@Edge를 활용한 Image Resizing에 대한 포스팅도 첨부하여 이미지 리사이징을 통해 SEO 최적화하는 방법을 더하려 했으나… 왜 안되는지 잘 모르겠습니다?… )

CloudFront의 캐시 적중률을 높여보자는 게 그만 내용이 꽤 길어졌습니다.
처음에 작성하려 했던 양보다 훨씬 많아졌네요…
7가지 중에 3가지만 했는데도 이 정도 양이니…
나머지 4개는 제가 여러분들에게 드리는 숙제인걸로…!
찾아보면서 공부하는 재미가 있더라구요😋 홧팅!!

다음 AWS 공부는 Elasticache나 Elastic Beanstalk을 생각하고 있지만 곧 NCP 시험과 Kubernetes 일정으로 인해 다음 AWS 포스팅까지는 다소 시간이 걸릴듯합니다.

긴 글 읽어주셔서 감사합니다.


References

AWS

[AWS] Cloudfront + Lambda@Edge를 활용한 Image Resizing

안녕하세요. ManVSCloud 김수현입니다.

오늘은 CloudFront와 Lambda@Edge를 활용하여 이미지 리사이징을 해보려합니다.
이전부터 해봐야겠다고 생각은 했으나 저는 무언가를 시작할 때 왜 이것을 해야 하는지,
이것을 함으로써 어떤 장점을 얻을 수 있는지를 먼저 알지 못하면 시작하지 못하는 편입니다.

이미지 리사이징는 왜 사용해야할까요?

최근에 나오는 휴대 전화와 디지털 카메라는 고화질로 촬영할 수있는 스펙을 가지고있어
웹 사이트에 업로드하기 적합하지 않을 때가 있습니다.

또한 여러 사용자들의 디바이스에서 요구하는 해상도가 달라 고정된 해상도의 이미지 파일 하나를 업로드 해둘 경우 최적화된 서비스를 할 수 없거나 여러 해상도마다의 이미지를 스토리지 서버에 올려두어 용량 낭비를 하게 될 수도 있습니다.

이런 케이스마다 이미지를 리사이징 해주는 기능이 있다면 좋을 것같습니다.
게다가 이미지 리사이징은 SEO(Search Engine Optimization)에도 큰 도움이 됩니다.
* SEO(Search Engine Optimization) : 검색 엔진 최적화

하지만 이미지 리사이징은 생각보다 간단하지 않습니다.
이미지의 크기를 변경한다는 것은 이미지의 품질이 손상 될 수 있지 않을까라는 고민을 하게 만들기도 합니다.

실습을 통해 이미지 리사이징이 어떻게 되는지 원리에 대해 알아보고
품질에 영향이 클지 등을 알아보도록 하겠습니다.
(실습의 결론부터 말하자면 잘 되지않았습니다…😥)


Principles of Image Resizing

<이미지 리사이징의 원리>

① URI에 원하는 해상도의 이미지를 요청합니다.
ex) https://img.manvscloud.com/image/tom.png?w=200&h=150&f=webp&q=90

② 캐시 서버(CloudFront/Edge Location)에서 캐싱된 이미지가 있으면 이미지를 전송하지만 캐시된 이미지가 없을 경우 이미지 리사이징 서버(Lambda@Edge)에 API 또는 SDK를 이용한 리사이징된 이미지를 요청합니다.

③ 이미지 리사이징 서버(Lambda@Edge)는 API나 SDK를 통해 이미지가 존재하는 스토리지 서버(S3)에 원본 이미지를 요청하게 됩니다.

④ 스토리지 서버(S3)가 이미지 리사이징 서버(Lambda@Edge)로 이미지를 전송합니다.

⑤ 이미지 리사이징 서버(Lambda@Edge)는 스토리지 서버(S3)에게 이미지를 받은 후 요청한 URI를 해석한 뒤 원하는 이미지로 리사이징하여 캐시 서버(CloudFront/Edge Location)로 전송합니다.

⑥ 요청한 해상도의 이미지를 GET 합니다.

원리는 대략 이런 식이라고 볼 수 있습니다.
그럼 이제 실습하여 실제로 이미지 리사이징이 잘 되는지 확인해보도록 하겠습니다.


IAM – Policy & Role

우선 IAM을 만들어보겠습니다.
Lambda를 Cloudfront 배포하고 연결하기 위한 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": "*"
        }
    ]
}
💡 역할 생성

[IAM] – [액세스 관리] – [역할] – [역할 만들기]
→ 사용할 서비스 : Lambda
→ 위에서 생성한 정책을 추가합니다.
→ 역할 만들기

역할 생성 후 생성된 역할의 신뢰 관계가 아래 그림과 같이 설정되어 있는지 확인합니다.

신뢰할 수 있는 개체가 그림과 같이 설정되어있지 않다면 [신뢰 관계 편집]을 이용하여
수정 후 업데이트 해줍니다.

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

S3 & CloudFront

💡 S3 버킷 생성

[Amazon S3] – [버킷 만들기]

S3 버킷 생성
image 라는 이름의 폴더를 생성하였습니다.

이후 image/에 tom.png 파일을 업로드 해두도록 하겠습니다.

💡 CloudFront 생성

[CloudFront] – [Create Distribution] – [Get Started]

Origin Domain Name에 위에서 생성한 S3를 선택해줍니다.

Enable Origin Shield는 이번 포스팅에서 다루지 않겠습니다.
이후 포스팅 될 “CloudFront Cache Hit Ratio” 를 참고해주세요.

Restrict Bucket Access를 “Yes”로 체크해주게 되면 버킷의 접근 제한을 활성화 하겠다는 의미입니다.
Origin Access Identity는 신규로 생성하여 사용하겠습니다.
Grant Read Permisstions on Bucket은 해당 S3 버킷 정책에 읽기 권한을 Update할거야? 하고 묻는겁니다.

Query String Forwading and Caching 설정이 Forward all, cache based on whitelist으로 되었다면
Query String whitelist에 쿼리 스트링 키를 지정해줄 수 있게 됩니다.

예를 들어 쿼리 스트링 키로 img를 주고 싶다면 Query String whitelist에 img를 입력하시면 됩니다.

오리진 서버에서 하나 이상의 쿼리 문자열 파라미터를 기준으로 다른 버전의 객체를 반환할 경우 필요합니다. (쿼리 문자열을 이용하여 캐시 우회 가능)

SSL Certificate를 Default로 하는 경우 CNAMEs 을 사용할 수 없습니다.
또한 Default는 보안 TLS 버전을 선택할 수 없습니다.

버지니아 북부 리전에서 ACM을 생성하여 Cloudfront의 Custom SSL Certificate를 선택할 수 있으니 이 방법을 적극 권장드립니다.

생성된 Cloudfront에서 Behaviors를 이용하면 S3 버킷내에 디렉토리 별로 옵션 설정이 가능합니다.

설정 이후 cloudfront를 이용하여 S3 버컷에 올려둔 이미지 파일이 보이는지 확인해보도록 하겠습니다.


Lambda@Edge (with Cloud9)

💡 Create Lambda

[버지니아 북부 리전] – [AWS Lambda] – [함수 생성] – [새로 작성]
(Lambda@Edge는 미국 동부(버지니아 북부)(us-east-1)만 허용됩니다.)

위 그림과 동일하게 이름/버전/역할을 정해준 뒤 [함수 생성]을 클릭해줍시다.
(역할은 위 <IAM – Policy & Role>에서 생성한 역할로 선택해줍니다. )

Entity오리진 요청 및 응답 이벤트 제한최종 사용자 요청 및 응답 이벤트 제한
함수 리소스 할당Lambda 제한과 동일128MB
함수 제한 시간30초5초
헤더 및 본문을 포함하여 Lambda 함수에서 생성되는 응답의 크기1MB40KB
Lambda 함수 및 포함된 라이브러리의 최대 압축 크기50MB1MB
💡 Cloud9

[AWS Cloud9] – [Create environment]

[Next step] – [Create environment]

잠시 후 Cloud9이 생성됩니다.

좌측 AWS 마크를 누른 후 AWS:Explorer에서 Show region in the Explorer를 클릭하여
Lambda가 있는 리전을 선택해주도록 합시다.

Lambda 함수를 생성 했던 US East (N. Virginia) 버지니아 북부 리전을 선택해줍니다.

좌측 버지니아 북부 Lambda에서 생성한 ImageResize 함수를 가져옵니다

위 사진에 이어 명령어를 입력하여 npm초기화와 설치를 진행해줍니다.

cd ImageResize  (디렉토리 이동)
npm init -y
npm install sharp 
npm install aws-sdk
'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);
};

코드는 아래 github 페이지를 참고하였습니다.

마지막으로 코드를 Lambda로 업로드합니다.

💡 Lambda@Edge

[AWS Lambda] – [생성했던 함수] – [작업] – [Lambda@Edge 배포]

https://도메인/image/tom.png?w=200&h=150&f=webp&q=90

가로 200px, 세로 150px, 품질 90% 이미지 출력을 요청하였지만 변하는게 없네요…
뭔가 잘 안된 것같습니다.

이후 다시 해보고 성공하는대로 해당 포스팅을 성공 후기를 추가하여 수정할 예정입니다.


Reference

위 사이트는 Lambda Tutorial 사이트 입니다.
괜찮아보여서 추가해두었습니다.

비록 오늘 실습은 실패하였지만 이후 다시 도전했을 때 성공하기를 바랍니다.
곧 성공한 포스팅으로 변경하여 돌아오겠습니다.

긴 글 읽어주셔서 감사합니다.