AWS

AWS Lambda 를 이용하여 S3 버킷에 업로드된 이미지 자동 리사이징

misankim 2023. 4. 6. 00:30

최근 Lambda 서비스 관련 학습을 진행하고 있는데요. Lambda 를 이용한 대표적인 사용 예로 자주 등장하는 S3 버킷에 업로드된 이미지를 자동으로 리사이징하는 내용으로 학습 진행 후 내용 공유드리려고 합니다.

1. 로컬 환경에서 python 스크립트로 이미지 리사이징 테스트

Lambda 환경에서 이미지를 리사이징하기 앞서 연습삼아 로컬의 python 환경에서 이미지를 리사이징 해봅니다. python 에서 이미지를 리사이징하기 위해서는 Pillow 라는 모듈을 사용하기 때문에 설치 후 스크립트를 작성합니다.

## 모듈 설치
pip install Pillow

## 스크립트(resize.py)

from PIL import Image
import sys

src_img=sys.argv[1] # 첫 번째 인수를 원본 파일명 변수로 받음
dst_img=sys.argv[2] # 두 번째 인수를 대상 파일명 변수로 받음
image = Image.open(src_img)
resize_image = image.resize((480,270)) # 리사이징 목표 해상도
resize_image.save(dst_img)

 

리사이징된 파일을 확인하면 아래와 같이 정상적으로 리사이징이 완료된 것을 확인할 수 있습니다.

로컬에서 이미지 리사이징 테스트는 완료하였으니, 이제 AWS Lambda 환경에서 이미지 리사이징 함수 및 S3 버킷 업로드 시 자동으로 해당 함수를 호출하도록 구성합니다.

2. 람다 계층 생성

이전 Serverless API 구성 관련 게시물에서도 여러번 언급한 것과 같이 Lambda 의 python 런타임이 모든 python 모듈을 포함하고 있지는 않기 때문에, 이미지 리사이징에 사용하는 Pillow 모듈을 다운로드하여 zip 파일 형태로 업로드, 람다 함수에 사용 가능한 계층의 형태로 만들어줍니다.

# (로컬 또는 서버 등) python 환경에서 진행
mkdir python && cd python

pip install Pillow -t .

cd ../
zip -r pillow.zip python/

3. 람다 함수 작성

함수 이름은 구별하기 쉬운 이름으로 임의로 합니다. 런타임 환경은 계층의 버전 호환성에 맞게 설정합니다.

이전 단계에서 생성한 이미지 리사이징 모듈 pillow 계층을 함수에 추가해줍니다.

구성 -> 일반 구성 탭으로 이동하여 함수의 메모리 할당량과 제한 시간을 수정해줍니다. 이미지의 사이즈의 따라 기본 메모리로는 아예 함수 실행이 안되거나, 제한 시간을 초과해서 함수가 종료되는 경우가 있습니다. 만약 프로덕션 환경이라면 주로 업로드될 이미지의 크기에 따라 조정이 필요할 것으로 생각됩니다.

람다 함수가 S3 버킷에 오브젝트를 읽고 쓸수 있도록 권한을 주기 위해 구성 -> 권한 탭으로 이동하여 람다 함수가 사용할 IAM 역할에 부여된 정책에 추가로 AmazonS3FullAccess 정책을 부여합니다. 만약 프로덕션 환경이라면 업로드/다운로드가 발생할 S3 버킷으로 제한된 별도 정책을 구성하여 람다 함수의 IAM 역할에 연결하는 것이 바람직합니다.

 

이제 소스 코드를 구성합니다. 함수 생성 시 기본으로 생성되어 있는 lambda_function.py 파일의 내용을 아래와 같이 대체합니다. 여기서 bucket_name = 'premisan-test' 부분은 테스트 환경의 S3 버킷으로 치환해줍니다.

from PIL import Image
import logging
import boto3
from botocore.exceptions import ClientError

s3 = boto3.client('s3')
bucket_name = 'premisan-test' # 버킷 이름에 맞게 적절히 수정

def resize_img(file_name, new_file_name):
    image = Image.open(file_name)
    resize_image = image.resize((480,270)) # 리사이징할 목표 사이즈, 적절하게 수정
    resize_image.save(new_file_name)

def upload_file(file_name, bucket_name, object_name):
    try:
        response = s3.upload_file(file_name, bucket_name, object_name)
    except ClientError as e:
        logging.error(e)
        return False
    return True

