[AWS] 온디맨드 이미지 리사이징을 위한 Lambda@Edge 트리거는? (feat. CloudFront 이벤트)

조성우·2025년 6월 2일
0

AWS

목록 보기
9/9
post-thumbnail

이전 글: [AWS] Lambda@Edge와 CloudFront를 활용한 이미지 리사이징 적용기

위 글에서 Lambda@Edge를 활용해 On-The-Fly(On-Demand) 방식의 이미지 리사이징을 구현했었다. 사실 이를 구현하기 전에 다양한 레퍼런스를 찾아보았는데, 대부분 Lambda 트리거를 Origin Response로 설정한다는 것을 알았다.

  • "왜 Origin에서 응답이 오는 시점인 Origin Response 이벤트를 트리거로 설정해야 하지?"
  • "캐시 미스가 확인된 시점에(Origin Request)! 원본 이미지를 가져와 리사이징하는 람다를 수행해야하는 거 아닌가?"

위 궁금증으로 자료를 좀 더 탐색해보았다.


CloudFront 이벤트 트리거

참고: [AWS 공식 블로그] AWS CDN Blog - Resizing Images with Amazon CloudFront & Lambda@Edge

Lambda@Edge는 CloudFront의 위 4가지 이벤트를 트리거로 설정할 수 있다. 각각의 이벤트가 트리거로 설정되었을 때, 람다는 아래와 같이 수행된다고 정리할 수 있다.

  1. Viewer Request – CloudFront가 뷰어로부터 요청을 받을 때 실행됨 (요청된 객체가 엣지 캐시에 있는지 확인하기 전)
  2. Origin Request – CloudFront가 요청을 오리진으로 전달할 때만 실행됨 (요청된 객체가 엣지 캐시에 없는 경우만)
  3. Origin Response - CloudFront가 오리진으로부터 응답을 받은 후 실행됨 (응답 객체를 캐시하기 전)
  4. Viewer Response – 요청된 객체를 뷰어에게 반환하기 전에 실행됨



1. Origin Response? 오리진에 두 번 접근하는 이유는?

우선 흔히 찾아볼 수 있는 람다 함수의 리사이징 코드를 보면, 하나 같이 모두 S3 GetObject를 수행한다. 그런데도 다들 Origin Response 트리거로 람다를 실행하니까 이상하게 느껴졌다.

"Origin Response 트리거라면 이미 오리진으로부터 객체를 응답 받은 시점인데, 왜 GetObject를 수행하는 거지?"


나는 AWS 콘솔에서 Lambda 코드를 수정하고 디버깅을 해보고, 당연히 존재할 것이라고 생각했던 response.body가 비어있는 걸을 확인했다.

그래서 공식 문서를 찾아본 결과 아래의 내용을 확인했다.

Fields in the response object
...
headers, status, statusDescription

body는 Lambda@Edge에 전달되지 않았고, 생각해보니 다음과 같은 이유가 있을 수 있겠다고 생각했다.

  1. Lambda@Edge는 엣지 로케이션에서 빠르게 실행되는 함수이기 때문에 굳이 전달하지 않는다.
    • 응답 body 크기가 클 수 있으며 전송 비용이 크다.
  2. 오리진의 응답이 Lambda@Edge에 전달되면 보안상 좋지 않을 수 있다.
    • 필요하다면 Lambda@Edge에서 별도로 가져오는게 맞을 것 같다. (그래서 Role 생성 및 신뢰관계를 추가했지!)

결과적으로, 람다 함수의 리사이징 코드에서 다시 GetObject를 수행하여 리사이징하여 reponse.body로 넘겨주면 우리는 원하는 로직이 원활하게 수행된다.



2. Origin Request도 가능한가?

다음 궁금증은 이거였다.

"굳이 두 번 갔다오지 말고, Origin Request 이벤트에서 GetObject 및 리사이징을 수행하여 바로 응답할 수는 없을까?"

