안녕하세요. ManVSCloud 김수현입니다.
오늘은 CloudFront와 Lambda@Edge를 활용하여 이미지 리사이징을 해보려합니다.
이전부터 해봐야겠다고 생각은 했으나 저는 무언가를 시작할 때 왜 이것을 해야 하는지,
이것을 함으로써 어떤 장점을 얻을 수 있는지를 먼저 알지 못하면 시작하지 못하는 편입니다.
이미지 리사이징는 왜 사용해야할까요?
최근에 나오는 휴대 전화와 디지털 카메라는 고화질로 촬영할 수있는 스펙을 가지고있어
웹 사이트에 업로드하기 적합하지 않을 때가 있습니다.
또한 여러 사용자들의 디바이스에서 요구하는 해상도가 달라 고정된 해상도의 이미지 파일 하나를 업로드 해둘 경우 최적화된 서비스를 할 수 없거나 여러 해상도마다의 이미지를 스토리지 서버에 올려두어 용량 낭비를 하게 될 수도 있습니다.
이런 케이스마다 이미지를 리사이징 해주는 기능이 있다면 좋을 것같습니다.
게다가 이미지 리사이징은 SEO(Search Engine Optimization)에도 큰 도움이 됩니다.
* SEO(Search Engine Optimization) : 검색 엔진 최적화
하지만 이미지 리사이징은 생각보다 간단하지 않습니다.
이미지의 크기를 변경한다는 것은 이미지의 품질이 손상 될 수 있지 않을까라는 고민을 하게 만들기도 합니다.
실습을 통해 이미지 리사이징이 어떻게 되는지 원리에 대해 알아보고
품질에 영향이 클지 등을 알아보도록 하겠습니다.
(실습의 결론부터 말하자면 잘 되지않았습니다…?)
Principles of Image Resizing

① URI에 원하는 해상도의 이미지를 요청합니다.
ex) https://img.manvscloud.com/image/tom.png?w=200&h=150&f=webp&q=90
② 캐시 서버(CloudFront/Edge Location)에서 캐싱된 이미지가 있으면 이미지를 전송하지만 캐시된 이미지가 없을 경우 이미지 리사이징 서버(Lambda@Edge)에 API 또는 SDK를 이용한 리사이징된 이미지를 요청합니다.
③ 이미지 리사이징 서버(Lambda@Edge)는 API나 SDK를 통해 이미지가 존재하는 스토리지 서버(S3)에 원본 이미지를 요청하게 됩니다.
④ 스토리지 서버(S3)가 이미지 리사이징 서버(Lambda@Edge)로 이미지를 전송합니다.
⑤ 이미지 리사이징 서버(Lambda@Edge)는 스토리지 서버(S3)에게 이미지를 받은 후 요청한 URI를 해석한 뒤 원하는 이미지로 리사이징하여 캐시 서버(CloudFront/Edge Location)로 전송합니다.
⑥ 요청한 해상도의 이미지를 GET 합니다.
원리는 대략 이런 식이라고 볼 수 있습니다.
그럼 이제 실습하여 실제로 이미지 리사이징이 잘 되는지 확인해보도록 하겠습니다.
IAM – Policy & Role
우선 IAM을 만들어보겠습니다.
Lambda를 Cloudfront 배포하고 연결하기 위한 IAM 권한 설정이 필요합니다.
? 정책 생성
[IAM] – [액세스 관리] – [정책] – [정책 생성]
{
"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": "*"
}
]
}
? 역할 생성
[IAM] – [액세스 관리] – [역할] – [역할 만들기]
→ 사용할 서비스 : Lambda
→ 위에서 생성한 정책을 추가합니다.
→ 역할 만들기
역할 생성 후 생성된 역할의 신뢰 관계가 아래 그림과 같이 설정되어 있는지 확인합니다.

신뢰할 수 있는 개체가 그림과 같이 설정되어있지 않다면 [신뢰 관계 편집]을 이용하여
수정 후 업데이트 해줍니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
S3 & CloudFront
? S3 버킷 생성
[Amazon S3] – [버킷 만들기]


