본문 바로가기
개발 공부 기록하기/- AWS & Infra

[AWS] API Gateway > Lambda > S3 Upload images

by soulduse 2023. 8. 29.

API Gateway로 이미지를 여러장 전송해서 Lambda를 통해 S3에 이미지를 업로드할 일이 생겼다. 단순히 API Gateway를 통해 multipart/form-data로 전송하면 되겠지라고 구성을 다했는데 여러가지 우여곡절이 있어서 기록차 글을 작성하게 되었다.

 

 

문제 1

  우선 내가 작성한 코드인데 해당 코드를 AWS Lambda function에 그냥 넣으면 코드에 import된 라이브러리를 불러오지 못해서 첫번째 문제에 봉착하게 된다. AWS 공식 홈페이지에서 어떻게 Lambda에 라이브러리와 함께 업로드 하면되는지 링크를 참조해서 해결하였는데 PIL(Pillow) 라이브러리를 사용하는 경우 AWS Lambda: cannot import name '_imaging' from 'PIL' 라는 에러가 발생한다. 꽤나 오래전부터 있어왔던 충돌문제같아 보이는데 아직까지 깔끔한 해결책이 나오진 않은것 같다.

 

해결책

1-1

pip install 할때 우선 Pillow 라이브러리를 추가하지 않고 그 외에 라이브러리만 설치하여 zip 파일로 만들고 lambda에 업로드한다.

AS-IS

# Local 환경
pip install --target ./package boto3 Pillow shortuuid

# 가상환경
pip install boto3 Pillow shortuuid

TO-BE

# Local 환경
pip install --target ./package boto3 shortuuid

# 가상환경
pip install boto3 shortuuid​

1-2

AWS Lambda console 창으로 이동하여 add layer를 누른다.

arn:aws:lambda:ap-northeast-2:770693421928:layer:Klayers-p39-pillow:1

ARN으로 지정하고 레이어를 추가해줍니다. 저는 python 3.9 버전을 기준으로 사용하였고 각자 파이썬 버전에 맞는 버전을 찾아서 ARN을 추가 해주시면 됩니다. (참고링크)

 

문제 2

AWS API Gateway가 multipart/form-data 형식으로 데이터를 던지면 받아주질 못합니다.

 

해결책

API Gateway에서 multipart/form-data를 받을 수 있게 추가 설정을 해주도록 합니다.

 

1. AWS API Gateway 콘솔로가서 자신이 만든 API를 클릭합니다.

2. 메뉴 하단에 설정으로 진입

3. 가장 하단에 있는 이진 미디어 형식에 multipart/form-data 형식 추가

4. 왼쪽 메뉴중 리소스 탭을 선택 > 자신이 만든 메소드를 선택 > 통합 요청을 클릭합니다.

5. Lambda에서 데이터를 받아주기 위해 Lambda 함수를 연결 해줍니다.

6. 아래와 같이 매핑 템플릿을 설정후 저장합니다. 

 

이제 API로 데이터를 전송하면 body-json으로 데이터가 넘어오게 됩니다. 그런데 이게 좀 복잡한게 넘어온 데이터가 base64러 인코딩 되어 이걸 다시 base64로 디코딩해주고 파싱을 하는 과정을 거쳐야 합니다. (여기서 꽤나 많은 삽질을 함) 파싱 관련 내용은 아래 코드중 parse_multipart_data를 유심히 보시면 이해가 되실거라 생각합니다..! 

 

 

AWS Lambda 전체코드

lambda_function.py
import cgi
import json
from PIL import Image, UnidentifiedImageError
import io
import boto3
import base64
import os
import shortuuid
from datetime import datetime

S3_BUCKET_NAME = 'YOUR_S3_BUCKET_NAME'
STEP_FUNCTION_ARN = 'YOUR_STEP_FUNCTION_ARN'

s3_client = boto3.client('s3')
step_functions_client = boto3.client('stepfunctions')


def lambda_handler(event, context):
    try:
        title, images_data = parse_multipart_data(event)
        if images_data:
            base_folder_name = upload_images_to_s3(images_data)
            return start_step_functions(event, title, base_folder_name)
        else:
            return {'status': 'Invalid title or image data'}
    except Exception as e:
        print("Error:", e)
        return {'status': 'Error occurred'}


