AWS Lambda@Edge에서 실시간 이미지 리사이즈 & WebP 형식으로 변환

Marco
Marco
Jan 24, 2019 · 16 min read

안녕하세요, 당근마켓에서 백엔드 서버 개발 인턴으로 근무하고 있는 Marco입니다. 저는 이번에 당근마켓 서비스의 썸네일 생성 방식을 On-The-Fly 이미지 리사이징으로 새롭게 구현하였습니다. 이번 글을 통해 그 과정을 공유하려고 합니다.

당근마켓은 당신 근처의 마켓이라는 뜻으로 따뜻하고 건강한 지역 기반의 정보를 연결하는 서비스입니다. 이용자 수가 꾸준히 증가하고 있으며 작년 12월을 기준으로 월간 유니크 활동 유저 수(MAU)가 160만을 넘어섰습니다.

2016년 1월부터 2018년 12월까지 3년간 꾸준히 성장중인 MAU(월간 유니크 활동 유저 수) 그래프.

이러한 이용자 수의 증가와 더불어 당근마켓에는 많은 게시글이 올라오고 있습니다. 또한 게시글과 함께 하루에 약 50만 장의 이미지를 사용자들이 업로드하고 있고, 이미지에 대한 요청은 하루에 약 2억 건 발생하고 있습니다. 이 수치는 꾸준히 증가하고 있기 때문에 이미지 저장 및 처리 방식에 개선이 필요하게 되었습니다.


기존의 썸네일 이미지 생성 방법

기존 썸네일 생성 방식의 구성도.

기존의 당근마켓의 썸네일 이미지 생성 방법은 다음과 같습니다. 클라이언트가 이미지를 S3 저장소로 직접 업로드하면, S3에서s3:ObjectCreated:Put 이벤트를 개시합니다. AWS Lambda의 이벤트 트리거는 이벤트를 받아서, S3 Bucket에 당근마켓에서 사용하는 썸네일을 생성합니다. 예를 들어, origin/article 폴더에 이미지를 업로드하면, resize/s/article, resize/l/article 경로에 썸네일을 생성하고 이에 대한 주소를 DB에 저장합니다. 기존의 방식에 대한 자세한 내용은 AWS Lambda를 이용한 이미지 썸네일 생성 개발 후기에서 볼 수 있습니다.

기존 방식의 장단점


새로운 썸네일 생성 방식의 필요성

On-The-Fly 이미지 리사이징의 도입

1280x1280 크기의 이미지일 때, WebP의 크기가 32% 작습니다.

On-The-Fly 이미지 리사이징의 여러가지 방식


Lambda@Edge

Origin Response를 이벤트로 사용하면 캐싱하기 전에 응답을 조작할 수 있습니다.

Lambda@Edge를 이용한 이미지 리사이징의 간단한 예를 들어 보겠습니다. 클라이언트가 게시글 리스트에 필요한 썸네일 이미지를 요청합니다. 캐싱되어 있지 않다면 CloudFront는 S3로 이미지를 요청하고 S3는 해당하는 이미지로 응답합니다. 이 때, Origin Response를 조작할 수 있습니다. 요청받은 이미지가 S3에 존재한다면 원본 이미지를 조작하여 응답의 Body로 설정할 수 있습니다. 그리고 이를 CloudFront에 전달하면, 최종적으로 클라이언트에 썸네일을 제공할 수 있습니다.

