앞서 Lambda 함수 사용하여 S3 저장소 업로드 이미지 리사이징 방법을 살펴보았습니다.
참고. https://dev-ljw1126.tistory.com/460
Lambda를 활용하여 S3 이미지 리사이징 하는 방식은 아래와 같은 장단점이 있었습니다.
장점.
① 자동화된 리사이징
이미지를 S3 업로할 때 자동으로 람다 트리거 되므로, 이를 통해 개발 및 운영 부담을 줄일 수 있습니다.
② 비용 효율성 증가
서버리스 람다 함수는 필요할 때만 실행하여, 사용한 리소스에 대해서만 비용이 부가됩니다.
③ 단순한 구조
S3, Lambda 로 이루어진 구조는 상대적으로 간단하여 설정 및 관리가 비교적 쉽습니다. 별도의 API나 복잡한 배포 과정 없이도 이미지 처리 작업을 자동화 할 수 있습니다.
④ 그외 확장성과 원본 이미지 유지
단점.
① 리소스 낭비 가능성
모든 이미지 업로드에 대해 리사이징이 필요하지 않은 경우에도 Lambda 트리거 될 수 있습니다. 이는 불필요한 호출로 이어져 비용이 증가할 수 있습니다. (ex. 작은 이미지나 최적화된 이미지, 그리고 지원하지 않는 이미지 형식 등등)
그리고 원본 이미지와 리사이징된 이미지를 같이 S3 저장하기 때문에 용량이 증가하게 됩니다.
② 실시간 요구에 부적합
파일 업로드시 트리거 되어 리사이징이 수행되기 때문에, 실시간 사용자 요청 처리하기에는 한계가 있습니다.
③ Lambda의 제약
실행 시간과 메모리 제약이 있기 때문에 대용량 이미지나, 복잡한 작업을 다루기에는 적합하지 않습니다.
④ 그외 이미지 형식 추가나 변경시 유연성 부족, 그리고 캐싱 부재 (ex.S3 업로드 트리거 시 같은 사이즈 이미지를 매번 처리 가능)
S3에서 Lambda 함수 트리거를 사용한 이미지 리사이징은 자동화되고 비용 효율적이며 확장성이 뛰어난 솔루션입니다. 그러나 실시간 이미지 리사이징 요구에는 적합하지 않으며, Lambda의 리소스 제약과 불필요한 함수 호출 문제가 있을 수 있습니다.
이번 포스팅에서는 Lambda@Edge를 사용하여 실시간 이미지 리사이징을 제공하는 방법에 대해 살펴보도록 하겠습니다.
필요한 전체 코드와 과정은 AWS 공식 가이드와 기업 기술 블로그 참고하여 진행했습니다.
CloudFront에서 Lambda@Edge 설정할 수 있는 이벤트는 4가지가 있습니다. 그 중에서 "원본 응답"을 사용합니다.
① 뷰어 요청 (Viewer Request)
CloudFront가 뷰어로부터 요청을 받을 때, 그리고 요청된 오브젝트가 에지 캐시에 있는지 확인하기 전에 실행됩니다.
② 뷰어 응답 (Viewer Response)
요청된 객체를 뷰어에 반환하기 전에 실행됩니다. 이 함수는 객체가 이미 엣지 캐시에 있는지 여부와 관계없이 실행됩니다
③ 원본 요청 (Origin Request)
CloudFront가 요청을 오리진에 전달할 때만 실행됩니다. 요청된 오브젝트가 엣지 캐시에 있으면 함수가 실행되지 않습니다.
④ 원본 응답 (Origin Response)
CloudFront가 오리진으로부터 응답을 받은 후 응답에 있는 개체를 캐시하기 전에 실행됩니다.
아래 주소 형식으로 요청을 하게 되면 람다 엣지를 통해 리사이징된 이미지를 실시간으로 응답받게 됩니다.
https://[clounfrontId].cloudfront.net/{이미지 파일 경로}?w=500&h=500
- {이미지 파일 경로} : /origin/sample.jpg
- w : width, h : height
클라이언트에서 (GET)요청을 보내면 동작 흐름은 아래와 같습니다.
- 이미지 요청을 했지만 cache 되어 있지 않은 상태면 CloudFront가 s3(origin)에 요청을 보냅니다.
- s3에 이미지가 있다면 응답합니다. (*Origin Response 이벤트)
- Lambda 함수를 실행하여 이미지 리사이징 결과를 콜백 함수로 반환합니다.
- CloudFront는 이미지를 cache 처리 후 클라이언트에게 응답합니다.
S3 버킷 생성, CloudFront 생성
앞서 생성해둔 S3와 CloudFront를 재활용하도록 합니다.
참고. https://dev-ljw1126.tistory.com/460
IAM 정책과 역할 생성
- 정책은 "이 작업을 할 수 있다"라는 권한 규칙을 정의
- 역할은 그 정책을 실제로 적용할 수 있는 주체(서비스나 사용자)를 정의
IAM 정책 (Policy)
- AWS 리소스에 대해 어떤 작업이 허용되거나 거부되는지를 정의하는 권한 명세입니다
- 예를 들어 아래 작성한 정책은 다음과 같은 작업을 허용합니다
① lambda:GetFunction : Lambda 함수 가져오기
② lambda:EnableReplication* : 복제 활성화
③ cloudfront:UpdateDistribution : CloudFront 배포 업데이트
④ s3:GetObject : S3 객체 가져오기
IAM 메뉴에서 정책을 생성합니다. 명칭은 LambdaEdgeS3AccessPolicy로 지정하고 편집을 통해 JSON 형식으로 정의합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"lambda:GetFunction",
"lambda:EnableReplication*",
"cloudfront:UpdateDistribution",
"s3:GetObject"
],
"Resource": "*"
}
]
}
IAM 역할 (Role)
- 역할은 AWS 서비스나 사용자에게 임시적으로 부여되는 권한 집합입니다.
- 역할을 사용하면 특정 서비스나 어플리케이션이 특정 작업을 수행할 수 있도록 필요한 권한을 부여할 수 있습니다.
- 역할은 정책과 연결되어 있으며, 어떤 작업이 허용되는지 정의하는 정책을 통해 구체화됩니다.
IAM 메뉴에서 역할을 생성합니다.
앞서 생성한 정책 LambdaEdgeS3AccessPolicy를 검색하여 선택 후 다음으로 넘어갑니다
역할의 명칭을LambdaEdgeExecutionRole로 지정하였습니다.
신뢰 정책 편집하여 아래와 같이 설정합니다.
- Principal : 이 역할을 가질 수 있는 서비스를 정의합니다.
- Action : 이 역할을 통해 허용되는 작업을 정의합니다. ("sts:AssumeRole"은 edgelambda.amazonaws.com이 역할을 "가정"하게 설정함으로써 Lambda@Edge가 해당 역할의 권한을 사용할 수 있도록 허용합니다.)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com",
"edgelambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
Lambda 함수 생성
참고. Lambda@Edge와 Lambda의 차이
Lambda@Edge는 기본적으로 AWS Lambda를 기반으로 동작하지만, CloudFront의 엣지 로케이션(전 세계 여러 위치)에서 실행됩니다. Lambda@Edge는 요청을 처리하는 시점에 실행되어 클라이언트 가까운 위치에서 로직을 수행할 수 있어 지연 시간을 줄일 수 있습니다. (일반 Lambda 함수는 특정 AWS 리전에서 실행되는 반면, Lambda@Edge는 전 세계의 CloudFront 엣지 로케이션에서 실행됩니다.)
- Lambda@Edge 생성시 region은 "us-east-1" 에서만 허용합니다
- region이 틀릴 경우 CloudFront에서 Lambda@Edge 연결시 아래와 같이 에러 메시지가 출력됩니다
참고. stackoverflow
https://stackoverflow.com/questions/61635935/why-lambdaedge-has-to-be-in-us-east-1-region
aws console 우측 상단에 region을 us-east-1로 변경 후 람다 함수를 생성합니다
앞서 생성한 역할 LambdaEdgeExecutionRole을 선택 후 생성합니다
람다 함수의 script의 경우 크몽 개발자의 포스팅을 참고했습니다. 쿼리스트링 파싱해서 변환해준 후 콜백 함수로 이미지 리사이징 결과를 반환하는 내용입니다. 이때 쿼리 스트링 파라미터가 없는 경우 원본 리소스를 반환합니다.
참고. 크몽 - 이미지 리사이징
람다 생성시 nodejs 20.x 선택해서 node 버전은 v20.11.0으로 작업했습니다.
$ mkdir lambda-image-resize
$ npm init -y
index.js
const sharp = require('sharp');
const aws = require('aws-sdk');
const s3 = new aws.S3({
region: 'ap-northeast-2'
});
const BUCKET = '버킷명';
exports.handler = async (event, _, callback) => {
const { request, response } = event.Records[0].cf;
/** 쿼리 설명
* w : width
* h : height
* f : format
* q : quality
* t : type (contain, cover, fill, inside, outside)
*/
const querystring = request.querystring;
const searchParams = new URLSearchParams(querystring);
if (!searchParams.get('w') && !searchParams.get('h')) {
return callback(null, response);
}
const { uri } = request;
const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);
const width = parseInt(searchParams.get('w'), 10);
const height = parseInt(searchParams.get('h'), 10);
const quality = parseInt(searchParams.get('q'), 10) || DEFAULT_QUALITY;
const type = searchParams.get('t') || DEFAULT_TYPE;
const f = searchParams.get('f');
const format = (f === 'jpg' ? 'jpeg' : f) || extension;
try {
const s3Object = await getS3Object(s3, BUCKET, imageName, extension);
const resizedImage = await resizeImage(s3Object, width, height, format, type, quality);
response.status = 200;
response.body = resizedImage.toString('base64');
response.bodyEncoding = 'base64';
response.headers['content-type'] = [
{
key: 'Content-Type',
value: `image/${format}`
}
];
response.headers['cache-control'] = [{ key: 'cache-control', value: 'max-age=31536000' }];
return callback(null, response);
} catch (error) {
return callback(error);
}
};
const DEFAULT_QUALITY = 80;
const DEFAULT_TYPE = 'contain';
async function getS3Object(s3, bucket, imageName, extension) {
try {
const s3Object = await s3.getObject({
Bucket: bucket,
Key: decodeURI(imageName + '.' + extension)
}).promise();
return s3Object;
} catch (error) {
console.log('s3.getObject error: ', error);
throw new Error(error);
}
}
async function resizeImage(s3Object, width, height, format, type, quality) {
try {
const resizedImage = await sharp(s3Object.Body)
.resize(width, height, { fit: type })
.toFormat(format, {
quality
})
.toBuffer();
return resizedImage;
} catch (error) {
console.log('resizeImage error: ', error);
throw new Error(error);
}
}
zip 파일로 작업 디렉토리 전체를 압축합니다.
$ npm install sharp aws-sdk
// 리눅스 환경 경우
$ npm install --arch=x64 --platform=linux --target=16x sharp aws-sdk
// zip 파일 압축
$ zip -r lambda-edge-image-resize .
같은 region은 S3만 연결이 되는데 us-east-1 에 S3 생성해서 파일 업로드 후 람다에 업로드 해줍니다.
CloudFront, Lambda@Edge 설정
글로벌 cloudfront 상세에서 [동작] 탭으로 이동하여 설정을 정의합니다.
(경로 패턴을 /origin/*으로 해서 querystring이 없는 경우 원본 리소스를 S3에서 가져올 수 있도록 하였습니다.)
그리고 아래와 같이 us-east-1 리전에 생성한 람다 함수의 arn 주소를 설정해줍니다.
마지막으로 us-east-1 리전에 생성한 람다 함수로 이동하여 트리거로 cloudfront를 연결해줍니다.
최종 결과
샘플 이미지는 제가 좋아하는 펭수 이미지를 사용했습니다 !
원본 이미지 정보는 아래와 같고, 이제 w=700, h=700으로 이미지 리사이징 호출해보겠습니다.
첫 이미지 호출시 Miss from cloudfront가 확인되고 605ms의 시간이 소요되었습니다.
두 번째 호출시 Hit from cloudfront가 확인되고 31ms의 시간이 소요되었습니다.
CloudFront 캐싱 덕분에 약 20배 정도의 시간이 절약된 것을 확인할 수 있네요.
CloudFront + Lambda@Edge 방식 외에도 CloudFront + API Gateway + Lambda 방식도 있는 것으로 보였습니다.
차이라면 API Gateway 통해 좀 더 세밀하게 트래픽 제어가 가능한 것으로 보였는데, 기회가 된다면 포스팅하도록 하겠습니다.
주말에 시간내서 해봤는데 실습과 정리하는데 이틀이 걸렸네요. (내 주말...)
직접 코드를 작성하는 것도 재미있지만 이렇게 인프라 연결해서
실무에서도 유용하게 활용가능한 기능을 구현해 볼 수 있어 재미있는 시간이었습니다.
Reference.
aws 공식 가이드 - Lambda@Edge를 사용하여 엣지에서 사용자 지정
크몽 - 더 나은 사용자 경험을 위한 이미지 리사이징을 해보자
aws 공식 - Node.js에서 Lambda 함수 핸들러 정의
aws 블로그 - Resizing Images with Amazon CloudFront & Lambda@Edge | AWS CDN Blog
블로그 - AWS Lambda@Edge에서 실시간 이미지 리사이징(On-The-Fly) & WebP 형식으로 변환
'공부 > AWS' 카테고리의 다른 글
[AWS] Lambda 활용하여 image resize (+CloudFront) - (1) (0) | 2024.09.08 |
---|---|
[AWS] EC2 서버 application heap dump 생성/다운로드 (with wsl2) (0) | 2023.01.16 |
[AWS] EC2 와 RDS 연결/접속 하기 (0) | 2023.01.14 |
[AWS] MFA 설정 가이드 (0) | 2022.03.14 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!