BLOG

[Lab To Scale] Image resizing as-a-Service 구현하기
작성일: 2019-12-09

SaaS 서비스를 위한 모바일 앱을 개발하기 위한 이미지를 활용 할 경우 디바이스에 맞추어 이미지 크기를 조정하는 것이 중요합니다. 이를 위해, 적절한 크기 (너비 x 높이)와 해상도 (dpi) 가 있어야 합니다 .

 

이미지 리사이징을 진행 해야 하는 이유는 다음과 같습니다.

  • 고해상도 디스플레이의 저해상도 이미지는 잘못된 UX를 만듭니다.
  • 저해상도 디스플레이의 고해상도 이미지는 대역폭 과 장치 및 서버 리소스를 모두 낭비 합니다 특히 프레임 워크를 사용하는 경우 인앱 에서 이미지를 채택하고 이미지를 자르거나 크기를 조정 하면 앱이 무거울 수 있습니다.
  • 웹 응용 프로그램에서도 이미지 크기를 즉시 조정할 수 있습니다. CDN을 통해보다 효율적으로 배포 할 수 있습니다.
  • 웹 사이트의 작동 방식은 서버에 저장된 원본 이미지의 사본을 보유하는 것입니다. 일부 컨텐츠를 요청하면 이미지는 URL을 통해 그 일부입니다.
  • 이미지의 URL 다음에 서버의 파일에 직접 액세스합니다. 이것은 웹 사이트에 큰 부담을 줄 것입니다. 요즘 널리 사용되는 첫 번째 단계는 CDN을 사용하는 것입니다.
  • CDN 은 불필요한 HTTP 헤더의 대부분 이 생략되어 내용이 더 가벼워지므로 더 빠릅니다 . 또한 컨텐츠는 네트워크의 다른 엔드 포인트로 복제되어 가까운 거리에서 클라이언트로 전달됩니다.

 

Associating Cloudfront and Lambda@Edge

다음과 같이 AWS 내 CloudFront (CDN) + Lamda@Edge(On The Fly Resizing) 을 진행 해 보겠습니다.

  • Cloudfront Viewer request
  • CloudFront Origin request
  • Cloudfront Origin response
  • Cloudfront Viewer response
    1. 쿼리 매개 변수로 크기를 지정하여 이미지를 그 자리에서 리사이즈 한다
    2. 뷰어에 따라 최적화 된 이미지 포맷을 제공한다
    3. 이미지 크기의 화이트리스트를 정의하여 생성 및 배포를 허용한다
    4. 요청한 이미지 크기, 포맷이 존재하지 않는 경우에만 크기 조정 작업을 수행한다

  1. CloudFront 배포에 연결된 2개의 Lambda@Edge 트리거. 뷰어 요청 및 원서 응답.
  2. 오리진으로 사용하는 Amazon S3.

 

예상 시나리오

  1. 요청된 이미지는 URI는 Lambda@Edge의 뷰어 요청 함수에서 조작된 적절한 크기와 포맷이다. 이것은 요구가 캐시에 히트하기 전에 발생한다.
  2. CloudFront는 오리진에서 이미지 개체를 읽는다.
  3. 필요한 이미지가 이미 S3 버킷에 존재하는 경우, 또는 5단계에서 생성된 저장되어있는 경우 CloudFront는 뷰어 이미지 오브젝트를 돌려준다. 이때 이미지가 캐시된다.
  4. 캐시된 이미지 객체가 뷰어에 반환된다.
  5. 리사이즈 작업은 오리진에 이미지가 존재하지 않는 경우에만 실행된다. S3 버킷(오리진) 네트워크 호출을 원래 이미지를 가져 크기를 조정한다. 생성된 이미지는 CloudFront에 전송하기 전에 S3 버킷에 유지된다.

 

Lambda@Edge 함수

  • Viewer-Request 함수 :: Request URI 조작
