[AWS] Lambda Edge: 이미지 리사이징하기

이전 포스트
https://blog.naver.com/sssang97/222504226420
https://blog.naver.com/sssang97?Redirect=Log&logNo=222567479583&from=postView

네이버쇼핑에 상품과 이미지를 업로드하는데, 이미지 형태에 문제가 있어서 리사이징 시스템을 구축할 일이 생겼다.

예를들어 다음 이미지를 올린다 치면

이런식으로 잘려서 올라갔다.

세로로 기니까 세로가 좀 잘려버린 것이다.
그래서 여백에 뭘 넣어서라도 정사각형을 만들 필요가 있었다.

그래서 원래 ".../foo.png"인걸 ".../resize/naver/foo.png"로 접근하면 리사이징된 이미지를 조회하도록 구현하려 한다.

먼저 세팅해야할건 cloudfront다.
cloudfront 캐싱을 붙이지 않았다면 먼저 붙여야한다.




Cloudfront 동작 생성

나는 모든 이미지를 다 리사이징해버리는게 아니라, 특정 경로 "/resize"로 요청이 들어올 때만 리사이징해서 던져주고 싶다.

그러려면 "동작"으로 경로를 분기해줘야 한다.

다음과 같이 resize/로 시작하는 모든 경로가 대응되도록 설정해서 만든다.

그럼 이런식으로 만들어질 것이다.

우선순위는 숫자가 낮을수록 높다.

저기에는 나중에 람다를 엮을 것이다.
저 경로로 들어올 때만 람다가 호출돼서 이미지를 가공하도록 말이다.




람다 생성

먼저 버지니아 북부 리전으로 이동한다.

이유는 다른게 아니라, 람다엣지가 여기서만 지원되기 때문이다.

템플릿 골라서 만든다.




Lambda Edge 권한 설정

자동으로 다 해줬으면 좋겠지만, 해주진 않는다.

역할 들어가서


신뢰관계에 람다엣지를 추가해준다.


