aws

[AWS] cloudfront + Lambda@edge 를 활용한 이미지 리사이징하기 3

Lo_gos 2023. 11. 29. 23:25

 

이제 마지막으로 이미지 리사이징을 위한 코드를 작성 및 배포를 진행하겠습니다. 코드는 node.js 로 작성 및 배포합니다. 이미지 리사이징은 라이브러리는 Sharp.js 를 사용하여 리사이징 할 것입니다.


 

버전 및 라이브러리 정보.

항목 설명
서버 node.js
버전 v16.x
이미지 라이브러리 Sharp 

 

프로젝트 세팅하기

 

1. 프로젝트 경로로 이동 후 소스폴더를 생성합니다.

# 소스 폴더 생성
mkdir /src

 

2. 이제 npm 을 통해 프로젝트 초기 설정을 하겠습니다.

# 프로젝트 초기화
npm init 

# Sharp 라이브러리 설치
npm install sharp

 

3. index.js 파일을 생성하고 코드를 작성해 보겠습니다.

"use strict";


// ❶ 필수 모듈
const querystring = require("querystring"); // Don't install.
const AWS = require("aws-sdk"); // Don't install.
const Sharp = require("sharp");

// ❷ S3 클라이언트, 버킷명 설정
const S3 = new AWS.S3({
  region: "ap-northeast-2",
});

// ❷-1. 버킷명 버킷명이 틀리면 오류가 발생할 수 있습니다.
const BUCKET = "lo-gos-test";

// ❸ 코드 시작 
// (아래 코드에서 사용하는 내부 함수는 생략했습니다. 주요 코드는 github 에서 확인해주세요.)

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;
  let [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);

  extension = extension.toLowerCase();

  if (isGif(extension)) {
    console.log("GIF image requested!");
    console.log("response, content-type", response.headers["content-type"]);
    // change content-type to image/gif
    response.headers["content-type"] = [
      {
        key: "Content-Type",
        value: `image/gif`,
      },
    ];

    return callback(null, response);
  }

  // Init variables
  let format;
  let s3Object;
  let resizedImage;

  try {
    s3Object = await S3.getObject({
      Bucket: BUCKET,
      Key: decodeURI(imageName + "." + extension),
    }).promise();
  } catch (error) {
    console.log("S3.getObject: ", error);
    return callback(error);
  }

  const origintLength = s3Object.ContentLength;
  console.log(`origin byteLength ${origintLength}`);

  try {
    const image = Sharp(s3Object.Body, {
      animated: isGif(extension),
      // failOn: "truncated", // 짤린(손상된) 이미지만 오류 반환
      failOn: "none", // 모든 변환 실패 오류 반환 하지 않음
    });

    const meta = await image.metadata();

    // Init format.
    format = initFormat(params.f, extension);

    console.log(`format : ${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`.

    // Image crop logic
    const crop = params.crop ? params.crop.split("x") : null;

    // crop
    if (crop && !isGif(extension)) {
      const top = parseInt(crop[0], 10);
      const cropHeight = parseInt(crop[1], 10);

      image
        .extract({
          left: 0,
          top: top,
          width: meta.width,
          height: cropHeight,
          quality: getQuality(format, params.q),
        })
        .resize({
          width: initWidth(meta.width, params.w),
          height: initHeight(meta.height, params.h),
        });
    } else {
      image.rotate().resize({
        width: initWidth(meta.width, params.w),
        height: initHeight(meta.height, params.h),
      });
    }

    if (isPng(params.f)) {
      image.jpeg({
        // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
        quality: getQuality(format, params.q),
        chromaSubsampling: "4:4:4",
        mozjpeg: true,
      });
    } else {
      image.toFormat(format, {
        // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
        quality: getQuality(format, params.q),
      });
    }

    resizedImage = await image.toBuffer();
  } catch (error) {
    console.log("Sharp: ", error);
    return callback(error);
  }

  const resizedImageByteLength = Buffer.byteLength(resizedImage, "base64");

  console.log("resized 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}`,
    },
  ];

  // cahce 만료시간 설정
  response.headers["cache-control"] = [
    { key: "Cache-Control", value: "public, max-age=5184000" },
  ];

  return callback(null, response);
};

 

배포 파일 생성을 위해 Docker 설정하기

1. Dockerfile 작성

FROM amazonlinux:2

WORKDIR /tmp

#install the dependencies
RUN yum -y update
RUN yum -y install gcc-c++
RUN yum -y install findutils
RUN yum -y install tar gzip
RUN yum -y install glibc

RUN touch ~/.bashrc && chmod +x ~/.bashrc

RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

RUN source ~/.bashrc && nvm install 14.15.1

WORKDIR /build

 

 