‘use strict’;
const querystring = require(‘querystring’);
// defines the allowed dimensions, default dimensions and how much variance from allowed
// dimension is allowed.
const variables = {
allowedDimension : [ {w:100,h:100}, {w:200,h:200}, {w:300,h:300},{w:400,h:400} ],
defaultDimension : {w:200,h:200},
variance: 20,
webpExtension: ‘webp’
};exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// parse the querystrings key-value pairs. In our case it would be d=100×100
const params = querystring.parse(request.querystring);
// fetch the uri of original image
let fwdUri = request.uri;
// if there is no dimension attribute, just pass the request
if(!params.d){
callback(null, request);
return;
}
// read the dimension parameter value = width x height and split it by ‘x’
const dimensionMatch = params.d.split(“x”);
// set the width and height parameters
let width = dimensionMatch[1];
let height = dimensionMatch[2];
// parse the prefix, image name and extension from the uri.
// In our case /images/image.jpg
const match = fwdUri.match(/(.*)\/(.*)\.(.*)/);
let prefix = match[1];
let imageName = match[2];
let extension = match[3];
// define variable to be set to true if requested dimension is allowed.
let matchFound = false;
// calculate the acceptable variance. If image dimension is 105 and is within acceptable
// range, then in our case, the dimension would be corrected to 100.
let variancePercent = (variables.variance/100);
for (let dimension of variables.allowedDimension) {
let minWidth = dimension.w – (dimension.w * variancePercent);
let maxWidth = dimension.w + (dimension.w * variancePercent);
if(width >= minWidth && width <= maxWidth){
width = dimension.w;
height = dimension.h;
matchFound = true;
break;
}
}
// if no match is found from allowed dimension with variance then set to default
//dimensions.
if(!matchFound){
width = variables.defaultDimension.w;
height = variables.defaultDimension.h;
}
// read the accept header to determine if webP is supported.
let accept = headers[‘accept’]?headers[‘accept’][0].value:””;
let url = [];
// build the new uri to be forwarded upstream
url.push(prefix);
url.push(width+”x”+height);// check support for webp
if (accept.includes(variables.webpExtension)) {
url.push(variables.webpExtension);
}
else{
url.push(extension);
}
url.push(imageName+”.”+extension);
fwdUri = url.join(“/”);
// final modified url is of format /images/200×200/webp/image.jpg
request.uri = fwdUri;
callback(null, request);
};

 

위 코드에서는 뷰어의 ‘Accept’헤더에 기초하여 다른 이미지 포맷을 제공하기 위해 입력된 URI를 조작합니다.

입력된 화이트리스트와 비교하여 가장 가까운, 조작 가능한 크기로 변환합니다.

따라서 비 표준 크기(미리 입력 해 두지 않은) 요청이 온 경우에 그와 가장 비슷한 값을 제공하며, 캐싱되는 이미지를 효율적으로 적중 시켜주는 매커니즘입니다.

또한 불필요한 이미지 크기 생성을 방지할 수 있습니다

 

  • Origin-Response 함수 : : 이미지 개체가 있는지 확인하고 필요에 따라 이미지 크기 조정 수행
