이전 글: [AWS] Lambda@Edge와 CloudFront를 활용한 이미지 리사이징 적용기
위 글에서 Lambda@Edge를 활용해 On-The-Fly(On-Demand) 방식의 이미지 리사이징을 구현했었다. 사실 이를 구현하기 전에 다양한 레퍼런스를 찾아보았는데, 대부분 Lambda 트리거를 Origin Response로 설정한다는 것을 알았다.
- "왜 Origin에서 응답이 오는 시점인 Origin Response 이벤트를 트리거로 설정해야 하지?"
- "캐시 미스가 확인된 시점에(Origin Request)! 원본 이미지를 가져와 리사이징하는 람다를 수행해야하는 거 아닌가?"
위 궁금증으로 자료를 좀 더 탐색해보았다.
참고: [AWS 공식 블로그] AWS CDN Blog - Resizing Images with Amazon CloudFront & Lambda@Edge
Lambda@Edge는 CloudFront의 위 4가지 이벤트를 트리거로 설정할 수 있다. 각각의 이벤트가 트리거로 설정되었을 때, 람다는 아래와 같이 수행된다고 정리할 수 있다.
우선 흔히 찾아볼 수 있는 람다 함수의 리사이징 코드를 보면, 하나 같이 모두 S3 GetObject를 수행한다. 그런데도 다들 Origin Response 트리거로 람다를 실행하니까 이상하게 느껴졌다.
"Origin Response 트리거라면 이미 오리진으로부터 객체를 응답 받은 시점인데, 왜 GetObject를 수행하는 거지?"
나는 AWS 콘솔에서 Lambda 코드를 수정하고 디버깅을 해보고, 당연히 존재할 것이라고 생각했던 response.body
가 비어있는 걸을 확인했다.
그래서 공식 문서를 찾아본 결과 아래의 내용을 확인했다.
Fields in the response object
...
headers, status, statusDescription
body는 Lambda@Edge에 전달되지 않았고, 생각해보니 다음과 같은 이유가 있을 수 있겠다고 생각했다.
결과적으로, 람다 함수의 리사이징 코드에서 다시 GetObject를 수행하여 리사이징하여 reponse.body로 넘겨주면 우리는 원하는 로직이 원활하게 수행된다.
다음 궁금증은 이거였다.
"굳이 두 번 갔다오지 말고, 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의 시간이 걸렸다.
오리진에 한 번만 다녀오니까 개선된 것이겠지?!