여기에 대한 답도 공식 문서에 있었다.

When CloudFront receives a request, you can use a Lambda function to generate an HTTP response that CloudFront returns directly to the viewer without forwarding the response to the origin. Generating HTTP responses reduces the load on the origin, and typically also reduces latency for the viewer.

원본 서버에 요청을 보내지 않고도 Lambda@Edge에서 직접 HTTP 응답을 생성하여 CloudFront에 반환할 수 있다!


그래서 람다 함수의 리사이징 코드를 GetObject를 수행하도록 다음과 같이 수정하고, 트리거를 Origin Request 이벤트로 바꿔보았다.

const sharp = require('sharp');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({ region: 'ap-northeast-2' });

const DEFAULT_QUALITY = 80;
const DEFAULT_TYPE = 'contain';

exports.handler = async (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const querystring = request.querystring;
    const searchParams = new URLSearchParams(querystring);

    // 리사이징 조건 확인
    const width = parseInt(searchParams.get('w'), 10) || null;
    const height = parseInt(searchParams.get('h'), 10) || null;

    if (!width && !height) {
        // 원본 요청을 그대로 오리진에 전달
        return callback(null, request);
    }

    const quality = parseInt(searchParams.get('q'), 10) || DEFAULT_QUALITY;
    const type = searchParams.get('t') || DEFAULT_TYPE;
    const f = searchParams.get('f');

    const uri = decodeURIComponent(request.uri);
    const fileNameMatch = uri.match(/\/?(.+)\.(\w+)$/);
    if (!fileNameMatch) {
        return callback(new Error('Invalid image path'));
    }

    const [, imageName, extension] = fileNameMatch;
    const format = (f === 'jpg' ? 'jpeg' : f) || extension;

    const s3BucketName = 'bucket-name';

    try {
        const s3Object = await getS3Object(s3, s3BucketName, imageName, extension);
        const resizedImage = await resizeImage(s3Object, width, height, format, type, quality);

        const response = {
            status: '200',
            statusDescription: 'OK',
            headers: {
                'content-type': [{ key: 'Content-Type', value: `image/${format}` }],
                'cache-control': [{ key: 'Cache-Control', value: 'max-age=31536000' }]
            },
            bodyEncoding: 'base64',
            body: resizedImage.toString('base64')
        };

        return callback(null, response);
    } catch (error) {
        console.error('Image processing error:', error);
        const response = {
            status: '500',
            statusDescription: 'Internal Server Error',
            headers: {
                'content-type': [{ key: 'Content-Type', value: 'text/plain' }]
            },
            body: 'Image processing failed',
            bodyEncoding: 'text'
        };
        return callback(null, response);
    }
};

async function getS3Object(s3, bucket, imageName, extension) {
    const command = new GetObjectCommand({
        Bucket: bucket,
        Key: `${imageName}.${extension}`
    });

    const s3Object = await s3.send(command);
    const bodyBytes = await s3Object.Body.transformToByteArray();
    return { Body: bodyBytes };
}

async function resizeImage(s3Object, width, height, format, type, quality) {
    return sharp(s3Object.Body)
        .resize(width, height, { fit: type })
        .toFormat(format, { quality })
        .toBuffer();
}

특별히 다른 점은 아래와 같다.

  • return callback(null, request); - w와 h 파라미터가 없다면 요청을 그대로 넘겨주기
  • const response = { ... } - 응답 객체를 아예 직접 생성해버리기
  • 오리진 객체가 존재하지 않을 때 에러 처리

Lambda 테스트도 성공하고,

리사이징이 잘 되는 것을 확인하였다.


그리고 성능(응답 시간)이 개선되었다!!!

이전의 람다 함수(Origin Reponse 이벤트 트리거)는 캐시 미스일 때 700~800ms이 소요되었으나,

이번에는 평균적으로 600~700ms의 시간이 걸렸다.

오리진에 한 번만 다녀오니까 개선된 것이겠지?!

0개의 댓글