def lambda_handler(event, context):
    print(event)
    if 'Records' in event: # s3 이벤트 알림을 통해 넘어오는 event
        key = event['Records'][0]['s3']['object']['key']
    elif 'detail' in event: # cloudtrail, cloudwatch event 를 통해 넘어오는 event
        key = event['detail']['requestParameters']['key']
    if 'resized' in key.split('/'): # 리사이징 후 업로드된 이미지에 의한 재귀호출 방지
        return True
    path = key.rsplit('/',maxsplit=1)[0]
    file_name = key.rsplit('/',maxsplit=1)[1]
    tmp_path = '/tmp/' + file_name
    tmp_new_path = '/tmp/[resized]' + file_name
    new_key = path + '/resized/[resized]' + file_name

    s3.download_file(bucket_name, key, tmp_path) # 이미지 다운로드
    resize_img(tmp_path, tmp_new_path) # 이미지 리사이징
    return upload_file(tmp_new_path, bucket_name, new_key) # 리사이징한 이미지 업로드

소스의 구성을 간단하게 설명해드리자면

1) 업로드된 오브젝트의 키(경로+파일명이 합쳐진 파라미터)를 변수에 담아, 
2) 경로에 리사이징된 파일이 업로드되는 "resized"경로가 포함되어 있는지 확인 후, 
3) 경로와 파일명을 나누고, 
4) S3 버킷에서 임시 디렉토리(/tmp/)로 파일을 다운로드하여, 
5) 이미지를 리사이징하고, 
6) 리사이징된 이미지를 S3 버킷의 "기존 경로 + resized/" 경로로 업로드합니다.

참고로 2) 항목의 경우 리사이징된 이미지가 업로드됨으로 인해 함수가 무한히 반복되는 재귀호출을 방지하기 위한 구문으로, 현재는 이미지가 업로드되는 S3 버킷과 리사이징된 이미지가 업로드되는 S3 버킷이 동일하기 때문에 재귀호출이 발생할 수 있어, 위와 같이 소스 코드에서 처리할 수도 있으나, 가능하면 이미지가 업로드되는 S3 버킷과 리사이징된 이미지가 업로드되는 S3 버킷을 달리하여, 재귀호출을 막는 것이 제일 좋은 방법일 것 같습니다.

 

만약 그렇게 구성한다면 소스의 내용 중 아래 부분을 수정하시면 됩니다.(필수는 아니며, 테스트 환경에서는 동일 버킷으로 진행하였습니다.)

# 라인 넘버 7
bucket_name = '이미지_업/다운로드_버킷_이름'
->
uploaded_bucket_name = '이미지_업로드_버킷_이름'
resized_bucket_name = '리사이징된_이미지_업로드_버킷_이름'

# 라인 넘버 36
s3.download_file(bucket_name, key, tmp_path)
->
s3.download_file(uploaded_bucket_name, key, tmp_path)

# 라인 넘버 38(마지막 줄)
return upload_file(tmp_new_path, bucket_name, new_key)
->
return upload_file(tmp_new_path, resized_bucket_name, new_key)

4. S3 버킷 이벤트 알림 설정 및 폴더 생성

이미지 파일이 업로드되면 람다 함수를 트리거하도록 S3 버킷의 이벤트 알림 설정을 추가해줍니다. s3 콘솔 -> [해당 버킷 이름] -> 속성 -> 이벤트 알림 -> 이벤트 알림 생성 버튼을 눌러 이벤트 알림을 생성합니다.

 

 

이미지 파일이 업로드될 폴더(upload-img/)도 생성해줍니다.

참고로 오브젝트 업로드 발생 시 람다 함수로 넘어오는 event 파라미터의 내용은 아래와 같이 구성됩니다.

{
    'Records': 
    [
        {
            'eventVersion': '2.1', 
            'eventSource': 'aws:s3',
            'awsRegion': 'ap-northeast-2', 
            'eventTime': '2021-06-14T07:45:46.203Z', 
            'eventName': 'ObjectCreated:Put', 
            'userIdentity': 
                {
                    'principalId': 'AWS:AIDARMNVENIMHCPAPUJJD'
                },
            'requestParameters': 
                {
                    'sourceIPAddress': '123.123.123.123'
                }, 
            'responseElements': 
                {
                    'x-amz-request-id': 'CZD1DQ2G76GD5KC0', 
                    'x-amz-id-2': 'EJM8B5lOU0PW84b18PzJfxGKqs1PCkwzo+UQzJnbPG9RSqyinQaj7fL3omgAe0PoWsnj/SgZIMrMSR1fsNve0i8CWk7BwXau'
                }, 
            's3': 
                {
                    's3SchemaVersion': '1.0', 
                    'configurationId': 'resize-img', 
                    'bucket': 
                        {
                            'name': 'premisan-test', 
                            'ownerIdentity': 
                                {
                                    'principalId': 'A1SMUGNYW59NOO'
                                }, 
                            'arn': 'arn:aws:s3:::premisan-test'
                        }, 
                    'object': 
                        {
                            'key': 'upload-img/pink-flowers-on-trees-981364.jpg', 
                            'size': 5879557, 
                            'eTag': 'aa2edbdab63a78627ecbc90e2c721848', 
                            'versionId': 'muLmm1Irdfj8Wo7m3AYczN1kQyEYbIld', 
                            'sequencer': '0060C7092F98351A24'
                        }
                }
        }
    ]
}