2. 도커 빌드 실행을 위한 스크립트 작성

# build.sh
#!/bin/bash

# docker를 빌드
docker build -t amazon-nodejs .

echo 'docker volume =====>'  ${PWD}/src:/build 

# 빌드된 이미지로 sharp를 제외한 라이브러리 설치 (querystring, request)
# 빌드된 결과를 /src에 동기화하기 위해 --volume 옵션 사용
docker run --rm --volume ${PWD}/src:/build amazon-nodejs /bin/bash -c \
"source ~/.bashrc; npm init -f -y; npm install querystring --save; npm install request --save;npm install --only=prod"


echo '================ install sharp ================'

# Corss-Platform 방법으로 Sharp를 설치 
cd src
rm -rf node_modules/sharp
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux sharp

echo '================ finish sharp ================ \n'

echo mkdir /dist in ...  $PWD

# Dist 폴더 생성
mkdir -p ../dist

# Deployment Package로 .zip 파일로 만들기
zip -FS -q -r ../dist/functions.zip *

 

 

3. 스크립트 실행. (Mac 기준)

$ sh build.sh

# ./dist/functions.zip 생성 확인

 

스크립트를 실행하면  아래 경로에 압축파일이 생성 됩니다.

 

IAM 정책 및 역할 생성

람다(Lambda)를 CloudFront 배포(Deploy)와 연결하기 위한 IAM 권한을 설정합니다.

 

1. AWS Console 에서 IAM 설정 페이지로 이동 합니다. 왼쪽 메뉴에서 [정책] 메뉴를 클릭 후 설정 페이지로 이동합니다.

2. 정책 생성 버튼을 클릭 합니다.

 

3. [정책 작성] 페이지에서 [JSON] 으로 선택 합니다.

 

4. [JSON 편집] 영역에 정책 권한을 입력합니다.

  • s3:PutObject 권한은 필요치 않습니다.
  • cloudfront:CreateDistribution 또는 cloudfront:UpdateDistribution 중 하나만 설정합니다.
  • CloudWatch에서 로그 데이터를 처리하기 위해 logs:xxx 권한들을 설정합니다.
{
    "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": "*"
        }
    ]
}

 

 

5. [다음] 버튼을 클릭 후 정책 명을 추가 한뒤 저장합니다. 저는 정책 명은 [LambdaResizePolicy] 으로 작성하겠습니다.

 

IAM / 역할 생성

Lambda@edge에 연결할 역할을 생성합니다.

 

1. AWS Console 에서 역할 페이지로 이동 후 [역할 만들기] 버튼을 클릭 합니다.

 

2. 이 역할을 사용할 서비스로 Lambda를 선택합니다.

3. 정책 [LambdaResizePolicy] 를 선택합니다.

 

4. 역할 이름과 (lambdaResize) 기타 정보를 입력 후 [역할 생성] 버튼을 누릅니다.

 

2. 역할의 신뢰 관계(정책) 수정

 

2. 나의 역할 목록에서 방금 생성한 lambdaResize 선택합니다.

 

3. [신뢰 관계] 탭 이동 후 편집 선택을 선택합니다.

 

4. 아래의 신뢰 관계 정보를 입력합니다.

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

 

5. [정책 수정] 버튼을 클릭 하고 저장합니다.

 

 

CloudFront 동작 설정 하기

 

클라우드 프론트에서 [동작] 설정을 변경해야 합니다. 기존 동작에서는 쿼리스트링을 적용해도 정상적으로 캐싱되지 않습니다.

 

1. 클라우드 프론트 설정에서 [동작] 탭으로 이동 합니다. ❶ 변경할 [동작] 항목을 체크하고 ❷ [수정] 버튼을 클릭 합니다.

 

2. 동작설정 페이지에서 [캐시 키 및 원본 요청] 설정 항목에 [❷ 캐시 정책]을 선택 합니다. 만약 생성된 정책이 없다면 [❶ 정책 생성 링크] 를 클릭 후 2-1 번 항목을 따라 수행하세요.

 

2-1. [캐시 정책] 생성 하기

 

❶ [상세] 항목에서 정책 이름을 입력합니다..

❷ [캐시 키 설정] 항목에서[ 지정된 쿼리 문자열 포함] 선택 합니다.

❸ 허용 할 문자열 정보를 추가 합니다. (q, f, w, h, crop)

❹ 변경 사항을 [저장] 후 [2 동작 편집]에서 캐시 정책을 설정하세요.

 

AWS Lambda 함수 배포하기

이제 함수 파일을 Lambda에 배포해보겠습니다. AWS Consol 로그인 후 US East (N. Virginia) 리젼으로 설정합니다.

콘솔에서 Lambda 설정 페이지로 이동합니다.

 