이후 image/에 tom.png 파일을 업로드 해두도록 하겠습니다.
? CloudFront 생성
[CloudFront] – [Create Distribution] – [Get Started]

Origin Domain Name에 위에서 생성한 S3를 선택해줍니다.
Enable Origin Shield는 이번 포스팅에서 다루지 않겠습니다.
이후 포스팅 될 “CloudFront Cache Hit Ratio” 를 참고해주세요.
Restrict Bucket Access를 “Yes”로 체크해주게 되면 버킷의 접근 제한을 활성화 하겠다는 의미입니다.
Origin Access Identity는 신규로 생성하여 사용하겠습니다.
Grant Read Permisstions on Bucket은 해당 S3 버킷 정책에 읽기 권한을 Update할거야? 하고 묻는겁니다.

Query String Forwading and Caching 설정이 Forward all, cache based on whitelist으로 되었다면
Query String whitelist에 쿼리 스트링 키를 지정해줄 수 있게 됩니다.
예를 들어 쿼리 스트링 키로 img를 주고 싶다면 Query String whitelist에 img를 입력하시면 됩니다.
오리진 서버에서 하나 이상의 쿼리 문자열 파라미터를 기준으로 다른 버전의 객체를 반환할 경우 필요합니다. (쿼리 문자열을 이용하여 캐시 우회 가능)

SSL Certificate를 Default로 하는 경우 CNAMEs 을 사용할 수 없습니다.
또한 Default는 보안 TLS 버전을 선택할 수 없습니다.
버지니아 북부 리전에서 ACM을 생성하여 Cloudfront의 Custom SSL Certificate를 선택할 수 있으니 이 방법을 적극 권장드립니다.

생성된 Cloudfront에서 Behaviors를 이용하면 S3 버킷내에 디렉토리 별로 옵션 설정이 가능합니다.

설정 이후 cloudfront를 이용하여 S3 버컷에 올려둔 이미지 파일이 보이는지 확인해보도록 하겠습니다.

Lambda@Edge (with Cloud9)
? Create Lambda
[버지니아 북부 리전] – [AWS Lambda] – [함수 생성] – [새로 작성]
(Lambda@Edge는 미국 동부(버지니아 북부)(us-east-1)만 허용됩니다.)

위 그림과 동일하게 이름/버전/역할을 정해준 뒤 [함수 생성]을 클릭해줍시다.
(역할은 위 <IAM – Policy & Role>에서 생성한 역할로 선택해줍니다. )
| Entity | 오리진 요청 및 응답 이벤트 제한 | 최종 사용자 요청 및 응답 이벤트 제한 |
|---|---|---|
| 함수 리소스 할당 | Lambda 제한과 동일 | 128MB |
| 함수 제한 시간 | 30초 | 5초 |
| 헤더 및 본문을 포함하여 Lambda 함수에서 생성되는 응답의 크기 | 1MB | 40KB |
| Lambda 함수 및 포함된 라이브러리의 최대 압축 크기 | 50MB | 1MB |
? Cloud9
[AWS Cloud9] – [Create environment]



[Next step] – [Create environment]

잠시 후 Cloud9이 생성됩니다.

좌측 AWS 마크를 누른 후 AWS:Explorer에서 Show region in the Explorer를 클릭하여
Lambda가 있는 리전을 선택해주도록 합시다.

Lambda 함수를 생성 했던 US East (N. Virginia) 버지니아 북부 리전을 선택해줍니다.

좌측 버지니아 북부 Lambda에서 생성한 ImageResize 함수를 가져옵니다

