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
https://devlog-wjdrbs96.tistory.com/331
https://medium.com/hackernoon/how-to-solve-the-api-gw-30-seconds-limitation-using-alb-700bf3b1bd0e
https://github.com/serverless/serverless/issues/3171
https://stackoverflow.com/questions/57197283/aws-lambda-cannot-import-name-imaging-from-pil
'개발 공부 기록하기 > - AWS & Infra' 카테고리의 다른 글
ElasticBeanstalk + GithubAction Deployment failed: Error 해결 (0) | 2024.02.08 |
---|---|
Kuberneties 책 정리 2장 - 팟, 레플리케이션 컨트롤러, 서비스 (1) | 2022.08.29 |
ElasticBeanstalk DB 연결시 에러발생 해결 (2) | 2022.02.28 |
AWS EC2 하드디스크 용량 늘리기 (4) | 2019.08.25 |
AWS 개인 서버 이전하기 - EC2 (0) | 2019.07.23 |