Lambda@Edge의 제한사항

  1. Origin Response를 트리거로 하는 Lambda에는 Event 객체가 전달됩니다. 이 때 Body에 담긴 원본 이미지를 사용할 수 없습니다. 따라서 이미지의 사이즈를 조절하기 위해 S3로 다시 객체를 요청해야 합니다. 또한 Accept 헤더가 Event 객체에 담겨있지 않습니다. 따라서 Lambda 함수에서 WebP 지원 여부를 알 수 없습니다. 헤더를 캐싱하도록 CloudFront 설정을 하면 Accept 헤더를 사용할 수 있지만, 모든 Accept에 대해 개별적으로 캐싱하도록 동작하기 때문에 사용하기에 알맞지 않다고 판단했습니다. 서버에서 WebP 지원여부를 확인하여 쿼리스트링의 파라미터로 추가하도록 구현했습니다.
  2. 만약 응답의 Body를 조작한다면 그 크기는 1MB 이하여야 합니다. 따라서 이미지의 크기를 한 번 조절했을 때 1MB를 넘는 경우를 대처해야 합니다. 응답을 조작하지 않는다면 1MB 이상의 응답이 가능합니다.
  3. CloudFront는 쿼리 문자열 파라미터를 기반으로 컨텐츠를 캐싱합니다. 이 때, 파라미터의 순서, 대소문자 등에 따라 다르게 캐싱됩니다. 리사이즈할 정보를 쿼리 문자열에 담아 리사이징 하도록 구현한다면 파라미터의 순서가 경우에 따라 다르지 않도록 해야합니다.
  4. Node.js만 사용 가능합니다. 만약 Node.js에 익숙하지 않다면 구현에 어려움이 따를 수 있습니다. 하지만 AWS에서 제공하는 기본 예제가 충실한 편이어서 어렵지 않게 구현할 수 있습니다.
  5. us-east-1 리전에만 Lambda@Edge를 배포할 수 있습니다. 결과적으로 모든 리전에 함수가 복사되기 때문에 큰 문제가 되지는 않습니다.

On-The-Fly 이미지 리사이징 구현

Lambda@Edge를 사용한 썸네일 생성 방식 구성도. (실제로는 Lambda@Edge가 CloudFront 상에 존재합니다.)

위의 그림과 같이 On-The-Fly 이미지 리사이징이 동작합니다. 예를 들어, 클라이언트가 300x300 사이즈인 WebP 포맷의 썸네일을 요청하는 상황을 가정하겠습니다. 클라이언트에서 https://CDN_ID.cloudfront.net/sample.jpg?s=300x300&f=webp 와 같은 경로에 이미지를 요청합니다. 이 때, 캐싱되어 있다면 CloudFront는 304 Status Code 와 함께 이미지를 응답합니다. 캐싱되어 있지 않다면 S3 Bucket에 원본 이미지를 요청합니다. 그리고 S3의 응답 이벤트가 트리거가 되어 Lambda@Edge 함수를 동작시킵니다. S3에 원본 이미지가 존재한다면 응답 이벤트에는 200 Status Code 가 담겨 있습니다. 함수에서, 주어진 파라미터에 따라 이미지가 리사이징 되고 그 결과가 CloudFront에 캐싱됩니다. 이제 클라이언트는 캐싱되어 있는 썸네일을 곧바로 CDN에서 제공받습니다.

AWS CDN Blog의 예제는 Node 6.10 기반이기도 하고 실제 서비스에서 곧바로 사용하기에는 부족한 면이 있어 이번에 구현한 코드를 첨부합니다. Node.js 8.10에 맞추어 작성되었습니다. 일부 참고하시고, 본인의 서비스에 맞게 수정하여 사용하시면 됩니다. 코드에 대해 지적할 부분이나 개선 사항이 있으시다면, 부디 gist에 댓글을 달아주시거나, 이 글에 댓글을 달아주시면 감사하겠습니다. Node.js를 처음 다루어, 부족한 부분이 많습니다:)

