[AWS] cloudfront + Lambda@edge 를 활용한 이미지 리사이징하기 3
이제 마지막으로 이미지 리사이징을 위한 코드를 작성 및 배포를 진행하겠습니다. 코드는 node.js 로 작성 및 배포합니다. 이미지 리사이징은 라이브러리는 Sharp.js 를 사용하여 리사이징 할 것입니다.
버전 및 라이브러리 정보.
항목 | 설명 |
서버 | node.js |
버전 | v16.x |
이미지 라이브러리 | Sharp |
프로젝트 세팅하기
1. 프로젝트 경로로 이동 후 소스폴더를 생성합니다.
# 소스 폴더 생성
mkdir /src
2. 이제 npm 을 통해 프로젝트 초기 설정을 하겠습니다.
# 프로젝트 초기화
npm init
# Sharp 라이브러리 설치
npm install sharp
3. index.js 파일을 생성하고 코드를 작성해 보겠습니다.
"use strict";
// ❶ 필수 모듈
const querystring = require("querystring"); // Don't install.
const AWS = require("aws-sdk"); // Don't install.
const Sharp = require("sharp");
// ❷ S3 클라이언트, 버킷명 설정
const S3 = new AWS.S3({
region: "ap-northeast-2",
});
// ❷-1. 버킷명 버킷명이 틀리면 오류가 발생할 수 있습니다.
const BUCKET = "lo-gos-test";
// ❸ 코드 시작
// (아래 코드에서 사용하는 내부 함수는 생략했습니다. 주요 코드는 github 에서 확인해주세요.)
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;
let [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);
extension = extension.toLowerCase();
if (isGif(extension)) {
console.log("GIF image requested!");
console.log("response, content-type", response.headers["content-type"]);
// change content-type to image/gif
response.headers["content-type"] = [
{
key: "Content-Type",
value: `image/gif`,
},
];
return callback(null, response);
}
// Init variables
let format;
let s3Object;
let resizedImage;
try {
s3Object = await S3.getObject({
Bucket: BUCKET,
Key: decodeURI(imageName + "." + extension),
}).promise();
} catch (error) {
console.log("S3.getObject: ", error);
return callback(error);
}
const origintLength = s3Object.ContentLength;
console.log(`origin byteLength ${origintLength}`);
try {
const image = Sharp(s3Object.Body, {
animated: isGif(extension),
// failOn: "truncated", // 짤린(손상된) 이미지만 오류 반환
failOn: "none", // 모든 변환 실패 오류 반환 하지 않음
});
const meta = await image.metadata();
// Init format.
format = initFormat(params.f, extension);
console.log(`format : ${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`.
// Image crop logic
const crop = params.crop ? params.crop.split("x") : null;
// crop
if (crop && !isGif(extension)) {
const top = parseInt(crop[0], 10);
const cropHeight = parseInt(crop[1], 10);
image
.extract({
left: 0,
top: top,
width: meta.width,
height: cropHeight,
quality: getQuality(format, params.q),
})
.resize({
width: initWidth(meta.width, params.w),
height: initHeight(meta.height, params.h),
});
} else {
image.rotate().resize({
width: initWidth(meta.width, params.w),
height: initHeight(meta.height, params.h),
});
}
if (isPng(params.f)) {
image.jpeg({
// Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
quality: getQuality(format, params.q),
chromaSubsampling: "4:4:4",
mozjpeg: true,
});
} else {
image.toFormat(format, {
// Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
quality: getQuality(format, params.q),
});
}
resizedImage = await image.toBuffer();
} catch (error) {
console.log("Sharp: ", error);
return callback(error);
}
const resizedImageByteLength = Buffer.byteLength(resizedImage, "base64");
console.log("resized 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}`,
},
];
// cahce 만료시간 설정
response.headers["cache-control"] = [
{ key: "Cache-Control", value: "public, max-age=5184000" },
];
return callback(null, response);
};
배포 파일 생성을 위해 Docker 설정하기
1. Dockerfile 작성
FROM amazonlinux:2
WORKDIR /tmp
#install the dependencies
RUN yum -y update
RUN yum -y install gcc-c++
RUN yum -y install findutils
RUN yum -y install tar gzip
RUN yum -y install glibc
RUN touch ~/.bashrc && chmod +x ~/.bashrc
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
RUN source ~/.bashrc && nvm install 14.15.1
WORKDIR /build
2. 도커 빌드 실행을 위한 스크립트 작성
# build.sh
#!/bin/bash
# docker를 빌드
docker build -t amazon-nodejs .
echo 'docker volume =====>' ${PWD}/src:/build
# 빌드된 이미지로 sharp를 제외한 라이브러리 설치 (querystring, request)
# 빌드된 결과를 /src에 동기화하기 위해 --volume 옵션 사용
docker run --rm --volume ${PWD}/src:/build amazon-nodejs /bin/bash -c \
"source ~/.bashrc; npm init -f -y; npm install querystring --save; npm install request --save;npm install --only=prod"
echo '================ install sharp ================'
# Corss-Platform 방법으로 Sharp를 설치
cd src
rm -rf node_modules/sharp
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux sharp
echo '================ finish sharp ================ \n'
echo mkdir /dist in ... $PWD
# Dist 폴더 생성
mkdir -p ../dist
# Deployment Package로 .zip 파일로 만들기
zip -FS -q -r ../dist/functions.zip *
3. 스크립트 실행. (Mac 기준)
$ sh build.sh
# ./dist/functions.zip 생성 확인
스크립트를 실행하면 아래 경로에 압축파일이 생성 됩니다.
IAM 정책 및 역할 생성
람다(Lambda)를 CloudFront 배포(Deploy)와 연결하기 위한 IAM 권한을 설정합니다.
1. AWS Console 에서 IAM 설정 페이지로 이동 합니다. 왼쪽 메뉴에서 [정책] 메뉴를 클릭 후 설정 페이지로 이동합니다.
2. 정책 생성 버튼을 클릭 합니다.
3. [정책 작성] 페이지에서 [JSON] 으로 선택 합니다.
4. [JSON 편집] 영역에 정책 권한을 입력합니다.
- s3:PutObject 권한은 필요치 않습니다.
- cloudfront:CreateDistribution 또는 cloudfront:UpdateDistribution 중 하나만 설정합니다.
- CloudWatch에서 로그 데이터를 처리하기 위해 logs:xxx 권한들을 설정합니다.
{
"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": "*"
}
]
}
5. [다음] 버튼을 클릭 후 정책 명을 추가 한뒤 저장합니다. 저는 정책 명은 [LambdaResizePolicy] 으로 작성하겠습니다.
IAM / 역할 생성
Lambda@edge에 연결할 역할을 생성합니다.
1. AWS Console 에서 역할 페이지로 이동 후 [역할 만들기] 버튼을 클릭 합니다.
2. 이 역할을 사용할 서비스로 Lambda를 선택합니다.
3. 정책 [LambdaResizePolicy] 를 선택합니다.
4. 역할 이름과 (lambdaResize) 기타 정보를 입력 후 [역할 생성] 버튼을 누릅니다.
2. 역할의 신뢰 관계(정책) 수정
2. 나의 역할 목록에서 방금 생성한 lambdaResize 선택합니다.
3. [신뢰 관계] 탭 이동 후 편집 선택을 선택합니다.
4. 아래의 신뢰 관계 정보를 입력합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
5. [정책 수정] 버튼을 클릭 하고 저장합니다.
CloudFront 동작 설정 하기
클라우드 프론트에서 [동작] 설정을 변경해야 합니다. 기존 동작에서는 쿼리스트링을 적용해도 정상적으로 캐싱되지 않습니다.
1. 클라우드 프론트 설정에서 [동작] 탭으로 이동 합니다. ❶ 변경할 [동작] 항목을 체크하고 ❷ [수정] 버튼을 클릭 합니다.
2. 동작설정 페이지에서 [캐시 키 및 원본 요청] 설정 항목에 [❷ 캐시 정책]을 선택 합니다. 만약 생성된 정책이 없다면 [❶ 정책 생성 링크] 를 클릭 후 2-1 번 항목을 따라 수행하세요.
2-1. [캐시 정책] 생성 하기
❶ [상세] 항목에서 정책 이름을 입력합니다..
❷ [캐시 키 설정] 항목에서[❷ 지정된 쿼리 문자열 포함] 선택 합니다.
❸ 허용 할 문자열 정보를 추가 합니다. (q, f, w, h, crop)
❹ 변경 사항을 [저장] 후 [2 동작 편집]에서 캐시 정책을 설정하세요.
AWS Lambda 함수 배포하기
이제 함수 파일을 Lambda에 배포해보겠습니다. AWS Consol 로그인 후 US East (N. Virginia) 리젼으로 설정합니다.
콘솔에서 Lambda 설정 페이지로 이동합니다.
1. 람다 함수 생성하기
아래 이미지 순서대로 실행하여 함수를 생성합니다.
2. 람다 함수 파일 업로드 하기
2-1. 생성 이동 된 페이지에서 람다 파일을 업로드 하겠습니다. 아래 [Code] 탭에서 [Upload from] 버튼을 클릭 합니다.
2-2. 선택항목에서 [.zip file] 을 클릭합니다.
2-3. 파일 업로드 후 저장합니다.
2. 람다 함수 테스트 하기
람다 함수에 빌드된 funtions.zip 파일 업로드를 완료 했습니다. 이제 정상적으로 동작하는 지 테스트를 해보겠습니다. 람다 함수 설정 페이지에서 [Test] 탭으로 이동합니다.
2-1. 테스트 이벤트 추가하기.
[create new event] 체크 후 생성할 이벤트 명을 추가합니다. 그리고 아래 [Event JSON] 란에 테스트 json 을 추가하겠습니다.
{
"Records": [
{
"cf": {
"config": {
"distributionId": "EXAMPLE"
},
"request": {
"uri": "cute-7973191_1280.webp",
"querystring": "",
"method": "GET",
"clientIp": "2001:cdba::3257:9652",
"headers": {
"host": [
{
"key": "Host",
"value": "d123.cf.net"
}
],
"user-agent": [
{
"key": "User-Agent",
"value": "Test Agent"
}
],
"user-name": [
{
"key": "User-Name",
"value": "aws-cloudfront"
}
]
}
},
"response": {
"headers": {
"content-type": [
{
"key": "Content-Type",
"value": " image/webp;"
}
],
"content-length": [
{
"key": "Content-Length",
"value": "9593"
}
]
},
"status": "200",
"statusDescription": "OK"
}
}
}
]
}
2-2. [테스트] 버튼을 클릭 하면 응답 결과가 나옵니다. 아래와 같이 성공 메시지가 나오면 잘 적용된 것입니다.
3. 람다 함수 배포하기
3-1. 이제 람다 함수를 배포하겠습니다, 오른쪽 상단에 [액션] 버튼을 클릭 후 [Lamba@Edge] 배포 버튼을 클릭 하세요.
3-2. 아래 항목을 선택합니다.
❶ lambda@edge를 배포할 Cloudfront 를 선택 합니다.
❷ 캐시 동작은 [ * ] 로 선택합니다.
❸ CloudFront 이벤트는 [Origin response] 로 선택합니다.
❹ 입력 사항을 잘 확인 하고 [체크 버튼] 을 클릭 합니다.
❺ [배포] 버튼을 클릭 합니다.
3-3. 배포 버튼을 클릭 하면 위에서 설정한 [대상 Cloudfront]에 배포가 됩니다. 배포가 완료되면 실제 이미지 주소로 들어가 정상적으로 리사이징 된 이미지가 캐싱되는 지 확인 하겠습니다.
최종 테스트 하기
이제 마지막으로 S3 에 업로드 된 이미지의 width 값 변경 및 이미지 포멧을 변경하는 테스트를 해보겠습니다. 배포된 cloudfront 주소로 png 이미지 파일을 접속 해봤습니다. 첫번째 이미지는 쿼리스트링 없이 이미지 주소로만 접속한 결과 입니다. 원본 이미지의 사이즈는 1280x851입니다. 두번째 이미지는 쿼리스트링에 w=100&f=webp 를 주고 다시 요청해본 결과 입니다. 넓이가 100으로 변경되었고 이미지 포맷 또한 webp로 변경되었습니다. 쿼리스트링을 포함한 주소로 첫번째 요청의 경우 x-Cache 정보가 Miss from cloudfront 로 나타나지만 두번째 요청 부터는 Hit from cloudfront 로 캐싱 처리된 이미지가 반환 되는 것을 확인하실 수 있습니다.
이제 최종 마무리 테스트까지 완료 하였습니다. Lambda@edge 관련해서 설정해야하는 영역이 이곳 저곳 많다 보니 포스팅 내용이 많이 길어 졌습니다. 최대한 상세하게 작성하였으나 놓친 부분이나 잘못된 내용이 있을 수 있습니다. 잘못된 내용이 있으면 포스팅 아래에 댓글로 남겨주시면 포스팅 내용에 반영 하겠습니다. 여기까지 읽어 주셔서 감사합니다.
[참고내용]
- https://heropy.blog/2019/07/21/resizing-images-cloudfrount-lambda/
- https://afrobambacar.github.io/2018/12/image-resizing-with-lambda-edge.html
- https://aws.amazon.com/ko/blogs/networking-and-content-delivery/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/
- https://v3.leedo.me/image-resize-by-cloudfront-lambda-edge
- https://blog.wonizz.tk/2020/04/21/lambda-edge-guide/