위 사진에 이어 명령어를 입력하여 npm초기화와 설치를 진행해줍니다.
cd ImageResize (디렉토리 이동) npm init -y npm install sharp npm install aws-sdk
'use strict';
const querystring = require('querystring'); // Don't install.
const AWS = require('aws-sdk'); // Don't install.
// http://sharp.pixelplumbing.com/en/stable/api-resize/
const Sharp = require('sharp');
const S3 = new AWS.S3({
signatureVersion: 'v4',
region: 'ap-northeast-2' // 버킷을 생성한 리전 입력
});
const BUCKET = '[mybucket]' // Input your bucket
// Image types that can be handled by Sharp
const supportImageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'tiff'];
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 { uri } = request;
const ObjectKey = decodeURIComponent(uri).substring(1);
const params = querystring.parse(request.querystring);
const { w, h, q, f } = params
/**
* ex) https://dilgv5hokpawv.cloudfront.net/dev/thumbnail.png?w=200&h=150&f=webp&q=90
* - ObjectKey: 'dev/thumbnail.png'
* - w: '200'
* - h: '150'
* - f: 'webp'
* - q: '90'
*/
// 크기 조절이 없는 경우 원본 반환.
if (!(w || h)) {
return callback(null, response);
}
const extension = uri.match(/\/?(.*)\.(.*)/)[2].toLowerCase();
const width = parseInt(w, 10) || null;
const height = parseInt(h, 10) || null;
const quality = parseInt(q, 10) || 100; // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
let format = (f || extension).toLowerCase();
let s3Object;
let resizedImage;
// 포맷 변환이 없는 GIF 포맷 요청은 원본 반환.
if (extension === 'gif' && !f) {
return callback(null, response);
}
// Init format.
format = format === 'jpg' ? 'jpeg' : format;
if (!supportImageTypes.some(type => type === extension )) {
responseHandler(
403,
'Forbidden',
'Unsupported image type', [{
key: 'Content-Type',
value: 'text/plain'
}],
);
return callback(null, response);
}
// Verify For AWS CloudWatch.
console.log(`parmas: ${JSON.stringify(params)}`); // Cannot convert object to primitive value.\
console.log('S3 Object key:', ObjectKey)
try {
s3Object = await S3.getObject({
Bucket: BUCKET,
Key: ObjectKey
}).promise();
console.log('S3 Object:', s3Object);
}
catch (error) {
responseHandler(
404,
'Not Found',
'The image does not exist.', [{ key: 'Content-Type', value: 'text/plain' }],
);
return callback(null, response);
}
try {
resizedImage = await Sharp(s3Object.Body)
.resize(width, height)
.withMetadata()
.toFormat(format, {
quality
})
.toBuffer();
}
catch (error) {
responseHandler(
500,
'Internal Server Error',
'Fail to resize image.', [{
key: 'Content-Type',
value: 'text/plain'
}],
);
return callback(null, response);
}
// 응답 이미지 용량이 1MB 이상일 경우 원본 반환.
if (Buffer.byteLength(resizedImage, 'base64') >= 1048576) {
return callback(null, response);
}
responseHandler(
200,
'OK',
resizedImage.toString('base64'), [{
key: 'Content-Type',
value: `image/${format}`
}],
'base64'
);
/**
* @summary response 객체 수정을 위한 wrapping 함수
*/
function responseHandler(status, statusDescription, body, contentHeader, bodyEncoding) {
response.status = status;
response.statusDescription = statusDescription;
response.body = body;
response.headers['content-type'] = contentHeader;
if (bodyEncoding) {
response.bodyEncoding = bodyEncoding;
}
}
console.log('Success resizing image');
return callback(null, response);
};
코드는 아래 github 페이지를 참고하였습니다.
마지막으로 코드를 Lambda로 업로드합니다.
? Lambda@Edge
[AWS Lambda] – [생성했던 함수] – [작업] – [Lambda@Edge 배포]


https://도메인/image/tom.png?w=200&h=150&f=webp&q=90
가로 200px, 세로 150px, 품질 90% 이미지 출력을 요청하였지만 변하는게 없네요…
뭔가 잘 안된 것같습니다.
이후 다시 해보고 성공하는대로 해당 포스팅을 성공 후기를 추가하여 수정할 예정입니다.
Reference

위 사이트는 Lambda Tutorial 사이트 입니다.
괜찮아보여서 추가해두었습니다.
비록 오늘 실습은 실패하였지만 이후 다시 도전했을 때 성공하기를 바랍니다.
곧 성공한 포스팅으로 변경하여 돌아오겠습니다.
긴 글 읽어주셔서 감사합니다.