5. S3 버킷에 이미지 파일 업로드하여 작동 테스트

테스트를 위해 upload-img/ 폴더에 약 5.6MB 크기의 고해상도 이미지를 업로드하였습니다.

아래와 같이 하위 폴더로 "resized/" 라는 폴더가 생성되며, "[resized] + 기존 파일명" 규칙으로 리사이징된 이미지가 업로드된 것을 확인할 수 있습니다.

 

해당 파일을 다운로드하여 확인 시 정상적으로 이미지가 리사이징된 것을 확인할 수 있습니다.

6. (추가) Cloudwatch Event 규칙 생성하여 람다 함수 트리거

위에서 구성한 것과 같이 S3 버킷의 이벤트 알림을 생성하여 람다 함수를 트리거하는 방법도 있지만, Cloudwatch Event 규칙을 생성하여 람다 함수를 트리거할 수도 있습니다. 다만, Cloudtrail 통해 S3 버킷의 API 활동을 로깅하게 되는데 기본적으로 Cloudtrail의 경우 버킷 수준의 이벤트만 로깅하도록 설정되어 있기 때문에, Cloudtrail 추적 메뉴를 통해 S3 버킷에 대한 데이터 이벤트 추적을 생성해야 오브젝트 수준의 이벤트까지 로깅이 가능합니다.

때문에 S3 버킷 이벤트 알림으로 람다 함수를 트리거하는 설정이 더 간단하지만, 이 방법도 가능은 하기에 방법을 간단히 소개해드립니다. 먼저 람다 함수까지 구성이 완료되어 S3 버킷 이벤트 알림을 설정하기 직전의 상태에서 진행합니다.

1) Cloudtrail 추적 생성

Cloudtrail 콘솔 -> 추적 -> 추적 생성 버튼을 눌러 추적을 생성합니다. CloudWatch Logs 옵션은 필수는 아니지만, 만약 함수가 실행되지 않거나 하는 경우 Cloudtrail 에서 넘어온 이벤트를 확인하기 위해 편의상 활성화합니다.

 

 

로그 이벤트 선택 단계에서 이벤트 유형은 "데이터 이벤트", 데이터 이벤트 소스는 "s3"로 선택합니다. 모든 버킷에 체크 해제 후 이벤트를 모니터링할 버킷 이름(premisan-test)과 폴더 이름(upload-img-cw)을 아래와 같이 구성해줍니다.

 

S3 에서 해당 버킷에 업로드용 폴더 "upload-img-cw/"를 생성해줍니다.

2) Cloudwatch Event 규칙 생성

Cloudwatch 콘솔 -> event -> 규칙 -> 규칙 생성 버튼을 눌러 규칙을 생성합니다.

이벤트 패턴 미리보기 항목으로 자동 완성되는 이벤트 패턴은 아래와 같습니다.

{
  "source": [
    "aws.s3"
  ],
  "detail-type": [
    "AWS API Call via CloudTrail"
  ],
  "detail": {
    "eventSource": [
      "s3.amazonaws.com"
    ],
    "eventName": [
      "PutObject"
    ],
    "requestParameters": {
      "bucketName": [
        "premisan-test"
      ]
    }
  }
}

3) S3 버킷에 이미지 파일 업로드하여 작동 테스트

람다 함수의 중복 실행을 막기 위해 기존 S3 버킷 이벤트 알림에 구성한 prefix 경로(upload-img/)와 다른 경로(upload-img-cw/)로 Cloudtrail 추적을 생성했기 때문에,
upload-img-cw/ 경로에 이미지 파일을 업로드해봅니다.

