온디맨드 이미지 리사이징

정수현·2021년 11월 3일
6
post-custom-banner
  1. 개요
    온디맨드 이미지 리사이징 동작 방식은 다음과 같습니다.
  1. cloudFront에 요청한 object가 cache되어 있는지 여부를 판단합니다
  2. 만약 존재하지 않는다면 s3 origin server로 request를 보내 사진의 원본이 존재하는지 찾습니다.
  3. origin s3 server에서는 요청한 object에 대한 response를 전달합니다.
  4. cloudFront는 응답을 end-user에게 전달하고 cloudFront에 caching합니다.

이 때 3번 과정에서 s3는 origin object가 존재한다면 200, 존재하지 않는다면 404 response를 cloudFront로 반환합니다.

가장 기본적인 개요는 cloudFront가 cache miss가 발생하여 s3 origin server로 요청하고 응답을 받았을 경우 (3번과정) Trigger를 걸어놓은 aws lambda를 수행하여 response Body를 조작합니다.

200 response라면 즉 실제로 있는 object가 응답으로 왔다면 이미지를 주어진 queryParameter로 resizing을 수행하고 caching 및 responseBody를 Resizing한 이미지로 치환합니다.

주의할점은 s3 origin file의 response를 3번과정에서 수정하는 경우 1MB크기를 넘겨서는 안됩니다.

만약 404 response라면 그냥 원본반환(에러메시지)를 그대로 end-user에게 반환하도록 합니다.

  1. cloudFront 쿼리 파라미터 설정

    기본적으로 cloudFront를 설정하는 것은 간단합니다. s3 bucket을 설정하고 대부분은 Default option으로 사용하기 때문에 적용건은 생략합니다.

    단 default cloudFront 설정에서는 queryParam을 허용하지 않습니다.

    따라서 아래 cloudFront에서 동작 → 편집을 클릭한 뒤

등장하는 캐시 키 및 원본 요청란을 수정합니다.

기본적으로 Default는 Cache policy and origin request policy (recommended) 로 설정되어있을 것입니다.

해당 내용을 Legucy cache settings로 바꾼다음 필요한 쿼리 문자열을 아래와 같이 추가합니다.

  1. 권한 설정

    뒤이어 구현할 lambda에서 필요한 권한을 가진 역할을 생성합니다.

    필요한 것은 lambda와 s3에 대한 접근 권한 입니다. 아래와 같이 역할을 생성하고 lambda, s3 full access 권한정책을 부여해줍니다.

    신뢰 관계 편집을 들어가 json포맷으로 확인한 결과가 아래와 같으면 됩니다.

  2. lambda 생성 spec

    trigger로 사용하기 위한 lambda를 만듭니다.

    중요한 점은 cloudFront에 trigger를 붙이는 것을 edge라고 표현합니다. 이 edge는 버지니아 북부 region에 존재하는 lambda만 허용합니다. 따라서 버지니아 북부로 region을 변경하여 lambda를 생성합니다.

    참고로 버지니아 북부에 있어도 lambda는 cloudFront에 붙는것이기 때문에 cloudFront가 존재하는 모든 AWS CDN sever로 배포되므로 성능에 아무런 문제없습니다.

람다를 생성할 때 역할은 기존역할 선택을 클릭 한 뒤 3번에서 만든 역할을 넣어주고, node의 버전은 14.x로 진행합니다. 아키텍쳐는 x86입니다.

  1. lambda 설정 spec

    람다를 만든 뒤 구성→ 편집을 선택합니다.

    1. 제한 시간은 default 기준 3초입니다. 이미지 리사이징은 처음에 시간이 걸릴수 있으므로 넉넉하게 10~30초로 timeout을 잡아줍니다. 3초로 하는 경우 간혹가다가 timeout exception이 발생하여 Error page가 return 되는 경우가 일어납니다. 최소 10초를 권장합니다.
    2. 메모리의 경우도 디폴트는 128MB입니다. 용량이 정말 큰 이미지를 불러와 리사이징을 하는 경우 OOM(out of memory) exception이 발생해 error가 나타나는 경우가 존재했습니다. 이를 방지하기위해 메모리의 최대 상한도 1024MB로 설정하는 것을 권장합니다.
  2. lambda 구현

    lambda edge를 구현합니다. 이 때 이미지 리사이징에 필요한 라이브러리인 sharp가 필요합니다. 이러한 라이브러리를 주입해주는 방식은 edge에서 존재하지 않기때문에 실제로 다운로드를 받아 project direct에 수동으로 포함하여야합니다.

    추가로 m1 Mac에서 진행하는 경우 npm을 통해 설치했을 시 arm64 방식의 라이브러리가 들어가므로 x86용 라이브러리가 필요합니다. 인텔 cpu를 사용하는 환경에서 npm을 통해 라이브러리를 설치해서 옮겨주세요.