{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "lambda.amazonaws.com",
                    "edgelambda.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

이렇게 말이다.




람다 트리거 연동

그럼 이제 저 람다함수를 트리거로 cloudfront에 엮어주면 된다.
람다엣지가 따로 있는게 아니라 이렇게 엮어주는 것 자체를 람다엣지라고 부르는 것이다.

클라우드프론트 골라주고 배포를 누른다.

그럼 이렇게 뜰텐데.

연결할 cloudfront와 아까 만든 동작 경로를 선택한다.
그리고 이벤트는 "오리진 응답"으로 설정한다.
이건 s3에서 cloudfront 캐시로 건너가기 직전에 호출되는 이벤트다.

나는 여기서 다음과 같은 프로세스를 구현할 예정이다.

  1. /resize 경로로 해당되는 s3 객체가 이미 있다면 그대로 넘겨준다.
  2. /resize 경로로 해당되는 s3 객체가 없다면 /resize를 뗀 원본 객체를 찾는다.
  3. 원본객체가 있다면 그걸 가공해서 /resize 경로로 저장하고, 가공한 이미지데이터도 바로 캐시로 보내준다.

배포는 몇분정도 걸린다.




트리거 확인

잘 올라갔으면, 제대로 연동이 되었는지 한번 로그를 찍어보자

지정한 경로 형태로 요청을 날려봤다.

참고로, 로그는 버지니아에 쌓이는게 아니라 실제로 사용된 람다 리전을 따라간다.
만약 서울에서 요청을 날렸다면 당연히 가장 가까운 서울 리전에서 실행될테니, 서울 리전의 로그를 보면 된다.

그래서 이런식으로 로그가 잘 쌓였다면, 트리거 연동이 잘 되었다는 것이겠다.

그리고 당연한거지만, 동일한 url로 다시 테스트를 하고 싶다면 해당 경로로 cloudfront 무효화를 날려줘야 한다.
이미 캐시되어있다면 다시 실행되지 않는다.




sharp 모듈 붙이기

람다엣지를 붙일때 가장 짜증나는 것 중 하나가

레이어를 못쓴다는 것이다.

그래서 람다 환경에 직접 들어가서 깔아줄 필요가 있는데, 여기서는 cloud9라는 녀석을 쓰겠다.

그냥 개발환경 대충 제공하는거라 보면 된다.
ec2 머신 위에 에디터 올려주는거라고 보면 된다.


최소옵션으로 설정해주고


생성한다.


그럼 왼쪽 탭에 저렇게 있을텐데


다운받는다.


그럼 이런식으로 가져와질 것이다.


밑에 터미널로 저 폴더에 들어가서 sharp를 깔아주고


다시 탭에서 업로드 람다를 클릭


디렉터리 선택


골라서 오픈, 이후엔 OK만 누르면 된다.

그럼 람다에 업로드가 될 것이다.

근데 이러면 올라가는건 잘 올라가는데,

sharp가 너무 커서 앞으로 저런 식으로만 업로드가 가능해진다.




이미지 리사이징 구현

밑준비는 거의 끝났다.
그럼 이제 본격적으로 이미지 리사이징 로직을 구현해보자

나는 /resize/naver로 들어올 경우에만 이미지를 140x140픽셀로 압축, 정사각형으로 만들고 남는 공간은 흰색으로 채우도록 했다.

const http = require('http');
const https = require('https');
const querystring = require('querystring');
const sharp = require('sharp');

const aws = require('aws-sdk');
const S3 = new aws.S3();

// 이미지를 정사각형으로 만들고, 남는 공간을 흰색으로 채움.
async function imageToSquare(image) {
	const metadata = await image.metadata();

	// 가로와 세로 중에 긴 사이즈를 구함 (정사각형으로 맞추기 위함)
	const bigLength =
		metadata.width > metadata.height
			? metadata.width
			: metadata.height;

	const resized = image.resize(bigLength, bigLength, {
		fit: 'contain', // 남는 공간을 background color로 채움
		background: { r: 255, g: 255, b: 255 },
	});

	return resized;
}

// 네이버에서 사용하는 이미지 사이즈로 축소
async function toNaverListItemSize(image) {
	const resized = image.resize({ width: 140, height: 140 });

	return resized;
}

<br>

exports.handler = async (event, context, callback) => {
	const response = event.Records[0].cf.response;

	try {
		console.log('Response status code :%s', response.status);

		// 이미 리사이징된 파일이 존재하지 않을 경우
		if (response.status == 404 || response.status == 403) {
			const request = event.Records[0].cf.request;

			const requestUri = request.uri.replace('/', ''); //첫번째 슬래시 제거
			const uri = requestUri.replace('resize/', '');

			console.log('requestUri: ', requestUri);

			const resizeType = uri.split('/')[0];
			const originUri = uri.replace(resizeType+'/', '');

			console.log('resizeType: ', resizeType);
			console.log('originUri: ', originUri);

			switch(resizeType) {
				case 'naver': {
					const data = await S3.getObject({
						Bucket: '...',
						Key: originUri,
					}).promise();

					let image = sharp(data.Body);
					const metadata = await image.metadata();
					const format = metadata.format;

					const mimeType ='image/' + format;

					image = await imageToSquare(image)
					image = await toNaverListItemSize(image);

					const buffer = await image.toBuffer();

					await S3.upload({
			            Body: buffer,
			            Bucket: '...',
			            Key: requestUri,
			            ContentType: mimeType,
			            ACL: 'public-read',
			        }).promise();

					// 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: mimeType }]
					callback(null, response);

					break;
				} 
				default: {
					callback(null, response);
					break;
				}
			}
		} else {
			callback(null, response);
		}
	} 
	catch(error) {
		console.log('!! ERROR');
		console.error(error);
		callback(null, response);
	}
};



재배포

람다엣지 수정사항을 재배포하려면 그냥 원본 람다함수에서 트리거를 다시 추가하면 된다.

그럼 새 버전으로 람다엣지를 배포하면서 기존의 트리거는 삭제된다.

잘 구성됐다면, 이제 다음과 같이 압축해서 보내줄 것이다.

아주 작고 아담해졌다.




기타

이미지처리를 하려면 람다 기본옵션으로는 조금 버겁다. 실행시간과 메모리를 약간 늘려야 한다.



참조
https://devhaks.github.io/2019/08/25/aws-lambda-image-resizing/
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html
https://aws.amazon.com/ko/blogs/networking-and-content-delivery/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/