‘use strict’;
const http = require(‘http’);
const https = require(‘https’);
const querystring = require(‘querystring’);
const AWS = require(‘aws-sdk’);
const S3 = new AWS.S3({
signatureVersion: ‘v4’,
});
const Sharp = require(‘sharp’);
// set the S3 endpoints
const BUCKET = ‘image-resize-${AWS::AccountId}-us-east-1’;
exports.handler = (event, context, callback) => {
let response = event.Records[0].cf.response;
console.log(“Response status code :%s”, response.status);
//check if image is not present
if (response.status == 404) {
let request = event.Records[0].cf.request;
let params = querystring.parse(request.querystring);
// if there is no dimension attribute, just pass the response
if (!params.d) {
callback(null, response);
return;
}
// read the dimension parameter value = width x height and split it by ‘x’
let dimensionMatch = params.d.split(“x”);
// read the required path. Ex: uri /images/100×100/webp/image.jpg
let path = request.uri;
// read the S3 key from the path variable.
// Ex: path variable /images/100×100/webp/image.jpg
let key = path.substring(1);
// parse the prefix, width, height and image name
// Ex: key=images/200×200/webp/image.jpg
let prefix, originalKey, match, width, height, requiredFormat,imageName;
let startIndex;
try {
match = key.match(/(.*)\/(\d+)x(\d+)\/(.*)\/(.*)/);
prefix = match[1];
width = parseInt(match[2], 10);
height = parseInt(match[3], 10);
// correction for jpg required for ‘Sharp’
requiredFormat = match[4] == “jpg” ? “jpeg” : match[4];
imageName = match[5];
originalKey = prefix + “/” + imageName;
}
catch (err) {
// no prefix exist for image..
console.log(“no prefix present..”);
match = key.match(/(\d+)x(\d+)\/(.*)\/(.*)/);
width = parseInt(match[1], 10);
height = parseInt(match[2], 10);
// correction for jpg required for ‘Sharp’
requiredFormat = match[3] == “jpg” ? “jpeg” : match[3];
imageName = match[4];
originalKey = imageName;
}
// get the source image file
S3.getObject({ Bucket: BUCKET, Key: originalKey }).promise()
// perform the resize operation
.then(data => Sharp(data.Body)
.resize(width, height)
.toFormat(requiredFormat)
.toBuffer()
)
.then(buffer => {
// save the resized object to S3 bucket with appropriate object key.
S3.putObject({
Body: buffer,
Bucket: BUCKET,
ContentType: ‘image/’ + requiredFormat,
CacheControl: ‘max-age=31536000’,
Key: key,
StorageClass: ‘STANDARD’
}).promise()
// even if there is exception in saving the object we send back the generated
// image back to viewer below
.catch(() => { console.log(“Exception while writing resized image to bucket”)});
// generate a binary response with resized image
response.status = 200;
response.body = buffer.toString(‘base64’);
response.bodyEncoding = ‘base64’;
response.headers[‘content-type’] = [{ key: ‘Content-Type’, value: ‘image/’ + requiredFormat }];
callback(null, response);
})
.catch( err => {
console.log(“Exception while reading source image :%j”,err);
});
} // end of if block checking response statusCode
else {
// allow the response to pass through
callback(null, response);
}
};

 

위 함수는 CloudFront가 오리진에서 응답을 받은 후 캐시에 저장하기 전에 실행 됩니다.

  1. 오리진 응답 상태 코드를 확인하고 Amazon S3 버킷에 이미지 개체가 있는지 확인
  2. 이미지가 존재하는 경우는 그대로 CloudFront 응답 사이클 계속
  3. 이미지가 S3 버킷에 존재하지 않는 경우, 원본 이미지를 얻고 크기 조정을 수행 버퍼에 입력하여 리사이즈 이미지에 올바른 접두사와 메타 데이터를 부여하여 S3 버킷에 유지
  4. 이미지의 크기가 바뀔 경우 메모리 내의 리사이즈 이미지를 오리진으로 바이너리 응답을 생성하여, 적절한 상태코드를 헤더에 반환

 

Note : S3 버킷과 Lambda 함수를 실행하는 Edge 위치가 다른 지역의 경우 Amazon S3에서 Lambda 함수에 지역 데이터 전송 (out) 요금이 발생하므로 주의하시기 바랍니다. 이러한 비용은 이미지 생성 마다 1 발생합니다.

 

컨텐츠 관리 시스템으로부터 적절한 URL을 호출하여 필요한 이미지 포맷 및 크기를 미리 생성할 수 있습니다.

또한 워터마크를 부여하도록 코드를 수정하시면 확장도 가능합니다

 

기능 테스트

  1. 작성한 S3 버킷에 고해상도의 이미지 파일 (image.jpg)를 ‘images’디렉토리에 업로드합니다.
  2. 좋아하는 브라우저에서 다음 URL을 엽니다.

https://{cloudfront-domain}/images/image.jpg?d=100×100

 

 

** 메가존 클라우드의 ‘Lab to Scale’ 프로그램은 AWS SaaS Factory 베스트 프랙티스 기반의  확장성 높은 SaaS 서비스 설계/구축/운영 서비스를 제공 합니다. 메가존 클라우드의 클라우드 전문가들과 함께 신규 SaaS 서비스 설계부터 국내/외 비즈니스 협업까지 SaaS 서비스의 성공적인 사업화를 지원 합니다