Search

AWS Lambda + S3 Trigger 이미지 자동 생성 실습

카테고리
Dev-Ops
태그
Python
AWS
게시일
2024/02/02
수정일
2024/11/18 15:42
시리즈
AWS-Study
1 more property

1. AWS Lambda란?

AWS Lambda은 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있게 해주는 컴퓨팅 서비스입니다. Lambda는 고가용성 컴퓨팅 인프라에서 코드를 실행하고 서버와 운영 체제 유지 관리, 용량 프로비저닝 및 자동 조정, 코드 및 보안 패치 배포, 로깅 등 모든 컴퓨팅 리소스 관리를 수행합니다. Lambda를 사용하면 Lambda가 지원하는 언어 런타임 중 하나로 코드를 제공하기만 하면 됩니다. Lambda 함수에 코드를 구성합니다. Lambda 서비스는 필요할 때만 함수를 실행하고 자동으로 확장됩니다. 사용한 컴퓨팅 시간만큼만 비용을 지불하고, 코드가 실행되지 않을 때는 요금이 부과되지 않습니다.
Plain Text
복사
AWS 에서 제공하는 Lambda 라는 서비스는 다시말해, 서버없이(Serverless) 원하는 코드를 실행할 수 있는 서비스입니다. Lambda는 AWS에서 제공되는 여러 서비스와 연동이 가능하며, 마치 레고를 조립하듯 서비스와 로직을 원하는데로 조합이 가능합니다.
이번 글에서는 AWS Lambda 기능을 이용하여 S3에 파일이 생성되었을 때, 원하는 형태의 이미지를 자동으로 생성하는 기능(Function)을 구성해보도록 하겠습니다.
이번 실습에서 이용할 AWS S3 Bucket과 IAM에서 발행하는 Access-Key, Secret-Key를 생성해 두었다는 가정으로 작성되었습니다.

2. AWS IAM Policy & Role

먼저 AWS의 Lambda에 적절한 권한을 부여해야 합니다. 이를 위해서는 IAM(Identity and Access Management) 서비스에서 적절한 정책(Policy)과 역할(Role)을 설정하여 부여해주어야 합니다.

2.1 IAM Policies

Create Policy

