이미지 최적화 시즌2 - Lambda@Edge

허준기·2024년 10월 24일
4

BCSD

목록 보기
8/8
post-thumbnail

다시 돌아온 이미지 리사이징


이미지 최적화 블로깅

위의 블로깅한걸 보면 알 수 있는데 이전에는 서버에서 이미지 리사이징을 진행했었다.

우리 동아리에서의 이미지를 가장 많이 사용하는 곳은 식단쪽인데 식단사진이 아침, 점심, 저녁 이렇게 하루에 3번 정도 업로드 될 수 있다고 생각했다.

그래서 서버에서 크론 작업으로 매일 식단이 올라올 것이라고 생각되는 08:00, 11:30, 17:00 이렇게 하루 3번씩 파이썬으로 최적화 한 후 S3에 저장하는 식으로 하려고 했는데, 이 작업이 생각보다 커서 문제가 생겼다.

이미 리사이징 된 이미지를 다시 하지는 않았지만 S3 안에 있는 이미지들을 대상으로 하다보니 크론 작업을 통해 한 번 코드가 돌아가면 최소 5분은 걸렸고 리사이징이 안되어 있을 때 코드를 돌리면 30분 넘게 소요됐다.

당시 S3 버킷에 1.9GB정도의 용량과 6458개 정도의 이미지들을 가지고 있었는데 30분이 소요된거면 너무 효율이 떨어진다는 생각이 들었다. 매일 3번의 최적화 작업을 하며 사용되는 서버의 자원들이 아까웠고, 사용하지 않을 이미지에 대해서도 최적화를 하다보니 낭비라는 생각이 들었다.

그리고 고정으로 이미지를 리사이징 해놓다보니 원하는 사이즈의 이미지를 사용할 수 없다는 단점이 있었다.

그래서 고민을 하던 중에 다른 방법에 대한 추천을 받았다!

람다엣지를 이용한 이미지 리사이징 공식 문서

이 방식은 미리 리사이징을 해놓는게 아니라 요청이 들어오면 수요에 맞게 리사이징을 하는 방식이다!!

그래서 추가적인 용량이 많이 들지 않고, 시간도 오래 안걸리다보니 위에서 했던 고민들을 해결할 수 있는 방법이라는 생각이 들어서 진행을 해보게 되었다.

Serverless와의 싸움


처음으로 시도한 방법은 Serverless 을 사용한 방법이었다.

참고 블로그

위의 과정은 뒤에 쓰고 일단은 막힌 부분을 먼저 써보겠습니다.

분명 다른 블로그에는 AWS / Node.js / Starter 를 선택하라고 되어 있었는데 내가 Serverless를 사용하려고 보니까 Starter는 없었다..

그래서 일단 HTTP API 를 깔아봤는데 Starter 와 구조가 달라서 되지 않았다.
그래서 이 방법은 포기하고 다른 방법을 다시 찾아봤다...

Cloud9이 없어졌어요


두 번째로 시도한 방법은 Cloud9을 이용한 방법이다

이 방법도 기초 작업이 필요한데 그 부분은 뒤에 나와있어요
참고 블로그

기초 작업을 하고 이제 Cloud9으로 Lambda 함수를 작성하고 업로드 하는 작업이 남아있었다.

AWS에서 Cloud9을 찾아보고 있었는데 남아있기만 하고 생성이 되지는 않았다..

이런 화면만 떠서 내 계정문제인줄 알았는데 알고보니 그냥 Cloud9이 기존에 사용하지 않은 사용자들에 대해서는 서비스를 더 이상 지원하지 않아서 그런거였다..

신규 사용 불가능!!!!!!!!!!!!!!!!!!!!!

그래서 결국 이 방법도 포기하고 마지막 방법으로 진행을 하게 됐다.

여기서부터는 설명을 해보겠습니다

마지막 방법


이미 S3 버킷이 생성되어 있고, CloudFront가 적용되어 있다는 가정을 하고 설명하겠습니다.

동아리 AWS에 적용해 보기 전에 개인 AWS로 미리 적용해보려고 연습을 했었는데, 동아리 계정은 CloudFront가 적용되어 있었지만, 개인 계정은 CloudFront가 적용되어 있지 않아 약간 달랐다.