그 후 lambda에서 해당 zip file을 upload합니다.


7. source code

```jsx
const querystring = require('querystring'); // Node.js를 실행할 Lambda 머신이 가지고 있기에 별도의 설치를 하지 않습니다.
const AWS = require('aws-sdk'); // Node.js를 실행할 Lambda 머신이 가지고 있기에 별도의 설치를 하지 않습니다.
const S3 = new AWS.S3({region: "ap-northeast-2"});
const sharp = require('sharp');
const BUCKET = 'your bucket'; // your bucket
const supportImageTypes = ['jpg', 'jpeg', 'png', 'svg', 'tiff'];

exports.handler = async (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const request = event.Records[0].cf.request;
    console.log(request.uri);
    console.log(response.status);
    // check if image is present and not cached.
    if (response.status == 200) {
        const params = querystring.parse(request.querystring);
        const uri = request.uri;
        const [, imageName, extension] = uri.match(/\/(.*)\.(.*)/);

        const requiredFormat = extension == "jpg" ? "jpeg" : extension;

        // 이미지 포맷이 아닌 경우 무시
        if (!supportImageTypes.includes(requiredFormat)){
            callback(null, response);
            return;
        }

        response.headers["content-type"] = [{key: "Content-type", value: "image/" + requiredFormat}];
        if (!response.headers['cache-control']) {
            response.headers['cache-control'] = [{key: 'Cache-Control', value: 'public, max-age=86400'}];
        }
        if (!params.w && !params.h) {
            callback(null, response);
            console.log("no have param image just returned!")
            return;
        }
        try {
            const originalKey = decodeURIComponent(imageName) + "." + extension;
            console.log(originalKey) // 디코딩한 파일 이름 및 확장자
            const s3Object = await S3.getObject({Bucket: BUCKET, Key: originalKey}).promise();
            let resizedImage;
            if (params.w && params.h) { // 둘다 있으면
                const width = parseInt(params.w);
                const height = parseInt(params.h);
                resizedImage = await sharp(s3Object.Body).resize(width, height, {fit: "fill"}).withMetadata().rotate().toFormat(requiredFormat).toBuffer();
            } else if (params.w) { // 하나만 있으면
                const width = parseInt(params.w);
                resizedImage = await sharp(s3Object.Body).resize({width: width}).withMetadata().rotate().toFormat(requiredFormat).toBuffer();
            } else if (params.h) { // 하나만 있으면
                const height = parseInt(params.h);
                resizedImage = await sharp(s3Object.Body).resize({height: height}).withMetadata().rotate().toFormat(requiredFormat).toBuffer();
            } else {
                return callback(null, response);
            }
            byteLength = Buffer.byteLength(resizedImage, 'base64');
            console.log("image size : " + byteLength);
            // 만약 resizing을 했음에도 불구하고 1MB를 넘는다면 바디를 조작할 수 없다 ( AWS edge 한계 )
            // 따라서 이 경우는 화질을 70프로까지 떨어뜨리는 시도를 한다
            if (byteLength >= 1046528) {
                resizedImage.toFormat(requiredFormat, {quality: 70});
                resizedImage = await resizedImage.toBuffer();
                byteLength = Buffer.byteLength(resizedImage, 'base64');
                console.log("70 quality " + byteLength);
                if (byteLength >= 1046528)
                    return callback(null, response); // 리사이징 + 70프로에서도 1MB를 넘는다면 그냥 원본을 반환해준다
            }
            response.status = 200;
            response.body = resizedImage.toString('base64');
            response.bodyEncoding = "base64";
            return callback(null, response);
        } catch
            (error) {
            console.log(error);
            return callback(error);
        }
    } else {// allow the response to pass through
        console.log("status " + response.status);
        callback(null, response);
    }
}
;
```
  1. reference

https://medium.com/daangn/lambda-edge로-구현하는-on-the-fly-이미지-리사이징-f4e5052d49f3

  1. addtional tip

cloudWatch log는 lambda가 실제로 실행된 Region에서 가장 가까운 곳에 찍힙니다. 버지니아 북부가 아닌 서울로 region을 설정하여 조회해주세요

profile
백앤드 개발자
post-custom-banner

0개의 댓글