def parse_multipart_data(event):
    """multipart/form-data에서 title과 이미지 데이터를 추출하는 함수"""
    # body-json, params, stage-variables, context 등을 event에서 추출
    body_json = event.get('body-json', {})
    params = event.get('params', {})

    # base64 디코딩
    decoded_data = base64.b64decode(body_json)

    # Content-Type 헤더에서 boundary 값을 추출
    content_type_header = params.get('header', {}).get('Content-Type', '')

    # 멀티파트 헤더 정보 파싱
    pdict = cgi.parse_header(content_type_header)[1]
    if 'boundary' in pdict:
        pdict['boundary'] = pdict['boundary'].encode('utf-8')
    pdict['CONTENT-LENGTH'] = len(decoded_data)

    # multipart 데이터 파싱
    fp = io.BytesIO(decoded_data)
    form_data = cgi.parse_multipart(fp, pdict)

    # form에서 필요한 데이터를 추출
    title = form_data.get('title', [None])[0]
    images_data_files = form_data.get('images_data', [])

    return title, images_data_files


def upload_images_to_s3(images_data):
    """S3에 이미지들을 업로드하고 업로드된 폴더 이름을 반환하는 함수"""
    # S3에 저장할 폴더 구조를 생성
    uuid = shortuuid.uuid()
    base_folder_name = os.path.join('finetune_images', datetime.now().strftime('%Y%m%d'), uuid)
    for idx, image_data in enumerate(images_data):
        # 이미지 스트림 준비
        image_stream = io.BytesIO(image_data)

        # 이미지가 JPEG가 아닌 경우만 변환
        if not is_jpeg(image_stream):
            image = Image.open(image_stream)
            buffered = io.BytesIO()
            image.save(buffered, format="JPEG")
            image_data = buffered.getvalue()

        file_name = f"image_{idx + 1}.jpg"
        s3_path = os.path.join(base_folder_name, file_name)
        try:
            s3_client.put_object(Bucket=S3_BUCKET_NAME, Key=s3_path, Body=image_data,
                                 ContentType='image/jpeg')
        except Exception as e:
            print("S3 Upload Error:", e)

    return base_folder_name


def start_step_functions(event, title, base_folder_name):
    """Step Functions를 시작하고 그 결과를 반환하는 함수"""
    token = event['params']['header']['token']
    input_payload = {
        'token': token,
        'folder_path': base_folder_name,
        'title': title
    }

    response = step_functions_client.start_execution(
        stateMachineArn=STEP_FUNCTION_ARN,
        input=json.dumps(input_payload)
    )

    return {
        'status': 'Images Uploaded and Step Functions Started',
        'folder_path': base_folder_name,
        'step_function_execution_arn': response['executionArn']
    }


def is_jpeg(image_stream):
    """이미지가 JPEG 형식인지 확인하는 함수"""
    try:
        image_stream.seek(0)  # 스트림을 처음으로 되돌림
        image = Image.open(image_stream)
        return image.format == 'JPEG'
    except UnidentifiedImageError:
        return False

 

결과

Postman으로 생성한 API Gateway host로 파일전송

S3에 잘 저장된것 확인

 

 

관련 참고자료

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

 

Python Lambda 함수에 대한 .zip 파일 아카이브 작업 - AWS Lambda

배포 패키지 또는 계층의 종속 항목이 런타임 포함 라이브러리보다 우선하므로 SDK도 포함하지 않고 패키지에 urllib3과 같은 SDK 종속 항목을 포함하면 버전 불일치 문제가 발생할 수 있습니다. 자

docs.aws.amazon.com

https://devlog-wjdrbs96.tistory.com/331

 

[AWS] API Gateway, Lambda로 S3 파일 업로드 API 만들기

API Gateway, Lambda로 S3 파일 업로드 하기 저번 글 에서 API gateway를 만들고 해당 API가 호출되었을 때 람다 함수가 호출되는 간단한 예제를 진행해보았습니다. 이번 글에서는 조금 더 응용해서 API gatew

devlog-wjdrbs96.tistory.com

https://medium.com/hackernoon/how-to-solve-the-api-gw-30-seconds-limitation-using-alb-700bf3b1bd0e

 

How to solve the API-GW “30 seconds limitation” using ALB

You have a serveless solution using multiple AWS lambdas and API-GW for the APIs. all is working great but then you have this one (or…

medium.com

https://github.com/serverless/serverless/issues/3171

 

API Gateway timeout after 30 seconds · Issue #3171 · serverless/serverless

It seems aws api gateway has a timeout of 30 seconds. I am getting the following error message: "Endpoint request timed out". I don't think there is any way to increase the api gateway timeout. I c...

github.com

https://stackoverflow.com/questions/57197283/aws-lambda-cannot-import-name-imaging-from-pil

 

AWS Lambda: cannot import name '_imaging' from 'PIL'

I currently try to get this AWS Lambda Getting started tutorial running: https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example-deployment-pkg.html#with-s3-example-deployment-pkg-python Howe...

stackoverflow.com

 

반응형