본문 바로가기
공부/프로그래밍

[aws] lambda@edge 를 이용해 이미지 리사이즈 하기(cloud9 사용)

by demonic_ 2021. 2. 12.
반응형

여기서는 CloudFront를 사용한다고 전제되어 있으며 lambda@edge를 이용해 처리하는 방법이다.

(CloudFront 란 AWS에서 제공하는 CDN서비스)

 

단계는 다음과 같다.

1) Cloud9 생성 및 코드 작성

2) IAM 등록

3) 만든 역할을 Lambda에 등록하기

4) Lambda@Edge 배포 & 로그 확인

 

그럼 시작.

 

 

Lambda@edge를 배포하려면 '버지니아 북부' 리전의 Lambda만 가능하다. 그래서 리전을 먼저 이동한다.

AWS Console => Lambda 로 이동한 후 '함수 생성' 버튼을 클릭한다.

 

이미지 리사이징을 위한 lambda를 만들때 nodejs 버전을 10로 한다.(14는 아직 미지원)

Lambda 파일은 Cloud9을 이용해 만들었다.

 

 

 

1) Cloud9 생성 및 코드 작성

Cloud9 이 아닌 로컬에서 개발한다면 아래 설정은 보지 않아도 된다.

Cloud9 환경을 설정해보자. 이것 역시 서울리전은 불가하니 버지니아 북부 리전으로 만든다.

AWS Console => Cloud9 으로 이동한다.

 

Create enviroment 를 클릭한다.

 

이름을 적당히 입력하고 넘어간다.

 

가장 기본옵션(+ 싼거)로 인스턴스를 띄운다.

다 사용하고 나서 인스턴스는 반환을 잊지 않도록 한다. 안그러면 돈나간다.

(절전모드가 있긴 한데 굳이 유지할 이유도 없기 때문)

 

Create enviroment 를 클릭해 생성을 마무리한다.

생성되면 다음 화면을 볼 수 있다. 아래가 터미널 영역이고, 왼쪽이 파일 디렉토리 구조다.

 

Cloud9의 좋은점은 S3나 Lambda에 설정되어 있는 코드를 호출할 수 있다는 점이다. Import와 upload를 마우스 클릭으로 쉽게 할 수 있다. 여기서는 Lambda에 등록되있는 코드를 Import해볼 예정이다.

 

왼쪽에 Aws 로고를 클릭하면 북부리전에 등록되어 있는 서비스들이 뜬다. 여기서 Lambda를 클릭하면 생성한 Lambda 가 보이는데 마우스 오른쪽 버튼을 눌러 Import를 누른다.

import할 위치를 묻는다. testCloud9 를 클릭한다.

 

 

다시 왼쪽의 폴더처럼 되어있는 아이콘을 클릭하면 import 된 것을 확인할 수 있다.

 

리사이징 기능을 수행할 node.js 파일을 만든다

아래 터미널에서 해당 폴더로 접근한다.

cd ResizeImage-v2

sharp 패키지를 설치한다.(이미지 처리 패키지)

npm i sharp

그리고 다시 왼쪽 화면에서 해당폴더에 마우스 오른쪽 버튼을 눌러 New File을 클릭 => index.js 파일을 생성한다.

 

index.js 파일에 다음의 내용을 추가한다

(아래 코드의 출처는 아래 블로그)

https://devhaks.github.io/2019/08/25/aws-lambda-image-resizing/

 

[AWS] CloudFront Lambda@edge 를 이용한 이미지 리사이징

이번 글에서는 S3 에 있는 이미지를 Lambda@edge 를 통하여 리사이징 하고 새로운 이미지를 CloudFront 를 통하여 Caching 해보도록 하겠습니다.

devhaks.github.io

 

 

버킷에 들은 파일의 확장자가 반드시 다음중 하나여야 제대로 작동한다

- ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'tiff']

 

만약 확장자 없이 업로드 하는거라면 이 다음 스크립트를 사용하는게 좋다