처음에는 상관 없는줄 알고 그대로 진행했지만 하다보니, CloudFront가 적용되어 있으면 그냥 lambda가 아니라 lambda@Edge를 사용해야 하다보니 내 개인 계정에도 CloudFront를 적용했다!

대략적인 순서는 이렇다

1. IAM 생성
2. CloudFront 권한/신뢰관계 확인
3. LambdaEdge 생성
4. 2048, 30초 설정
5. 트리거 : Origin-Response 설정

IAM 생성

사실 IAM 까지는 생성하지 않아도 된다

나는 Policies 생성하고 Role에 연결시켜 줬다.


우선 imageResize라는 Policies를 만들어줬다


그리고 Permissions 쪽에서 CloudFront, IAM, Lambda, S3 쪽을 허용해줬다.
우리 동아리는 CloudWatch Logs를 사용하지 않는데 나중에 혹시 사용할까봐 일단 허용은 해줬다
사용하지 않으면 굳이 안해줘도 될 것 같다

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole",
                "lambda:GetFunction",
                "lambda:EnableReplication",
                "cloudfront:UpdateDistribution",
                "s3:GetObject",
                "s3:PutObject",
                "s3:ListBucket",
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ],
            "Resource": "*"
        }
    ]
}

JSON으로 편집하고 싶은 사람은 이대로 수정하면 될 것 같다

그리고 이번엔 imageResize라는 Role을 만들고 연결을 해줬다

그리고 lambdalambda@edge도 연결을 해주면 된다

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

CloudFront 권한/신뢰 관계 확인

이쪽 설정을 잘못 건드려서 오래 걸렸던 기억이 있다....!
아마 개인 CloudFront 초기 설정을 할 때 잘못한 것 같다


이쪽으로 들어가서 아래 사진의 설정을 확인해줘야한다.
이 부분에서 Legacy access identities로 되어 있어서 막혔다..

그리고 이번엔 Edit Behavior 쪽을 확인해줘야한다.

위의 사진처럼 설정을 해준 후에 아래 사진처럼 반드시 해줘야 한다!!


쿼리문을 사용하여 이미지의 사이즈를 설정해주는 방식이기 때문에 반드시 해줘야한다!

f = format
h = height
q = quality
w = width

Lambda@Edge 생성

Lambda@Edge를 만들기 위해서는 반드시 US East(N.Virginia) 지역에 Lambda 함수를 생성해야한다!!
그래야 CloudFront와 연결해서 사용할 수 있다!

ImageResizeProduction이라는 Lambda@Edge 함수를 만들어줬다

2048, 30초 설정

그리고 이쪽에서도 시간을 많이 썼는데 만들어 준 Lambda 함수에서 Configuration - General configuration 쪽에서 이 설정을 해주지 않으면 이미지에 접근하려고 할 때 에러가 나게 된다

그래서 위의 사진처럼 Memory에 2048, Timeout에 30s를 주면 에러가 나지 않는다!

트리거 : Origin-Response 설정

그리고 Lambda 함수를 배포할 때 트리거를 설정해줘야 하는데 이걸 CloudFront로 해줘야한다.

그래야 CloudFront를 통해서 이미지 요청이 왔을때, 람다 함수가 동작해서 이미지 리사이징을 진행해줄 수 있다

위처럼 Origin response에 이벤트 트리거를 걸어주면 된다!

코드

제일 중요한 코드 차례다!
Node.js를 사용하여 이미지 리사이징을 진행한다

'use strict';

const querystring = require('querystring'); // 설치하지 않아도 됨
const AWS = require('aws-sdk'); // 설치 해야함
const Sharp = require('sharp'); // 설치 해야함
const isImage = require('./is-image');

const S3 = new AWS.S3({
    region: '지역'
});
const BUCKET = '버킷 이름';

const MB = 1024 * 1024;

