response.body
가 1MB 이상일 경우에 대한 예외처리 코드를 추가했습니다.Lambda@Edge는 Amazon CloudFront의 기능 중 하나로서, 서버를 프로비저닝하고, 코드(Node.js)를 AWS Lambda에 업로드하고 요청에 대한 응답으로 함수가 사용자에게 좀 더 가까운 위치(리전)에서 트리거 되도록 구성할 수 있습니다.
Lambda@Edge를 이용해 다음과 같이 쿼리스트링을 옵션으로 요청에 따라 실시간으로 이미지의 크기(w, h), 품질(q), 파일 형식(포맷, f)을 변경할 수 있도록 구성하고자 합니다.
https://heropy.blog/heropy.png?w=150&q=70&f=webp
람다(Lambda)를 CloudFront 배포(Deploy)와 연결하기 위한 IAM 권한을 설정합니다.
s3:PutObject
권한은 필요치 않습니다.cloudfront:CreateDistribution
또는 cloudfront:UpdateDistribution
중 하나만 설정합니다.logs:xxx
권한들을 설정합니다.정책 생성
을 선택합니다.JSON
을 선택해 아래 정책을 입력합니다.ResizingImages
)과 설명을 작성합니다.정책 생성
!{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:CreateServiceLinkedRole",
"lambda:GetFunction",
"lambda:EnableReplication",
"cloudfront:UpdateDistribution",
"s3:GetObject",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
"Resource": "*"
}
]
}
역할 만들기
를 선택해 다음과 같이 진행합니다.Lambda
를 선택합니다.ResizingImages
를 선택합니다.ResizingImages
)과 설명을 작성합니다.역할 만들기
!서비스 보안 주체 lambda.amazonaws.com
및 edgelambda.amazonaws.com
에 권한을 위임하기 위해 IAM 역할을 수정합니다.
ResizingImages
선택합니다.신뢰 관계
탭의 신뢰 관계 편집
선택을 선택합니다.신뢰 정책 업데이트
!{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
CloudFront Origin Access Identity
가 연결한 CloudFront의 Origin Access Identity인지 확인하시면 됩니다.퍼블릭
액세스 권한이 필요할 수 있습니다.favicon.ico
파일을 업로드하세요. S3 Favicon error가 발생할 수 있습니다.다음 Lambda@Edge에 대한 제한을 주의합니다.
엔터티 | 오리진 요청 및 응답 이벤트 제한 | 최종 사용자 요청 및 응답 이벤트 제한 |
---|---|---|
함수 리소스 할당 | Lambda 제한과 동일 | 128MB |
함수 제한 시간 | 30초 | 5초 |
헤더 및 본문을 포함하여 Lambda 함수에서 생성되는 응답의 크기 | 1MB | 40KB |
Lambda 함수 및 포함된 라이브러리의 최대 압축 크기 | 50MB | 1MB |
미국 동부(버지니아 북부)
(us-east-1)만 허용됩니다.함수 생성
을 선택해 다음과 같이 설정합니다.ResizingImages
)을 입력합니다.Node.js 10.x
)을 선택합니다.실행 역할 선택 또는 생성
기존 역할 사용
ResizingImages
함수 생성
!3초
로 설정하면 최초 요청(Cache miss)에 연산이 많아지면 리사이징되지 않을 수 있습니다. 10초
로 설정합니다.저장
!(화면 오른쪽 위)리전을 미국 동부(버지니아 북부)
이 아닌 아시아 태평양(서울)
로 설정했더니 다음과 같이 에러 메시지가 두둥!
로컬(macOS)에서 람다 코드를 세팅하고 zip파일로 업로드하니,
"'darwin-x64' binaries cannot be used on the 'linux-x64' platform. Please remove the 'node_modules/sharp/vendor' directory and run 'npm install'."
라는 에러 메시지가 출력되네요.node_modules
디렉터리에 있는 Sharp 바이너리는 linux-x64
플랫폼용으로 설정되어야 하기 때문에,
docker-lambda로 람다 런타임과 일치하는 환경을 복제해 사용할 수 있습니다.
하지만 저는 다른 방법으로 파일 업로드(zip)를 하지 않고 작성하고 싶어 Cloud 9을 사용했습니다.
모듈은 Sharp만 설치하시면 됩니다.
람다 코드를 작성하기 위해 새로운 Cloud 9 환경을 설정합니다.
아직 서울 리전은 지원되지 않습니다.
Create environment
를 선택합니다.ResizingImages
)과 Description을 입력합니다.Create environment
!위에서 생성했던 람다 함수를 Cloud 9 환경으로 불러옵니다.
AWS Resources
를 선택합니다.Remote Functions
목록의 ResizingImages
를 Import
합니다.package.json
을 생성하고 Sharp 모듈을 설치합니다.)$ cd ResizingImages
$ npm init -y
$ npm i sharp
index.js
을 아래 코드와 같이 수정합니다.$LATEST
버전으로 배포합니다.Local Functions
목록의 ResizingImages
를 Deploy
합니다.'use strict';
const querystring = require('querystring'); // Don't install.
const AWS = require('aws-sdk'); // Don't install.
const Sharp = require('sharp');
const S3 = new AWS.S3({
region: '<YOUR_BUCKET_REGION>'
});
const BUCKET = '<YOUR_BUCKET_NAME>';
exports.handler = async (event, context, callback) => {
const { request, response } = event.Records[0].cf;
// Parameters are w, h, f, q and indicate width, height, format and quality.
const params = querystring.parse(request.querystring);
// Required width or height value.
if (!params.w && !params.h) {
return callback(null, response);
}
// Extract name and format.
const { uri } = request;
const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);
// Init variables
let width;
let height;
let format;
let quality; // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
let s3Object;
let resizedImage;
// Init sizes.
width = parseInt(params.w, 10) ? parseInt(params.w, 10) : null;
height = parseInt(params.h, 10) ? parseInt(params.h, 10) : null;
// Init quality.
if (parseInt(params.q, 10)) {
quality = parseInt(params.q, 10);
}
// Init format.
format = params.f ? params.f : extension;
format = format === 'jpg' ? 'jpeg' : format;
// For 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`.
try {
s3Object = await S3.getObject({
Bucket: BUCKET,
Key: decodeURI(imageName + '.' + extension)
}).promise();
} catch (error) {
console.log('S3.getObject: ', error);
return callback(error);
}
try {
resizedImage = await Sharp(s3Object.Body)
.resize(width, height)
.toFormat(format, {
quality
})
.toBuffer();
} catch (error) {
console.log('Sharp: ', error);
return callback(error);
}
const resizedImageByteLength = Buffer.byteLength(resizedImage, 'base64');
console.log('byteLength: ', resizedImageByteLength);
// `response.body`가 변경된 경우 1MB까지만 허용됩니다.
if (resizedImageByteLength >= 1 * 1024 * 1024) {
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);
};
만약 GIF 포맷을 사용하는 경우 원본 그대로 반환하도록 예외처리할 수 있습니다.
Sharp 라이브러리에서 GIF 포맷을 다시 GIF로 리사이징하는데 문제를 확인했습니다.
관련 이슈는 https://github.com/lovell/sharp/issues/1372에서 확인할 수 있습니다.
// Exception '.gif' image.
if (extension === 'gif') {
console.log('GIF image requested!');
return callback(null, response);
}
다음과 같이 변환할 포맷이 없는 경우만 처리할 수도 있습니다.
if (extension === 'gif' && !params.f) {
console.log('GIF image requested!');
return callback(null, response);
}
혹은 CloudFront에서 예외처리할 수도 있습니다.
캐시 동작 순서를 주의합니다!
Behaviors
탭에서 Create Behavior
를 선택합니다.Create
!*.gif
Yes
$LATEST 버전으론 Lambda@Edge를 사용할 수 없음으로 새로운 버전을 게시하여 CloudFront에 연결합니다.
내용을 수정했다면 수정된 버전을 게시하여 CloudFront에 연결해야 합니다.
ResizingImages
를 선택합니다.새 버전 게시
를 선택합니다.게시
합니다.ARN - arn:aws:lambda~~~:ResizingImages:1
, 화면 오른쪽 위)하여 CloudFront에 연결해야 합니다.만약 람다를 수정했다면 다음과 같이 새 버전을 게시하고 CloudFront와 연결합니다.
ARN - arn:aws:lambda~~~:ResizingImages:2
, 화면 오른쪽 위)합니다.Behaviors
탭에서 기본 Behavior를 체크하고 Edit
를 선택합니다.Yes, Edit
!arn:aws:lambda~~~:ResizingImages:2
)만약 람다를 수정했다면 좀 더 쉽게 새 버전을 게시하고 CloudFront와 연결할 수 있습니다.
CloudFront 설정이 미리 필요합니다.
Lambda@Edge 배포
를 선택합니다.'이 함수에 기존 CloudFront 트리거 사용
을 선택하고 내용을 확인한 뒤 배포
합니다.새로운 트리거를 구성할 경우 다음과 같이 설정할 수 있습니다.
Create Distribution
을 선택합니다.Get Started
를 선택하고 다음과 같이 설정합니다.Yes
(항상 CloudFront URL로 S3 액세스하도록 요구)Create a New Identity
Yes, Update Bucket Policy
(CloudFront가 S3의 버킷 정책에 액세스하여 업데이트)Forward all, cache based on whitelist
w
, h
, f
, q
(CloudFront가 사용할(캐싱 기반) 쿼리스트링 매개 변수 정의)Yes
(Accept-Encoding: gzip
요청에 대한 콘텐츠 압축 여부)Origin Response
arn:aws:lambda~~~:ResizingImages:1
)ResizingImages
(Distribution 구분을 위한 간략한 이름/설명)Create Distribution
!Distribution의 ‘Status’를 확인하세요. In Progress
에서 Deployed
로 변경되는데 10~15분 정도 소요됩니다.
CloudFront 설정에서 대체 도메인 이름(CNAME)을 사용하면 CloudFront에서 배포용 도메인 이름 대신에 고유의 도메인 이름(예: www.heropy.blog)이 사용됩니다.
이 포스트에선 대체 도메인을 지정하지 않습니다.
CloudFront Distribution가 배포 완료되면, CloudFront Domain Name으로 다음 예제와 같이 이미지에 접근할 수 있습니다.
d2d73zr4p2zskr.cloudfront.net/heropy.png?w=120&f=webp&q=90
쿼리스트링의 순서가 바뀌지 않도록 주의합니다!
CloudWatch를 통해 람다 함수에서 발생하는 로그를 확인할 수 있습니다.
Lambda@Edge를 배포하면 그 함수를 전 세계의 AWS 리전에 복제하기 때문에 요청이 들어온 리전에서 해당 람다의 로그를 확인해야 합니다.
따라서 일반적인 CloudWatch 로그는 서울 리전에서 확인하시면 됩니다.
한 번의 테스트에서 PNG 포맷의 원본 이미지(500x500px)를 555px 크기의 WEBP 포맷으로 변경하는데,
‘Cache miss’ 경우 5.26s
가, ‘Cache hit’ 경우 31ms
가 소요되었습니다.
CloudFront 엣지 캐시에서 파일이 만료되기 전에 파일을 제거해야 할 경우, 다음과 같이 설정합니다.
Invalidations
탭에서 Create Invalidation
을 선택합니다./heropy.png
, /*
, /images/*
Invalidate
!https://engineering.huiseoul.com/lambda-%ED%95%9C%EA%B0%9C%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-on-demand-image-resizing-d48167cc1c31
https://ikso2000.tistory.com/106
https://medium.com/daangn/aws-lambda%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%8D%B8%EB%84%A4%EC%9D%BC-%EC%83%9D%EC%84%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-acc278d49980
https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html
https://jasonbla.tistory.com/5
https://github.com/lovell/sharp/issues/1372
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-lambda-at-edge