Tips for beginners

  • AWS CDN Blog의 예제에서는 S3 Bucket의 정책을 PublicRead로 선언하지만, 실제 서비스에서는 PublicRead로 권한을 설정하지 않는 것이 좋습니다. PublicRead의 경우 익명의 누군가가 S3 Bucket에 접근할 수 있기 때문에 보안에 취약합니다. 당근마켓에서는 정해진 CloudFornt에 Origin Access Identity를 부여하여 사용합니다. 이 때, S3의 버킷 정책에 GetObjectListBucket 액션에 대한 권한을 추가해야 합니다. 예시는 아래와 같습니다.
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/YOUR_IDENTITY"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET/*"
},
{
"Sid": "2",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/YOUR_IDENTITY"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::YOUR_BUCKET"
},
{
"Sid": "3",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/YOUR_EDGE_ROLE"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET/*"
}
]
}
  • 함수는 버지니아 북부 리전(us-east-1)에 배포되지만, 서울에 있는 클라이언트에게는 서울 리전(ap-northeast-2)에 복제된 함수가 실행됩니다. 이 함수는 서울 리전의 Lambda 함수 목록에서 복제된 함수 표시 옵션을 켜면 볼 수 있습니다. CloudWatch 통계 또한 각 리전에 복사된 함수 별로 제공됩니다.
  • Lambda 함수의 메모리와 제한 시간은 본인의 서비스에 맞게 설정합니다. 이 곳에서 메모리 설정에 관한 팁을 얻을 수 있습니다.
  • AWS Lambda 함수 대시보드의 테스트를 통해 브라우저 상에서 함수를 테스트를 할 수 있습니다. 이 때, 아래와 같이 테스트 이벤트를 구성할 수 있습니다. 하지만 테스트 이벤트와 실제 발생하는 이벤트는 다를 수 있기 때문에 AWS 설명서에서 실제 이벤트가 어떻게 구성되는지 확인해야 합니다.
{
"Records": [
{
"cf": {
"config": {
"distributionDomainName": "YOUR_CDN.cloudfront.net",
"distributionId": "YOUR_CDN_ID",
"eventType": "origin-response",
"requestId": "xGN4KWpVEmB9Dp5ctcVFQC0E-nrcOcEKS3HyAez--19dV7TEXABCDE=="
},
"request": {
"clientIp": "1234:0db8:85a3:0:0:8a2e:5678:9012",
"method": "GET",
"uri": "/assets/images/sample.jpg",
"querystring": "s=256x256&f=webp",
"headers": {
"host": [
{
"key": "Host",
"value": "YOUR_CDN.cloudfront.net"
}
],
"user-agent": [
{
"key": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
}
]
}
},
"response": {
"status": "200",
"statusDescription": "OK",
"headers": {
"server": [
{
"key": "Server",
"value": "MyCustomOrigin"
}
],
"set-cookie": [
{
"key": "Set-Cookie",
"value": "theme=light"
},
{
"key": "Set-Cookie",
"value": "sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT"
}
]
}
}
}
}
]
}

결과

  • cache miss일 때, 응답 시간이 평균 105ms에서 501ms로 늘어났습니다. miss는 첫 번째 클라이언트에게만 발생하기 때문에 크게 사용자 경험에 지장을 주지 않는다고 판단했습니다.
  • 초기 리사이징할 때를 제외하고는 cache hit인 경우입니다. WebP 지원으로 인하여 이미지의 용량이 20% 줄었습니다. 따라서 사용자는 더 빠르게 이미지를 다운로드 받을 수 있고, 클라이언트의 모바일 데이터 사용량과, 저장 공간 사용량이 줄어드는 장점이 있습니다.
  • 512MB의 메모리를 할당받은 Lambda 함수가 평균 0.492초 동안 동작하고 일 평균 6.12M회 호출됩니다. 계산해보면 그 비용은 한 달에 약 2,400달러 입니다. 이는 최적화 과정을 통해 다소 줄일 수 있을 것으로 예상됩니다.
  • CloudFront의 트래픽이 약 20% 정도 감소하였습니다. 그래프에서 트래픽의 감소를 눈으로 확인할 수 있습니다. 1월 8일부터 21일까지 일간 요청 횟수는 약 2.1억 건을 전후로 조금씩 증가하는 추세입니다. 하지만 트래픽의 양은 On-The-Fly 이미지 리사이징이 적용된 15일 이후로 유의미하게 감소하였습니다. 또한 S3에 저장되어 있던 썸네일을 정리하고, 신규 썸네일을 S3에 저장하지 않게 되었습니다. CloudFront 트래픽 감소와, S3 저장소 정리를 통해 한 달에 약 3,000달러의 비용이 절약됩니다.
CloudFront의 일간 요청 횟수 그래프
CloudFront의 일간 트래픽 그래프

당근마켓에서는 이번에 Lambda@Edge를 통한 이미지 리사이징을 구현하여 크게 사용자 경험에 지장을 주지 않고 인프라 비용을 절감하였습니다. 당근마켓은 통계를 사용하여 문제를 인식하고 새로운 기술의 도입으로 그 문제를 해결하고 있습니다. 글의 서문에 언급한 것처럼, 당근마켓은 MAU 160만을 넘어 빠르게 성장하고 있는 서비스입니다. 이에 따라 크고 작은 도전 과제들이 생겨나고 있습니다. 이러한 도전 과제를 같이 해결하고 서비스를 개선해나갈 개발자를 찾고있습니다. 함께 하고 싶으시다면 이 곳을 확인해주시길 바랍니다.


당근마켓 팀블로그

당근마켓은 동네 이웃 간의 연결을 도와 따뜻하고 활발한 교류가 있는 지역 사회를 꿈꾸고 있어요.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store