exports.handler = async (event, context, callback) => {
    const { request, response } = event.Records[0].cf;
    console.log(response.headers);
    console.log(response.body);

    // 파라미터들은 w, h, f, q 이고 width, height, format, quality를 나타낸다.
    const params = querystring.parse(request.querystring);

    // 이미지 이름과 확장자를 추출.
    const { uri } = request;
    const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);

    // uri가 이미지가 아닐경우 원본 요청 반환
    if (!isImage(uri)) {
        return callback(null, response);
    }

    // 사이즈 초기화
    let width = parseInt(params.w, 10) || null;
    let height = parseInt(params.h, 10) || null;

    // 이미지 퀄리티 초기화
    // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
    let quality = parseInt(params.q, 10) || null;

    // 확장자 초기화.
    // 기본값: webp
    let format = params.f ? params.f : "webp"; 

    // 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`.

    // s3에서 이미지 객체 가져오기
    let s3Object;
    try {
        s3Object = await S3.getObject({
            Bucket: BUCKET,
            Key: decodeURI(imageName + '.' + extension)
        }).promise();
    } catch (error) {
        // 이미지가 없는 경우 원본 요청 반환
        if (error.code === 'NoSuchKey') {
            console.log('Image not found in S3. Returning original response.');
            return callback(null, response);
        }
        console.log('S3.getObject: ', error);
        return callback(error);
    }

    // 이미지 리사이즈 시작
    let resizedImage;
    let metadata;
    try {
        resizedImage = await Sharp(s3Object.Body).rotate();
        metadata = await resizedImage.metadata();
    } catch (error) {
        console.log('Sharp: ', error);
        return callback(error);
    }

    try {
        // width, height 둘 중 하나가 있고, 원본 사이즈보다 작은 경우에 리사이즈
        if ((width || height) && ((width <= metadata.width) && (height <= metadata.height))) {
            resizedImage.resize(width, height);
        }

        // 포맷 & 퀄리티 변경
        resizedImage = await resizedImage
            .toFormat(format, { quality })
            .toBuffer();
    } catch(error) {
        console.log('Sharp: ', error);
        return callback(error);
    }
    
    const resizedImageByteLength = Buffer.byteLength(resizedImage, 'base64');
    console.log('byteLength: ', resizedImageByteLength);

    // TODO 이미지가 1 MB보다 크면 리사이즈하여 반환해야 한다.
    // 이미지가 1 MB보다 크면 원본을 반환한다.
    if (resizedImageByteLength > 1 * MB) {
        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);
};

코드는 대충 이렇다

클라이언트의 요청으로 아무 쿼리도 안왔을때 자동으로 .webp 확장자로 변경 후 반환하는 로직도 있다

주석을 달아놔서 알아볼만 하다

이 코드를 잘 압축해서

.zip File에 업로드 하면 된다!

그리고 위에 써놓은것처럼 트리거 이용해서 배포하면 성공!

사용방법


사용방법

w,h,f,q 라는 쿼리를 통해 이미지를 리사이징 할 수 있음
아무 쿼리를 안붙일 경우 → webp로 반환

w를 통해 가로값을 지정해 줄 수 있음

h를 통해 높이를 지정해 줄 수 있음

f를 통해 이미지의 포맷을 변경해 줄 수 있음

q를 통해 이미지의 퀄리티를 변경해 줄 수 있음(1~100%)

w 나h 를 하나만 사용할 경우 값에 따라 자동으로 나머지 안쓴 길이의 비율이 맞춰집니다
여러개의 쿼리를 사용하려면 ?w=100&q=50 이런식으로 사용하시면 됩니다

후기

굉장히 오래 붙잡았던 작업이었다..
시도한 방법들이 계속 안돼서 개인적으로 좀 늘어졌던 것 같다

그래서 결국 이런 피드백도 받았었다..ㅠㅠ

다음에 이런 큰 작업을 맡으면 좀 더 책임감을 가지고 임해야 할 것 같다.

그리고 클라이언트의 요청으로 .webp를 반환하도록 하긴 했지만, 클라이언트들에게 .webp로 반환했을 때의 사이드 이펙트에 대해서 물어보지 못하고 바로 배포를 해버린 것이 아쉬웠다.

다음부터는 클라이언트 개발자들과 얘기를 해 본 후에 배포를 진행하는 방식으로 해야겠다!

성빈이가 안도와줬으면 아직까지 하고 있었을수도... 흑흑
무한 감사!

그래도 유의미한 결과가 눈에 보여서 좋다~! 확실히 예전과는 다르게 이미지가 바로 불러진다

profile
나는 허준기
post-custom-banner

0개의 댓글