'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 = '[버킷 이름]' // 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;
  
  // console.log("request: ", request)
  // console.log("response: ", response)
  
  // Parameters are w, h, f, q and indicate width, height, format and quality.
  const { uri } = request;
  // console.log("uri: ", uri)
  
  const ObjectKey = decodeURIComponent(uri).substring(1);
  const params = querystring.parse(request.querystring);
  const { w, h, q, f } = params
  
  // console.log("whqf: ", w,h,q,f)
  
  
  
  /**
   * 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() // 이미지 크기조절시 임의로 이미지 회전하는 상황 방지
      .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);
};

 

 

이미지 확장자가 없는 버킷을 사용하는 경우(비추천)

- 가급적이면 버킷 내 파일들에 확장자를 포함하는게 나중을 위해 좋을 수 있다.

 

'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' //버킷 Region
});
const BUCKET = 'cpi-service';
 
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 filepath = uri.substring(1)

  // 만약 이미지 파일로 확장자가 되어있다면 아래 사용
  // 아래 s3 에서 getObject 하는곳에도 수정해야 함
  //const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);
  
  //console.log("imageName: ", imageName)
  //console.log("extension: ", extension)
  
  //이미지 파일이 아니라면 썸네일 굽지 않음
  // if (extension !=='jpg'&& extension !=='jpeg' && extension !=='webp' && extension !== 'bmp' && extension != 'png') {
  //   console.log('not image file requested!');
  //   return callback(null, response);
  // }
 
  // 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 : 'webp'; //따로 포맷형태를 주지않으면 webp로 변경
  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)
      Key: filepath
    }).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);
};

 

 

그럼 모두 저장한 다음 다시 왼쪽의 창에서 Aws => lambda => [Lambda이름] 마우스 우클릭 => Upload Lambda 를 클릭한다.

 

다음 창에서 Zip과 Directory를 묻는데 내 경우 Directory를 클릭했다.

 

지금까지 작업한 폴더를 클릭, 아래 오픈을 클릭한다.

 

왼쪽 창에서 아래를 선택(Yes AWS ... )

 

코드 즉시 배포를 묻는다. Yes를 누른다.

 

AWS consle => Lambda에 들어가보면 함수코드가 다음과 같이 변경되어 있다.

 

함수 내보내기를 통해 다운받은 후 받은 파일과 upload 한 코드가 일치하는지 확인한다.

이제 Cloud9 콘솔창을 닫는다. (문제가 없으면 AWS Console => Cloud9 에 들어가서 testCloud9 을 삭제한다)

 

 

 

2) IAM 으로 역할 추가

lambda@edge 배포를 클릭하면 팝업이 뜬다. 설정 후 배포버튼을 누르면 edgelambda.amazonaws.com 에 수임해야 한다고 뜬다.

 

IAM을 통해 권한을 추가해야 한다.

IAM 이동 => 역할 => 역할 만들기를 클릭한다

 

Lambda를 클릭한 뒤 다음을 클릭한다.

 

정책 설정을 클릭한다

 

다음 화면에서 json 탭을 클릭해 다음을 입력한 후, 정책검토를 누른다

 

아래를 복사한 다음 붙여넣어 설정을 완료한다

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

 

위 설정에 따른 항목들이 다음처럼 보인다.

이름을 resize_policy로 입력하고 생성완료 한다

 

그럼 이전 연결할 정책을 선택하는 화면이 나오는데 여기서 지정한 이름을 검색, 선택한 후 다음을 누른다

 

완료를 눌러 역할을 만든다.

 

왼쪽 역할 탭을 클릭해 입력한 resize로 검색하면 만든 역할이 보인다. 클릭한다.

 

두번째 탭인 신뢰관계를 누른 후, 신뢰 관계 편집을 클릭한다.

 

다음을 입력한 후 업데이트 한다.

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

 

 

 

3) 만든 역할을 Lambda에 등록하기

이제 람다로 다시 돌아가서 권한을 변경한다.

Lambda의 권한 탭으로 이동한 후 실행 역할에 편집을 클릭한다

 

기존 역할을 새로 만든 ResizeImage로 변경한다

 

 

 

4) Lambda@Edge 배포 & 로그 확인

'작업 > Lambda@Edge' 배포를 클릭한다.

 

다음처럼 설정한다. 설정 중 CloudFront 이벤트에서 '오리진 응답' 으로 설정해야 한다.(기본값은 오리진 요청이니 반드시 수정해주자)

 

모니터링 탭으로 이동하여 CloudWatch에서 로그보기 버튼을 눌러 로그창으로 이동한다.

(중간에 사정이 있어 함수 이름을 ResizeImage => ResizeImage-v2 로 변경하였습니다)

 

CloudWatch로 접속하면 다음과 같은 에러가 발생한다. 이유는 리전때문이다.

 

위의 lambda가 저장되어 있는 리전은 '버지니아 북부' 인데, lambda 자체를 만드는건 서울리전에도 가능하지만 배포할 때 Lambda@edge 로 배포하는 것은 버지니아 북부 리전밖에 되지 않는다.(만들때 리전을 버지니아 북부로 이동한다음 만들어야 함)

 

서울 리전으로 설정하고 Lambda를 만들면 다음과 같이 Lambda@edge 버튼이 없다.

(참고로 CloudFront 도 글로벌 리전이라 표기되어 있다)

 

CloudWatch로 바로이동하면 보이지 않지만 왼쪽메뉴에서 '로그 => 로그 그룹'을 클릭하면 확인할 수 있다.

 

그럼 cloudfront의 도메인을 이용해 호출해보자

파라미터에 w와 h를 각각 입력한다

w는 width, h는 height의 약자로 썼으며 CloudFront에서 해당 파라미터를 화이트리스트로 등록하여 받았다.

아래처럼 파라미터를 구성한 뒤 브라우저에서 실행해보자.

https://[cloudfrontId].cloudfront.net/[이미지경로]?w=150&h=100

 

아래는 리사이징 성공 결과 스샷

 

성공한 경우 CloudWatch에 아래와 같이 로그가 남는다.

 

만약 파라미터를 입력해도 적용되지 않는다면 다음 포스팅을 참조

lemontia.tistory.com/1001

 

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

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

lemontia.tistory.com

 

 

끝.

 

 

참조:

https://devhaks.github.io/2019/08/25/aws-lambda-image-resizing/

 

[AWS] CloudFront Lambda@edge 를 이용한 이미지 리사이징

이번 글에서는 S3 에 있는 이미지를 Lambda@edge 를 통하여 리사이징 하고 새로운 이미지를 CloudFront 를 통하여 Caching 해보도록 하겠습니다.

devhaks.github.io

https://heropy.blog/2019/07/21/resizing-images-cloudfrount-lambda/

 

AWS Lambda@edge로 실시간 이미지 리사이징(updated)

AWS Lambda@edge(CloudFront)로 실시간 이미지 리사이징 기능을 구현합니다. Cloud 9으로 람다 함수를 작성하고 CloudWatch로 로그를 확인합니다.

heropy.blog

https://engineering.huiseoul.com/lambda-%ED%95%9C%EA%B0%9C%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-on-demand-image-resizing-d48167cc1c31

 

Lambda 한개로 만드는 On-demand Image Resizing

Lambda 단! 한개로 만드는 On-demand Image Resizing

engineering.huiseoul.com

 

반응형

댓글