아래와 같이 resized 폴더가 새로 생성되어 해당 폴더에 "[resized] + 기존 파일명" 규칙으로 리사이징된 이미지 파일이 업로드된 것을 확인할 수 있습니다.

참고로, Cloudwatch Event 를 통해 넘어오는 람다 함수의 event 파라미터는 아래와 같이 구성됩니다.

{
    'version': '0', 
    'id': '93ce8cd7-83c0-19d6-aa41-d7ace7882231', 
    'detail-type': 'AWS API Call via CloudTrail', 
    'source': 'aws.s3', 
    'account': '111222333444', 
    'time': '2021-06-15T00:38:08Z', 
    'region': 'ap-northeast-2', 
    'resources': [], 
    'detail': 
        {
            'eventVersion': '1.08', 
            'userIdentity': 
                {
                    'type': 'IAMUser', 
                    'principalId': 'AIDARMNVENIMHCPAPUJJD', 
                    'arn': 'arn:aws:iam::111222333444:user/premisan', 
                    'accountId': '111222333444', 
                    'accessKeyId': 'AXXXXXXXXXXXXXXXXXXXX', 
                    'userName': 'premisan', 
                    'sessionContext': 
                        {
                            'attributes': 
                                {
                                    'creationDate': '2021-06-15T00:01:48Z', 
                                    'mfaAuthenticated': 'false'
                                }
                        }
                }, 
            'eventTime': '2021-06-15T00:38:08Z', 
            'eventSource': 's3.amazonaws.com', 
            'eventName': 'PutObject', 
            'awsRegion': 'ap-northeast-2', 
            'sourceIPAddress': '123.123.123.123', 
            'userAgent': '[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36]',
            'requestParameters': 
                {
                    'X-Amz-Date': '20210615T003807Z', 
                    'bucketName': 'premisan-test', 
                    'X-Amz-Algorithm': 'AWS4-HMAC-SHA256', 
                    'x-amz-acl': 'private', 
                    'X-Amz-SignedHeaders': 'content-md5;content-type;host;x-amz-acl;x-amz-storage-class', 
                    'Host': 'premisan-test.s3.ap-northeast-2.amazonaws.com',
                    'X-Amz-Expires': '300', 
                    'key': 'resize-img/pink-flowers-on-trees-981364.jpg', 
                    'x-amz-storage-class': 'STANDARD'
                }, 
            'responseElements': 
                {
                    'x-amz-version-id': 'hf28yghCJCX3SazCnUC9Yw9UXcshdQ6s'
                }, 
            'additionalEventData': 
                {
                    'SignatureVersion': 'SigV4', 
                    'CipherSuite': 'ECDHE-RSA-AES128-GCM-SHA256', 
                    'bytesTransferredIn': 5879557.0, 
                    'AuthenticationMethod': 'QueryString', 
                    'x-amz-id-2': 'UHqFATREfi72Pv2MwnJ4QN8Z5d2jOMvwcEiFSCW6QuZm1ln40BXxQZfUYhYxpbIhEL+vFg5aEgY=', 
                    'bytesTransferredOut': 0.0
                }, 
            'requestID': 'TKHS9E9A0F3VCS9P', 
            'eventID': '6a076eb6-4ce2-4d4e-8703-0fcd4ac9c3c7', 
            'readOnly': False, 
            'resources': 
                [
                    {
                        'type': 'AWS::S3::Object', 
                        'ARN': 'arn:aws:s3:::premisan-test/upload-img-cw/pink-flowers-on-trees-981364.jpg'
                    }, 
                    {
                        'accountId': '111222333444', 
                        'type': 'AWS::S3::Bucket', 
                        'ARN': 'arn:aws:s3:::premisan-test'
                    }
                ], 
            'eventType': 'AwsApiCall', 
            'managementEvent': False, 
            'recipientAccountId': '111222333444', 
            'eventCategory': 'Data'
        }
}

7. 마치며

이렇게 S3 버킷에 이미지 업로드 시 자동으로 이미지를 리사이징하도록 S3 버킷 이벤트 알림 혹은 Cloudwatch Event 규칙을 생성하여 람다 함수를 트리거하고, 람수 함수를 구성하여 업로드된 이미지를 리사이징 후 업로드하도록 구성해봤습니다. 이미지 리사이징의 경우 람다 함수의 대표적인 사용 예이기 때문에 한 번쯤 직접 구성해보는 것이 람다 함수를 통해 AWS의 여러 서비스 간 상호 작용하는 방식을 이해하는 데에 많은 도움이 될 것으로 생각됩니다.