택시짱의 개발 노트

Cloudfront Lambda@Edge Image Resize - 이미지 사이즈 조절하기 본문

aws

Cloudfront Lambda@Edge Image Resize - 이미지 사이즈 조절하기

택시짱 2023. 12. 21. 11:07

개요

  • 고정된 사이지의 이미지를 여러 플랫폼에서 사용하게 되는데 플랫폼 마다 필요로 하는 이미지의 사이즈가 다를 수 있음
  • Cloudfront의 Lambda@Edge를 이용하여 최종적으로 클라이언트가 원하는 이미지의 사이즈를 능동적으로 호출할 수 있게 하려 함

Lambda@Edge 란

Lambda@Edge는 AWS Lambda의 확장으로 Amazon CloudFront 엣지 로케이션에 Python 및 Node.js 함수를 배포할 수 있습니다. Lambda@Edge의 일반적인 사용 사례는 함수를 사용하여 CloudFront 배포가 최종 사용자에게 제공하는 콘텐츠를 사용자 지정하는 것입니다. 오리진 서버가 아니라 최종 사용자에게 가까운 위치에서 이들 함수를 간접적으로 호출하므로 지연 시간이 크게 단축되고 사용자 경험이 상당히 개선됩니다.

CloudFront 배포를 Lambda@Edge 함수와 연결하면 CloudFront가 CloudFront 엣지 로케이션에서 요청 및 응답을 가로챕니다. 그러면 CloudFront는 이벤트를 전송하여 Lambda 함수를 간접적으로 호출합니다. 다음 이벤트가 발생할 때 CloudFront가 Lambda 함수를 간접적으로 호출하도록 할 수 있습니다.

  • Viewer-Request : CloudFront가 최종 사용자의 요청을 수신할 때(최종 사용자 요청)
  • Origin-Request : CloudFront가 오리진에 요청을 전달하기 전(오리진 요청)
  • Origin-Response : CloudFront가 오리진의 응답을 수신할 때(오리진 응답)
  • Viewer-Response : CloudFront가 최종 사용자에게 응답을 반환하기 전(최종 사용자 응답)

 

https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/lambda-edge.html

 

AWS 설정

  • Lambda@Edge를 이용하여 resize image를 하기 위해서는 여러 설정 및 추가가 필요
  • 현재 사용중인 역할 ( resize-image )

IAM 추가

  • IAM의 역할 에서 역활 만들기 ( 신뢰할 수 있는 엔터티 선택 )

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

 

2. 생성한 역할에 정책 추가

  • 인라인 정책 생성

  • 정책 편집기 작성
{
	"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": "*"
		}
	]
}

 

  • 정책 생성

 

Serverless Framework를 이용한 Lambda 배포

초기 설정

$ npm install -g serverless

로컬에서 서비스 만들기

$ serverless create \\
  --template aws-python3 \\
  --name resize-image-test \\
  --path resize-image-test
  • 프로젝트 생성시 기본으로 제공되는 파일을 제외하고 총 2개의 파일 생성 (requirements.txt, resize_image.py)
|- resize_image_test
|-|- .girignore <- create시 생성되는 파일
  |- handler.py <- create시 생성되는 파일
  |- serverless.yml <- create시 생성되는 파일
  |- resize_image.py <---- 파일 생성
  |- requirements.txt <---- 파일 생성
  • resize_image.py
import base64
import io
from urllib import parse

import boto3
from PIL import Image, ImageOps