먼저 IAM의 Policy로 들어가 Lambda에 부여할 Policy를 생성해주도록 합니다.
정책 생성 과정 중 Policy editor에서 JSON을 선택하여 아래와 같이 특정 정책을 추가해주도록 합니다. 정책에 대한 상세 내용은 주석을 참고하시면 되며, my-lambda-resources 버킷은 Lambda에서 사용할 리소스를 저장하는 버킷을 의미합니다.
{ "Version": "2012-10-17", "Statement": [ // CloudWatch LogGroup, LogStream 생성 권한과 Log를 생성하는 권한을 부여함 { "Effect": "Allow", "Action": [ "logs:PutLogEvents", "logs:CreateLogGroup", "logs:CreateLogStream" ], "Resource": "arn:aws:logs:*:*:*" }, // 원하는 Bucket, Bucket 디렉토리를 조회할 수 있는 권한을 부여함 { "Sid": "ListObjectsInBucket", "Effect": "Allow", "Action": [ "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::[YOUR-BUCKET-NAME]/[BUCKET-PATH]/*", "arn:aws:s3:::my-lambda-resources/*" ] }, // 원하는 Bucket, Bucket 내의 모든 Object 제어 권한을 부여함 { "Sid": "AllObjectActions", "Effect": "Allow", "Action": "s3:*Object", "Resource": [ "arn:aws:s3:::[YOUR-BUCKET-NAME]/[BUCKET-PATH]/*", "arn:aws:s3:::my-lambda-resources/*" ] } ] }
JSON
복사
설정을 완료했다면, 설정한 Policy의 이름을 검색하여 Policy details를 확인해보도록 합니다. 아래와 같이 Permissions Summary을 보았을 때 CloudWatch Logs, S3가 적절히 부여됨을 확인합니다.

2.2 IAM Roles

Create Role

앞서 Policy를 생성해주었다면, 이제 Lambda에 부여할 Role을 생성해주도록 합니다. Role을 생성하기 위해서는 앞서 Policy를 생성해주어야 합니다.

Trusted entity type & Use case

아래와 같이 신뢰하는 엔티티 타입은 AWS Service로 설정하고, Use case는 Lambda로 설정합니다.

Add permissions

다음으로 넘어가게 되면 Role에 적용할 Policy 즉, 정책을 설정하게 됩니다. 이 정책에는 방금전 생성한 Policy를 부여하면 됩니다. Permissions policies에서 검색하여 원하는 정책을 검색 및 선택하여 다음으로 넘어갑니다.
다음 페이지에서는 Role Name과 Description만 설정하고 완료하면 됩니다.

3. Lambda

3.1 Function

앞의 과정에서 Lambda를 위한 Policy와 Role을 설정하였다면, 본격적으로 Lambda를 생성할 준비가 되었습니다. Lambda를 검색하여 Function을 생성해주도록 합니다.

Create function & Permissions

생성할 Function은 Python3.11 버전의 코드로 작성될 예정입니다. 따라서 설정은 다음과 같이 해주도록 합니다.
Function name : 원하는 기능 이름
Runtime : Python 3.11
Execution role : Use an existing role

3.2 Trigger

Lambda의 Function이 정상적으로 생성된 것을 확인하였다면, Trigger를 생성해주어야 합니다. 먼저 Add trigger 버튼을 이용하여 트리거 생성 창으로 들어갑니다.

Trigger configuration

우리가 사용할 서비스는 S3입니다. 서비스로 S3를 선택하고, Bucket을 선택해줍니다. Event types는 어떤 이벤트가 발생했을 때 이 트리거가 동작하는지를 설정하는 영역입니다. 실습에서는 파일이 생성되었을 때를 고려하였기에 All object create events를 선택합니다. 그리고 Prefix는 원하는 bucket 내의 directory를 설정할 수 있습니다.

Configuration is ambiguously defined

만약 아래와 같이 두 개 이상의 Role을 넣을 수 없다는 에러가 발생한다면, 이미 해당 S3 Bucket에 이벤트가 등록돼 있다는 것을 의미합니다.
해당 에러는 S3의 특정 Bucket의 Event notifications를 참고하시면 됩니다.

Configuration → Trigger

정상적으로 등록되었다면, 다음과 같이 Configuration에서 확인할 수 있습니다.

Configuration → Permissions

생성한 Lambda에 정상적으로 권한이 부여됐는지도 확인해보도록 합니다.

4. Code & Test

이번 실습에서는, 위에 소개드린 것처럼 S3에 파일이 생성되었을 때, 원하는 형태의 이미지를 자동으로 생성하는 기능(Function)입니다. 따라서, 코드는 아래와 같은 환경으로 테스트 해볼 수 있습니다.

4.1 In local

먼저 로컬에서 PoC를 구현하고 코드가 잘 동작하는지 여부를 검토해야 합니다. 이를 위해 Directory와 Code를 다음과 같이 구성하였습니다.

Directory

. ├── NotoSansKR-Bold.ttf ├── NotoSansKR-Medium.ttf ├── NotoSansKR-Regular.ttf ├── give_me_more.png ├── pil_transparency_test.py └── result_give_me_more.png
Plain Text
복사

PoC

from PIL import Image, ImageDraw, ImageFont def insert_text_center(image, message, font, font_color=(255, 255, 255)): # Noto Sans KR W, H = image.size draw = ImageDraw.Draw(image, "RGBA") x, y, w, h = draw.textbbox((0, 0), message, font=font) draw.text(((W-w)/2, (H-h)/2), message, font=font, fill=font_color) return image def insert_transparency_rectangle(image, color=(0, 0, 0), transparency=0.70): # Transparency : 70% opacity = int(255 * transparency) overlay = Image.new("RGBA", image.size, color + (0, )) draw = ImageDraw.Draw(overlay) draw.rectangle(((0, 0), image.size), fill=color + (opacity, )) image = image.convert('RGBA') image = Image.alpha_composite(image, overlay) return image filename = "give_me_more.png" target_image = Image.open(filename) target_text = "그렇게 꿀꿀이가 되고...." target_font = ImageFont.truetype("NotoSansKR-Bold.ttf", 80) target_image = insert_transparency_rectangle(image=target_image) target_image = insert_text_center(image=target_image, message=target_text, font=target_font) target_image.save(f"result_{filename}")
Python
복사
코드 설명은 다음과 같습니다.
1. Image를 메모리에 로드함 2. 가져온 이미지 중앙에 쓸 텍스트와 폰트를 설정 3. 이미지 위에 검정색으로 70%(0%: 완전 투명, 100%: 투명하지 않음)의 투명도를 주어 덮어씌움 4. 이미지 위에 흰 색으로 글씨를 작성함

Before & After

4.2 In lambda

위의 코드를 그대로 Lambda에 올려서 테스트해볼 수는 없습니다. 특히 Lambda에서 ttf와 같은 코드가 아닌 값을 가져와 사용하기에는 다소 무리가 있습니다. 때문에 이를 위해 Lambda에서 사용할 리소스를 가지고 있는 S3 bucket을 새로 만들어주고, 이를 접근하기 위한 코드를 함께 작성하였습니다. 또한 PIL 과 같은 외부 라이브러리를 사용하기 위해서는 Lambda 코드 영역 아래에 Layers에 원하는 라이브러리를 추가해주어야 합니다.
Lambda에서 사용할 Resource를 보관하는 Bucket 필요
lambda-resources라는 버킷은 이미 예약돼 있는 것으로 보여 별도로 생성이 필요함
이름은 해당 글에서는 my-lambda-resources라고 하였음
Lambda에서 S3 파일을 읽고 사용할 Access-Key, Secret-Key 필요
IAM에서 적절한 Access-Key, Secret-Key 생성
Layers에 Lambda에서 사용할 외부 라이브러리를 검색하여 추가 필요
https://api.klayers.cloud/api/v2/p3.11/layers/latest/ap-northeast-2/html
위의 표시된 python 버전과 region을 수정하게 되면 Layers에 추가할 수 있는 라이브러리 검색이 가능함

Add a layers

먼저 Code 를 들어갑니다. Code source가 보일 겁니다. 아래로 내려가게 되면 Layers라는 탭이 나타나게 되는데 해당 탭에서 Add a layer를 클릭합니다.
이때 Choose a layer를 누르고 Specify an ARN 을 선택하여 적절한 layer인지 검증합니다.

PoC

이제 설정이 적절히 완료되었다면 Lambda에 들어갈 코드를 작성해봅시다. 아래의 코드내에 주석을 작성해두었습니다. 주석 내용 참고해주시면 될 것 같습니다.
import os import io import json import boto3 import urllib.parse from PIL import Image, ImageDraw, ImageFont def insert_text_center(image, message, font, font_color=(255, 255, 255)): # Noto Sans KR W, H = image.size draw = ImageDraw.Draw(image, "RGBA") x, y, w, h = draw.textbbox((0, 0), message, font=font) draw.text(((W - w) / 2, (H - h) / 2), message, font=font, fill=font_color) return image def insert_transparency_rectangle(image, color=(0, 0, 0), transparency=0.70): # Transparency : 70% opacity = int(255 * transparency) overlay = Image.new("RGBA", image.size, color + (0,)) draw = ImageDraw.Draw(overlay) draw.rectangle(((0, 0), image.size), fill=color + (opacity,)) image = image.convert('RGBA') image = Image.alpha_composite(image, overlay) return image def lambda_handler(event, context): # Lambda resource bucket resource_bucket = "my-lambda-resources" # Actual handling bucket : [YOUR BUCKET] handle_bucket_name = event['Records'][0]['s3']['bucket']['name'] # handle_key : Filename and directory. handle_key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding="utf-8") # Defense logic loop. if "result" in handle_key: return { 'statusCode': 400, 'body': json.dumps('File name has `result`.') } # Separate filename and directory. handle_filename = os.path.basename(handle_key) _, handle_extension = os.path.splitext(handle_filename) handle_directory = os.path.dirname(handle_key) # Set `S3` client s3 = boto3.resource( "s3", region_name="ap-northeast-2", aws_access_key_id="[ACCESS-KEY]", aws_secret_access_key="[SECRET-KEY]", ) # Set S3 bucket object. handle_bucket = s3.Bucket(handle_bucket_name) resource_bucket = s3.Bucket(resource_bucket) # Get file object from handling bucket. handle_object = handle_bucket.Object(handle_key).get()['Body'] # Get font file object from handling bucket. font_object = resource_bucket.Object("static/NotoSansKR-Bold.ttf").get()['Body'] # File control logic. target_image = Image.open(handle_object) target_text = "그렇게 꿀꿀이가 되고...." target_font = ImageFont.truetype(font=font_object, size=80) # Set image transparency and text. target_image = insert_transparency_rectangle(image=target_image) target_image = insert_text_center(image=target_image, message=target_text, font=target_font) target_image_buffer = io.BytesIO() # Save as `png` into Byte buffer. target_image.save(target_image_buffer, "png") # Set buffer pointer to the beginning. target_image_buffer.seek(0) # Upload into your handling bucket. handle_bucket.upload_fileobj( target_image_buffer, os.path.join(handle_directory, f"result_{handle_filename}") ) return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda!') }
Python
복사

Test & CloudWatch

Lambda를 Deploy하더라도 Test를 실행하지 않으면 정상적으로 동작하지 않는듯 하여, Test를 실행해보도록 합니다. 정상적으로 동작하는지 여부는 CloudWatch에서 LogGroup 및 LogStream이 생성됐는지 확인하면 동작 여부를 확인할 수 있습니다.
테스트를 위해서 Event JSON을 잘 써주는 것도 좋습니다. 다만, 이는 어디까지나 CloudWatch를 통해 로그가 잘 생성됐는지 여부를 확인하기 위함이기 때문에 아무런 테스트 이벤트여도 무관합니다.
이제 테스트를 실행해보면 당연하게도 에러가 발생합니다. 중요한 건 CloudWatch에 정상적으로 로그가 생성됐는가입니다.
정상적으로 잘 에러가 났는지 들어가서 LogStream이 생성됐는지, 에러는 어떻게 나왔는지 확인해봅니다.

5. Log & S3

이제 정상적인 동작을 하는지 여부를 확인하여 글을 마치고자 합니다. 간단하게 테스트하기 위해서는 설정한 Bucket과 Directory에 원하는 이미지(PNG 등)을 업로드해봅니다.
S3서비스로 들어가 Upload 버튼을 이용하여 파일을 업로드함
혹은 aws cli를 이용하여 aws s3 cp ./give_me_more.png s3://[bucket-name]/[bucket-full-directory]/give_me_more.png 과 같은 명령어로 파일을 업로드 함
위와 같은 방법 중 원하는 걸 하나 선택하여 업로드해보고 S3에 파일이 생성됐는지 확인해봅니다. 저는 코드 내에 파일 이름을 저장할 때 deprecated-를 추가하여 저장하도록 약간의 수정을 해보았습니다. 결과는 다음과 같습니다.

5.1 Issue tracking

만약 동작을 제대로 하지 않는다면, Lambda의 기본 설정 중 Timeout 이슈일 가능성이 있습니다. 특히 이미지를 다루는 코드이기 때문에 실행 속도에도 영향이 갈 것이라 생각됩니다. 따라서 아래의 설정을 수정해주도록 합니다.
여기서 저는 Timeout을 10초로 늘려주었더니 이슈가 해결되었습니다. (5.2초 정도 걸리는데, Timeout 때문에 Lambda 코드가 중간에 삑 종료되기에..)
만약 타임아웃 이슈라면 위의 설정으로 해결이 가능합니다.

6. Conclusion

이번 글에서는 AWS Lambda를 이용하여 S3 Trigger를 연결하여 파일이 생성되었을 때 원하는 이미지를 새로 생성해주는 기능을 추가해보았습니다. 이번 글을 작성하기 위해 Lambda를 리서치하며, 마치 레고를 조립하듯 원하는 기능을 설정하거나 추가하는 것이 정말 흥미로웠습니다. 이번 글에서는 Trigger를 추가하는 것만 실습하였으나, 추후 destination 을 이용하여 어떤 기능을 추가할 수 있을지 연구해보는 글을 작성해보고 싶어집니다.

References