1. 람다 함수 생성하기

아래 이미지 순서대로 실행하여 함수를 생성합니다.

 

 

2. 람다 함수 파일 업로드 하기

 

2-1. 생성 이동 된 페이지에서 람다 파일을 업로드 하겠습니다. 아래 [Code] 탭에서 [Upload from] 버튼을 클릭 합니다.

 

 

2-2.  선택항목에서 [.zip file] 을 클릭합니다.

 

 

2-3. 파일 업로드 후 저장합니다.

 

 

2. 람다 함수 테스트 하기

람다 함수에 빌드된 funtions.zip 파일 업로드를 완료 했습니다. 이제 정상적으로 동작하는 지 테스트를 해보겠습니다. 람다 함수 설정 페이지에서 [Test] 탭으로 이동합니다.

 

2-1. 테스트 이벤트 추가하기.

 

[create new event] 체크 후 생성할 이벤트 명을 추가합니다. 그리고 아래 [Event JSON] 란에 테스트 json 을 추가하겠습니다.

 

{
  "Records": [
    {
      "cf": {
        "config": {
          "distributionId": "EXAMPLE"
        },
        "request": {
          "uri": "cute-7973191_1280.webp",
          "querystring": "",
          "method": "GET",
          "clientIp": "2001:cdba::3257:9652",
          "headers": {
            "host": [
              {
                "key": "Host",
                "value": "d123.cf.net"
              }
            ],
            "user-agent": [
              {
                "key": "User-Agent",
                "value": "Test Agent"
              }
            ],
            "user-name": [
              {
                "key": "User-Name",
                "value": "aws-cloudfront"
              }
            ]
          }
        },
        "response": {
          "headers": {
            "content-type": [
              {
                "key": "Content-Type",
                "value": " image/webp;"
              }
            ],
            "content-length": [
              {
                "key": "Content-Length",
                "value": "9593"
              }
            ]
          },
          "status": "200",
          "statusDescription": "OK"
        }
      }
    }
  ]
}

 

2-2. [테스트] 버튼을 클릭 하면 응답 결과가 나옵니다. 아래와 같이 성공 메시지가 나오면 잘 적용된 것입니다.

 

 

3. 람다 함수 배포하기

 

3-1. 이제 람다 함수를 배포하겠습니다, 오른쪽 상단에 [액션] 버튼을 클릭 후 [Lamba@Edge] 배포 버튼을 클릭 하세요.

 

3-2. 아래 항목을 선택합니다.

❶ lambda@edge를 배포할 Cloudfront 를 선택 합니다.

❷ 캐시 동작은 [ * ]  로 선택합니다.

❸ CloudFront 이벤트는 [Origin response] 로 선택합니다.

❹ 입력 사항을 잘 확인 하고 [체크 버튼] 을 클릭 합니다.

❺ [배포] 버튼을 클릭 합니다.

 

3-3. 배포 버튼을 클릭 하면 위에서 설정한 [대상 Cloudfront]에 배포가 됩니다. 배포가 완료되면 실제 이미지 주소로 들어가 정상적으로 리사이징 된 이미지가 캐싱되는 지 확인 하겠습니다.

 

 

최종 테스트 하기

이제 마지막으로 S3 에 업로드 된 이미지의 width 값 변경 및 이미지 포멧을 변경하는 테스트를 해보겠습니다. 배포된 cloudfront 주소로 png 이미지 파일을 접속 해봤습니다. 첫번째 이미지는 쿼리스트링 없이 이미지 주소로만 접속한 결과 입니다. 원본 이미지의 사이즈는 1280x851입니다. 두번째 이미지는 쿼리스트링에 w=100&f=webp 를 주고 다시 요청해본 결과 입니다. 넓이가 100으로 변경되었고 이미지 포맷 또한 webp로 변경되었습니다. 쿼리스트링을 포함한 주소로 첫번째 요청의 경우 x-Cache 정보가 Miss from cloudfront 로 나타나지만 두번째 요청 부터는 Hit from cloudfront 로 캐싱 처리된 이미지가 반환 되는 것을 확인하실 수 있습니다. 

 

원본 이미지 요청 결과
쿼리문자열을 포함한 요청 결과

 

 

이제 최종 마무리 테스트까지 완료 하였습니다. Lambda@edge 관련해서 설정해야하는 영역이 이곳 저곳 많다 보니 포스팅 내용이 많이 길어 졌습니다. 최대한 상세하게 작성하였으나 놓친 부분이나 잘못된 내용이 있을 수 있습니다. 잘못된 내용이 있으면 포스팅 아래에 댓글로 남겨주시면 포스팅 내용에 반영 하겠습니다. 여기까지 읽어 주셔서 감사합니다.

 


[참고내용]