class ResizeImage:
    def __init__(self, event, context, region, bucket):
        self.event = event
        self.context = context
        self.region = region
        self.bucket = bucket
        self.s3_client = boto3.client("s3", region_name=self.region)

    def check_option_method(self, request):
        headers = request['headers']

        # lambda@edge 를 설정하기 전에 cloudfront에서 prefight 의 응답 으로 내려 주는 값을 그대로 작성
        if request['method'] == 'OPTIONS':
            origin = headers.get('origin', [{}])[0].get('value', None)
            if origin:
                response = {
                    'status': '200',
                    'headers': {
                        'access-control-allow-origin':
                            [{'key': 'Access-Control-Allow-Origin', 'value': origin}],
                        'access-control-allow-headers':
                            [{'key': 'Access-Control-Allow-Headers', 'value': 'authorization'}],
                        'access-control-allow-methods':
                            [{'key': 'Access-Control-Allow-Methods', 'value': 'GET, HEAD, PUT, POST'}],
                        'access-control-allow-credentials':
                            [{'key': 'Access-Control-Allow-Credentials', 'value': 'true'}],
                        'vary':
                            [{'key': 'Vary',
                              'value': 'Origin, Access-Control-Request-Headers, Access-Control-Request-Method'}],
                        "server":
                            [{'key': 'Server', 'value': 'AmazonS3'}]
                    }
                }
            else:
                response = {'status': '401'}
            return response

    def _get_s3_object(self, request):
        """
        s3 object key
        """
        uri = request['uri']
        s3_object_key = uri[1:]

        try:
            return self.s3_client.get_object(
                Bucket=self.bucket,
                Key=parse.unquote(s3_object_key)
            )
        except Exception as e:
            print("get_s3_object Exception", e)

    def _get_image_spec(self, query):
        """
        query params로 받은 정보 알맞게 추출
        """
        params = {k: v[0] for k, v in parse.parse_qs(query.lower()).items()}

        # TODO: 최대 크기는 어떻게 설정할 것인가?
        width = int(params.get('w', 1080))
        height = int(params.get('h', 1080))
        quality = int(params.get('q', 80))
        return (width, height, quality)

    def _resize_image(self, original_image, image_spec):
        """
        get_image_spec에서 알맞게 추출된 spec을 이용하여 이미지 변환
        """
        width, height, quality = image_spec
        try:
            fixed_image = ImageOps.exif_transpose(original_image)
            fixed_image.thumbnail((width, height), Image.LANCZOS)
            bytes_io = io.BytesIO()
            fixed_image.save(bytes_io, format=original_image.format, optimize=True, quality=quality)

            original_image.close()

            result_size = bytes_io.tell()
            result_data = bytes_io.getvalue()
            result = base64.standard_b64encode(result_data).decode()
            bytes_io.close()
            res = {
                'resized_image': result,
                'resized_image_size': result_size
            }
            return res
        except Exception as e:
            print(e)

    # def handler_resize_image(event, context):
    def result(self):
        request = self.event['Records'][0]['cf']['request']
        response = self.event["Records"][0]["cf"]["response"]

        # 정상 response 오지 않을 때
        if int(response.get('status')) != 200:
            return response

        # preflight request 처리
        request_is_option_method = self.check_option_method(request)
        if request_is_option_method:
            return request_is_option_method

        # s3 object 가져 오기
        s3_response = self._get_s3_object(request)
        if not s3_response:
            return response
        s3_object_type = s3_response["ContentType"]

        # 올바른 이미지 규격이 아닐 때
        # TODO: 어느 이미지 포맷 까지 지원 할지 모르 겠음 (일단 jpeg, png, jpg 만 지원)
        if s3_object_type not in ["image/jpeg", "image/png", "image/jpg"]:
            return response

        # front 에서 직접 입력한 query string 을 읽어 사이즈 조절
        query = request['querystring']
        image_spec = self._get_image_spec(query)
        original_image = Image.open(s3_response["Body"])
        result = self._resize_image(original_image, image_spec)

        if not result or result.get('resized_image_size') > 1024 * 1024:
            return response

        response["status"] = 200
        response["statusDescription"] = "OK"
        response["body"] = result.get('resized_image')
        response["bodyEncoding"] = "base64"
        response["headers"]["content-type"] = [
            {"key": "Content-Type", "value": s3_object_type}
        ]
        return response
from resize_image import ResizeImage

def handler_resize_image(event, context):
    region = "ap-northeast-2" 
    bucket = "file"

    resize_image = ResizeImage(
        event=event,
        context=context,
        region=region,
        bucket=bucket
    )

    return resize_image.result()
  • requirements.txt
Pillow==7.0.0

서비스 배포

service: resize-image

frameworkVersion: '3'

provider:
  name: aws
  # Pillow issue가 있어서 부득이하게 3.7을 이용
  runtime: python3.7
  # Lambda@Edge를 이용하려면 버지니아에 배포가 되어야 함
  region: us-east-1
  lambdaHashingVersion: 20201221

functions:
  func:
    # command로 배포시 환경을 구분하기 위해 사용됨
    handler: "handler.handler"
    # handler: "handler_${opt:stage, 'dev'}.handler_resize_image"
		
plugins:
  - serverless-python-requirements
  • command로 배포
$ sls deploy

배포된 Lambda에 역할 추가

  • Lambda의 구성 - 권한 에서 편집
  • 위에서 생성한 역할 추가 (resize-image)

Lambda@Edge 배포

  • Lambda의 작업에서 Lambda@Edge 클릭
  • 배포 되어 있는 CloudFront를 선택하고 CloudFront event에서 Origin Response를 선택

동작 확인

  • 원본: taxijjang.png

  • 리사이즈: taxijjang.png?w=100

반응형

'aws' 카테고리의 다른 글

django media를 S3에 public으로 올리는 방법  (0) 2021.07.14
aws s3 버켓 퍼블릭 엑세스  (1) 2021.03.07
Comments