여기서는 CloudFront를 사용한다고 전제되어 있으며 lambda@edge를 이용해 처리하는 방법이다.
(CloudFront 란 AWS에서 제공하는 CDN서비스)
단계는 다음과 같다.
1) Cloud9 생성 및 코드 작성
2) IAM 등록
3) 만든 역할을 Lambda에 등록하기
4) Lambda@Edge 배포 & 로그 확인
그럼 시작.
Lambda@edge를 배포하려면 '버지니아 북부' 리전의 Lambda만 가능하다. 그래서 리전을 먼저 이동한다.
AWS Console => Lambda 로 이동한 후 '함수 생성' 버튼을 클릭한다.
이미지 리사이징을 위한 lambda를 만들때 nodejs 버전을 10로 한다.(14는 아직 미지원)
Lambda 파일은 Cloud9을 이용해 만들었다.
1) Cloud9 생성 및 코드 작성
Cloud9 이 아닌 로컬에서 개발한다면 아래 설정은 보지 않아도 된다.
Cloud9 환경을 설정해보자. 이것 역시 서울리전은 불가하니 버지니아 북부 리전으로 만든다.
AWS Console => Cloud9 으로 이동한다.
Create enviroment 를 클릭한다.
이름을 적당히 입력하고 넘어간다.
가장 기본옵션(+ 싼거)로 인스턴스를 띄운다.
다 사용하고 나서 인스턴스는 반환을 잊지 않도록 한다. 안그러면 돈나간다.
(절전모드가 있긴 한데 굳이 유지할 이유도 없기 때문)
Create enviroment 를 클릭해 생성을 마무리한다.
생성되면 다음 화면을 볼 수 있다. 아래가 터미널 영역이고, 왼쪽이 파일 디렉토리 구조다.
Cloud9의 좋은점은 S3나 Lambda에 설정되어 있는 코드를 호출할 수 있다는 점이다. Import와 upload를 마우스 클릭으로 쉽게 할 수 있다. 여기서는 Lambda에 등록되있는 코드를 Import해볼 예정이다.
왼쪽에 Aws 로고를 클릭하면 북부리전에 등록되어 있는 서비스들이 뜬다. 여기서 Lambda를 클릭하면 생성한 Lambda 가 보이는데 마우스 오른쪽 버튼을 눌러 Import를 누른다.
import할 위치를 묻는다. testCloud9 를 클릭한다.
다시 왼쪽의 폴더처럼 되어있는 아이콘을 클릭하면 import 된 것을 확인할 수 있다.
리사이징 기능을 수행할 node.js 파일을 만든다
아래 터미널에서 해당 폴더로 접근한다.
cd ResizeImage-v2
sharp 패키지를 설치한다.(이미지 처리 패키지)
npm i sharp
그리고 다시 왼쪽 화면에서 해당폴더에 마우스 오른쪽 버튼을 눌러 New File을 클릭 => index.js 파일을 생성한다.
index.js 파일에 다음의 내용을 추가한다
(아래 코드의 출처는 아래 블로그)
https://devhaks.github.io/2019/08/25/aws-lambda-image-resizing/
버킷에 들은 파일의 확장자가 반드시 다음중 하나여야 제대로 작동한다
- ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'tiff']
만약 확장자 없이 업로드 하는거라면 이 다음 스크립트를 사용하는게 좋다
'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({
region: 'ap-northeast-2' // 버킷을 생성한 리전 입력(여기선 서울)
});
const BUCKET = '[버킷 이름]' // 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;
// console.log("request: ", request)
// console.log("response: ", response)
// Parameters are w, h, f, q and indicate width, height, format and quality.
const { uri } = request;
// console.log("uri: ", uri)
const ObjectKey = decodeURIComponent(uri).substring(1);
const params = querystring.parse(request.querystring);
const { w, h, q, f } = params
// console.log("whqf: ", w,h,q,f)
/**
* 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() // 이미지 크기조절시 임의로 이미지 회전하는 상황 방지
.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);
};
이미지 확장자가 없는 버킷을 사용하는 경우(비추천)
- 가급적이면 버킷 내 파일들에 확장자를 포함하는게 나중을 위해 좋을 수 있다.
'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: 'ap-northeast-2' //버킷 Region
});
const BUCKET = 'cpi-service';
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 filepath = uri.substring(1)
// 만약 이미지 파일로 확장자가 되어있다면 아래 사용
// 아래 s3 에서 getObject 하는곳에도 수정해야 함
//const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);
//console.log("imageName: ", imageName)
//console.log("extension: ", extension)
//이미지 파일이 아니라면 썸네일 굽지 않음
// if (extension !=='jpg'&& extension !=='jpeg' && extension !=='webp' && extension !== 'bmp' && extension != 'png') {
// console.log('not image file requested!');
// return callback(null, response);
// }
// 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 : 'webp'; //따로 포맷형태를 주지않으면 webp로 변경
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)
Key: filepath
}).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);
};
그럼 모두 저장한 다음 다시 왼쪽의 창에서 Aws => lambda => [Lambda이름] 마우스 우클릭 => Upload Lambda 를 클릭한다.
다음 창에서 Zip과 Directory를 묻는데 내 경우 Directory를 클릭했다.
지금까지 작업한 폴더를 클릭, 아래 오픈을 클릭한다.
왼쪽 창에서 아래를 선택(Yes AWS ... )
코드 즉시 배포를 묻는다. Yes를 누른다.
AWS consle => Lambda에 들어가보면 함수코드가 다음과 같이 변경되어 있다.
함수 내보내기를 통해 다운받은 후 받은 파일과 upload 한 코드가 일치하는지 확인한다.
이제 Cloud9 콘솔창을 닫는다. (문제가 없으면 AWS Console => Cloud9 에 들어가서 testCloud9 을 삭제한다)
2) IAM 으로 역할 추가
lambda@edge 배포를 클릭하면 팝업이 뜬다. 설정 후 배포버튼을 누르면 edgelambda.amazonaws.com 에 수임해야 한다고 뜬다.
IAM을 통해 권한을 추가해야 한다.
IAM 이동 => 역할 => 역할 만들기를 클릭한다
Lambda를 클릭한 뒤 다음을 클릭한다.
정책 설정을 클릭한다
다음 화면에서 json 탭을 클릭해 다음을 입력한 후, 정책검토를 누른다
아래를 복사한 다음 붙여넣어 설정을 완료한다
{
"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": "*"
}
]
}
위 설정에 따른 항목들이 다음처럼 보인다.
이름을 resize_policy로 입력하고 생성완료 한다
그럼 이전 연결할 정책을 선택하는 화면이 나오는데 여기서 지정한 이름을 검색, 선택한 후 다음을 누른다
완료를 눌러 역할을 만든다.
왼쪽 역할 탭을 클릭해 입력한 resize로 검색하면 만든 역할이 보인다. 클릭한다.
두번째 탭인 신뢰관계를 누른 후, 신뢰 관계 편집을 클릭한다.
다음을 입력한 후 업데이트 한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com",
"edgelambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
3) 만든 역할을 Lambda에 등록하기
이제 람다로 다시 돌아가서 권한을 변경한다.
Lambda의 권한 탭으로 이동한 후 실행 역할에 편집을 클릭한다
기존 역할을 새로 만든 ResizeImage로 변경한다
4) Lambda@Edge 배포 & 로그 확인
'작업 > Lambda@Edge' 배포를 클릭한다.
다음처럼 설정한다. 설정 중 CloudFront 이벤트에서 '오리진 응답' 으로 설정해야 한다.(기본값은 오리진 요청이니 반드시 수정해주자)
모니터링 탭으로 이동하여 CloudWatch에서 로그보기 버튼을 눌러 로그창으로 이동한다.
(중간에 사정이 있어 함수 이름을 ResizeImage => ResizeImage-v2 로 변경하였습니다)
CloudWatch로 접속하면 다음과 같은 에러가 발생한다. 이유는 리전때문이다.
위의 lambda가 저장되어 있는 리전은 '버지니아 북부' 인데, lambda 자체를 만드는건 서울리전에도 가능하지만 배포할 때 Lambda@edge 로 배포하는 것은 버지니아 북부 리전밖에 되지 않는다.(만들때 리전을 버지니아 북부로 이동한다음 만들어야 함)
서울 리전으로 설정하고 Lambda를 만들면 다음과 같이 Lambda@edge 버튼이 없다.
(참고로 CloudFront 도 글로벌 리전이라 표기되어 있다)
CloudWatch로 바로이동하면 보이지 않지만 왼쪽메뉴에서 '로그 => 로그 그룹'을 클릭하면 확인할 수 있다.
그럼 cloudfront의 도메인을 이용해 호출해보자
파라미터에 w와 h를 각각 입력한다
w는 width, h는 height의 약자로 썼으며 CloudFront에서 해당 파라미터를 화이트리스트로 등록하여 받았다.
아래처럼 파라미터를 구성한 뒤 브라우저에서 실행해보자.
https://[cloudfrontId].cloudfront.net/[이미지경로]?w=150&h=100
아래는 리사이징 성공 결과 스샷
성공한 경우 CloudWatch에 아래와 같이 로그가 남는다.
만약 파라미터를 입력해도 적용되지 않는다면 다음 포스팅을 참조
끝.
참조:
https://devhaks.github.io/2019/08/25/aws-lambda-image-resizing/
https://heropy.blog/2019/07/21/resizing-images-cloudfrount-lambda/
'공부 > 프로그래밍' 카테고리의 다른 글
[spring security, oauth2] invalid_token 에러를 핸들링(ExceptionHandler)하여 output form 설정하기 (0) | 2021.02.17 |
---|---|
[spring security] token 요청 시 에러 Exception 핸들링하기(/oauth/token 에러, ControllerAdvice 적용안됨) (0) | 2021.02.15 |
[springboot] 엑셀 다운로드 시 이름지정 및 한글 깨짐 방지 (1) | 2021.02.10 |
[aws] lambda@edge 설정 중 파라미터(query string)이 넘어오지 않는 경우(이미지 리사이징) (0) | 2021.02.08 |
[springboot] swagger 설정 & 사용법 (1) | 2021.02.04 |
댓글