서버리스 인프라 개발 가이드
목차
- 서버리스 인프라 개발 가이드
서론
문서의 목적
본 문서는 Covos 플랫폼 백엔드 시스템에서 AWS Lambda를 효과적으로 활용하기 위한 기술 가이드입니다. 개발자들이 Lambda의 기본 개념부터 실제 생성, 설정, 배포 과정, 그리고 우리 플랫폼에서의 활용 사례와 모범 사례까지 이해하는 데 도움을 주는 것을 목표로 합니다. 이 가이드를 통해 Lambda 기반 기능 개발의 일관성과 효율성을 높이고자 합니다.
AWS Lambda란?
AWS Lambda는 서버를 프로비저닝하거나 관리할 필요 없이 코드를 실행할 수 있게 해주는 서버리스(Serverless) 컴퓨팅 서비스입니다. 코드를 업로드하기만 하면, Lambda는 높은 가용성으로 코드를 실행하고 확장하는 데 필요한 모든 작업을 처리합니다.
주요 특징:
- 서버리스: 인프라 관리에 대한 걱정 없이 코드 실행에만 집중할 수 있습니다.
- 자동 확장: 요청량에 따라 자동으로 컴퓨팅 용량을 확장하거나 축소합니다.
- 실행 기반 과금: 코드가 실행되는 시간(밀리초 단위)과 요청 수에 대해서만 비용을 지불합니다. 유휴 시간에는 비용이 발생하지 않습니다.
- 다양한 트리거 지원: S3 버킷 이벤트, HTTP 요청(API Gateway), 메시지 큐(MQ, Covos 플랫폼에서는 주로 AWS MQ for RabbitMQ를 사용하며, 이 외에도 Amazon SQS, Apache Kafka 등 다양한 MQ 서비스 지원), DynamoDB 테이블 업데이트 등 다양한 AWS 서비스 및 외부 이벤트에 응답하여 코드를 실행할 수 있습니다.
- 다양한 언어 지원: Node.js, Python, Java, C#, Go, Ruby 등 다양한 프로그래밍 언어를 지원합니다.
Covos 플랫폼에서는 주로 Python 런타임을 사용하여 Lambda 함수를 개발하였습니다.
Covos 플랫폼에 AWS Lambda를 도입한 핵심 이유
Covos 플랫폼은 사용자에게 빠르고 안정적인 서비스를 제공하기 위해 여러 기술적 고민을 해왔습니다. 그 과정에서 AWS Lambda는 다음과 같은 주요 문제 해결 및 시스템 개선을 위해 도입되었습니다.
Cloud Run 콜드 스타트(Cold Start) 문제 완화 및 비동기 처리
플랫폼의 핵심 기능 중 일부는 Google Cloud Run에서 서비스되고 있습니다. Cloud Run은 유연한 확장이 가능하지만, 요청이 없는 기간 이후 첫 요청 시 컨테이너를 새로 시작하는 데 시간이 소요되는 콜드 스타트(Cold Start) 현상이 발생할 수 있습니다. 이는 특히 사용자 경험에 민감한 음성 합성 요청 등에서 응답 지연으로 이어질 수 있습니다.
이를 해결하기 위해 Covos 플랫폼에서는 AWS MQ for RabbitMQ 시스템과 Lambda를 연동하는 아키텍처를 도입했습니다:
- 백엔드(Spring Boot)의 요청 접수: 사용자의 음성 합성 등의 요청을 백엔드 API가 받습니다.
- AWS MQ for RabbitMQ로 작업 전달: 백엔드는 요청을 직접 처리하는 대신, 필요한 정보(예: 사용자 ID, 텍스트 내용, 선택된 보이스팩 정보 등)를 담은 메시지를 AWS MQ for RabbitMQ의 지정된 큐에 발행합니다. 이 시점에서 백엔드는 사용자에게 “요청이 접수되었다”는 빠른 응답을 줄 수 있습니다.
- Lambda 트리거: 해당 AWS MQ for RabbitMQ 큐를 구독(trigger)하도록 설정된 AWS Lambda 함수가 새 메시지를 감지하고 자동으로 실행됩니다.
- 데이터 유효성 검사 및 처리 요청: Lambda 함수는 AWS MQ for RabbitMQ로부터 받은 메시지 내용을 바탕으로 입력 값의 유효성을 검사하고, 실제 작업(예: 음성 합성)을 수행할 Cloud Run 서비스의 엔드포인트를 호출합니다.
- 결과 처리: Cloud Run 서비스는 작업을 완료한 후, 결과를 S3에 저장하거나 다른 방식으로 백엔드에 알릴 수 있습니다. (콜백 URL 등)
이 방식을 통해 백엔드는 무거운 작업을 비동기적으로 위임하고 즉시 다음 요청을 처리할 수 있게 되어, 콜드 스타트의 영향을 최소화하고 사용자 경험을 개선합니다.
백엔드 시스템 부하 분산 및 응답성 향상
음성 합성, 보이스팩 생성 등 리소스를 많이 사용하거나 처리 시간이 오래 걸릴 수 있는 작업들을 Lambda와 AWS MQ for RabbitMQ를 통해 분리함으로써, 메인 백엔드 시스템의 부하를 크게 줄일 수 있습니다. 이는 전체 시스템의 안정성과 사용자 요청에 대한 평균 응답 시간을 향상시키는 데 기여합니다.
개발 유연성 및 서비스 확장 용이성
자주 사용되거나 독립적으로 관리될 수 있는 기능(예: 특정 포맷으로의 데이터 변환, 외부 AI 모델 호출 등)을 Lambda 함수로 모듈화하면, 코드의 재사용성이 높아집니다. 특히, “음성 합성”과 같이 여러 서비스에서 공통으로 필요한 기능을 하나의 Lambda 함수로 구현해두면, 신규 서비스(예: ‘오늘의 명언’, ‘리멤버 보이스’) 개발 시 해당 Lambda를 쉽게 활용하여 개발 속도를 높이고 일관된 품질을 보장할 수 있습니다. 이는 곧 서비스 확장성의 증대로 이어집니다.
AWS Lambda 함수 생성 및 설정 (AWS 콘솔 기준 상세 가이드)
이 섹션에서는 AWS Management Console을 사용하여 Covos 플랫폼에서 주로 사용하는 Python 기반 Lambda 함수를 생성하고 설정하는 과정을 단계별로 안내합니다. (Terraform 등의 IaC 도구는 본 문서에서 다루지 않습니다.)
사전 준비 사항
- 유효한 AWS 계정
- AWS Management Console 접근 권한
- Lambda 함수가 접근해야 할 다른 AWS 서비스(예: AWS MQ for RabbitMQ, S3, CloudWatch Logs)에 대한 이해
Lambda 함수 생성 단계
AWS Management Console 로그인
AWS 계정으로 AWS Management Console에 로그인합니다.
Lambda 서비스 대시보드 이동
상단 검색창에 “Lambda”를 입력하고 Lambda 서비스를 선택하여 대시보드로 이동합니다. 현재 리전(Region)이 올바르게 선택되어 있는지 확인합니다. (예: ap-northeast-2
서울 리전)
“함수 생성” 선택
Lambda 대시보드에서 “함수 생성” 버튼을 클릭합니다.
생성 옵션 선택
Covos 플랫폼에서는 주로 “새로 작성” 옵션을 사용합니다.
- 새로 작성: 기본적인 Lambda 함수를 처음부터 만듭니다.
- 블루프린트 사용: 사전 구성된 샘플 코드를 기반으로 시작합니다.
- 컨테이너 이미지: Docker 컨테이너 이미지를 Lambda 함수로 배포합니다. (고급 사용 사례)
- 서버리스 애플리케이션 리포지토리에서 찾아보기: AWS Serverless Application Repository의 애플리케이션을 배포합니다.
“새로 작성”을 선택합니다.
기본 정보 입력
- 함수 이름: Lambda 함수를 식별할 수 있는 고유한 이름을 입력합니다. (예:
covos-synthesis-processor
,covos-daily-quote-generator
) 명명 규칙을 따르는 것이 좋습니다. - 런타임: 함수 코드를 작성할 프로그래밍 언어 및 버전을 선택합니다. Covos 플랫폼에서는 주로
Python 3.9
또는 최신 안정 버전을 사용합니다. - 아키텍처: 함수의 컴퓨터 아키텍처를 선택합니다. 대부분의 경우
x86_64
가 기본이며, 특정 라이브러리 요구사항에 따라arm64
를 고려할 수 있습니다. (일반적으로x86_64
사용)
권한 설정
Lambda 함수는 다른 AWS 서비스와 상호작용하기 위해 IAM(Identity and Access Management) 역할을 통해 권한을 부여받습니다.
- AWS 기본 Lambda 권한을 사용하여 새 역할 생성: 가장 간단한 방법으로, CloudWatch Logs에 로그를 작성할 수 있는 기본 권한을 가진 새 IAM 역할을 자동으로 생성합니다. (예:
함수이름-role-랜덤문자열
) - 기존 역할 사용: 미리 정의된 IAM 역할이 있다면 선택합니다.
- 템플릿에서 새 역할 생성: 특정 AWS 서비스 접근 권한 템플릿을 기반으로 역할을 생성합니다.
중요: 초기에는 “AWS 기본 Lambda 권한을 사용하여 새 역할 생성”을 선택하고, 이후 함수에 필요한 최소한의 권한(예: AWS MQ for RabbitMQ 메시지 읽기/삭제, S3 버킷 읽기/쓰기, 다른 Lambda 호출 등)을 해당 역할에 명시적으로 추가하는 것이 보안상 모범 사례입니다. 예를 들어 AWS MQ for RabbitMQ(특히 VPC 내에서 실행되는 경우)를 트리거로 사용한다면, Lambda 함수에는 AWSLambdaVPCAccessExecutionRole
또는 이에 준하는 VPC 접근 권한이 필요하며, 추가적으로 RabbitMQ 브로커에 대한 인증 및 네트워크 구성이 필요합니다. S3에 접근해야 한다면 해당 S3 버킷에 대한 s3:GetObject
, s3:PutObject
등의 권한이 필요합니다. (사용하는 AWS MQ for RabbitMQ 구성에 따라 필요한 IAM 권한 정책을 적용해야 합니다.)
모든 정보를 입력한 후 “함수 생성” 버튼을 클릭합니다.
함수 구성
함수가 생성되면 해당 함수의 구성 페이지로 이동합니다.
코드 소스
Lambda 함수 코드를 작성하고 배포하는 부분입니다.
- 인라인 편집기: 콘솔에서 직접 코드를 작성하거나 수정할 수 있습니다. 간단한 함수나 빠른 테스트에 유용하지만, 코드 크기가 3MB를 초과하거나 외부 라이브러리가 필요한 경우 제한적입니다.
- .zip 파일 업로드: 로컬에서 개발하고 의존성과 함께 압축한 .zip 파일을 업로드합니다. Covos 플랫폼에서 Python 함수를 배포할 때 주로 사용되는 방식입니다. (의존성 관리는 Lambda Layer를 통해 더 효율적으로 할 수 있습니다.)
- Amazon S3에서 .zip 파일 업로드: .zip 파일을 S3 버킷에 업로드하고 해당 경로를 지정합니다.
기본 Python 핸들러 함수 구조:
import json
import os
# 환경 변수 사용 예시
# CLOUD_RUN_ENDPOINT = os.environ.get('CLOUD_RUN_ENDPOINT')
def lambda_handler(event, context):
"""
Lambda 함수의 메인 핸들러입니다.
AWS MQ for RabbitMQ 이벤트의 경우, 'event' 객체는 메시지 정보를 담고 있습니다.
(실제 이벤트 구조는 사용 중인 AWS MQ for RabbitMQ 구성 및 Lambda 통합 방식에 따라 다를 수 있습니다.)
"""
print("Received event: " + json.dumps(event, indent=2))
try:
# 일반적으로 AWS MQ for RabbitMQ 메시지는 'Records'는 메시지 리스트일 수 있음
# 사용하는 AWS MQ for RabbitMQ 구성에 맞게 파싱 필요
for record in event.get('Records', []):
# AWS MQ for RabbitMQ 메시지 본문은 'data' 필드에 base64 인코딩된 형태로 있을 수 있습니다. 디코딩 후 JSON 파싱이 필요할 수 있습니다.
payload_str_b64 = record.get('data')
if not payload_str_b64:
continue
# import base64
# payload_str = base64.b64decode(payload_str_b64).decode('utf-8')
# payload = json.loads(payload_str)
payload = json.loads(base64.b64decode(payload_str_b64).decode('utf-8')) # 직접 디코딩 및 파싱
job_id = payload.get('jobId')
text_to_synthesize = payload.get('text')
voice_model_id = payload.get('voiceModelId')
# ... 기타 파라미터
try:
# 1. (필요시) DB에서 voiceModelId 관련 추가 정보 조회
# 2. Cloud Run 음성 합성 서비스 호출
# synthesis_request_payload = {
# "text": text_to_synthesize,
# "model": voice_model_id,
# # ... 기타 필요한 파라미터
# }
# response = requests.post(CLOUD_RUN_SYNTHESIS_URL, json=synthesis_request_payload, timeout=60)
# response.raise_for_status() # 오류 시 예외 발생
# synthesis_result = response.json() # 예: {"audioS3Path": "s3://bucket/path/to/audio.mp3"}
# 임시 결과 (실제로는 Cloud Run 호출)
audio_file_name = f"{job_id}.mp3"
# s3_client = boto3.client('s3')
# s3_client.put_object(Bucket=S3_BUCKET_NAME, Key=f"results/{audio_file_name}", Body=b"dummy_audio_content")
# audio_s3_path = f"s3://{S3_BUCKET_NAME}/results/{audio_file_name}"
audio_s3_path = f"https://{os.environ.get('S3_BUCKET_NAME', 'your-s3-bucket')}.s3.amazonaws.com/results/{audio_file_name}" # Public URL 예시
# 3. (필요시) 결과 DB에 업데이트 또는 콜백 호출
# db_update_status(job_id, "SUCCESS", audio_s3_path)
# requests.post(payload.get('callbackUrl'), json={"jobId": job_id, "status": "SUCCESS", "resultUrl": audio_s3_path})
print(f"Job {job_id} processed successfully. Audio at {audio_s3_path}")
except Exception as e:
print(f"Error processing job {job_id}: {e}")
# db_update_status(job_id, "FAILURE", str(e))
# requests.post(payload.get('callbackUrl'), json={"jobId": job_id, "status": "FAILURE", "error": str(e)})
# DLQ로 보내기 위해 예외를 다시 발생시킬 수도 있음
raise e
- 환경 변수 활용:
CLOUD_RUN_SYNTHESIS_URL
,S3_BUCKET_NAME
등을 환경 변수로 설정하여 유연하게 관리합니다. - Layer 활용:
requests
와 같은 외부 HTTP 클라이언트 라이브러리나boto3
의 특정 버전이 필요하다면 Layer로 관리합니다.
트리거 설정 (MQ 예시)
Lambda 함수를 실행시키는 이벤트를 설정합니다. Covos 플랫폼에서는 AWS MQ for RabbitMQ 서비스의 메시지를 트리거로 사용합니다. 아래는 AWS MQ for RabbitMQ 를 예시로 들어 트리거를 설정하는 방법이며, 다른 유형의 메시지 브로커(예: Apache Kafka, Amazon SQS 등)를 사용한다면 해당 서비스의 설명서를 참조하여 트리거 설정을 진행해야 합니다.
AWS MQ for RabbitMQ를 트리거로 설정하는 경우:
- 함수 구성 페이지의 “함수 개요”에서 “+ 트리거 추가” 클릭.
- “트리거 구성”에서 소스로 “Amazon MQ”를 선택합니다.
- Amazon MQ 브로커: Lambda 함수를 트리거할 AWS MQ 브로커를 선택합니다.
- 소스 ARN (큐 ARN): 트리거로 사용할 RabbitMQ 큐의 ARN을 지정합니다.
- 가상 호스트: RabbitMQ 브로커의 가상 호스트를 지정합니다 (기본값: /).
- 배치 크기: 한 번의 호출로 Lambda가 RabbitMQ 큐에서 가져올 최대 메시지 수입니다 (AWS MQ for RabbitMQ의 경우 일반적으로 1부터 10 사이, 브로커 구성 및 Lambda 설정에 따라 다를 수 있음).
- 배치 기간 (Batch window): 새 레코드를 수집하여 단일 배치로 처리하기 전에 Lambda가 대기하는 최대 시간(초)입니다.
- 추가 설정 (선택 사항): 동시성, 재시도 정책, 데드 레터 큐(DLQ) 등을 설정할 수 있습니다. RabbitMQ의 DLX(Dead Letter Exchange)와 연동하여 DLQ 기능을 구현할 수 있습니다.
- “추가” 클릭.
이제 설정된 AWS MQ for RabbitMQ에 메시지가 도착하면 (또는 구성된 이벤트 조건이 충족되면) 이 Lambda 함수가 자동으로 실행됩니다.
기본 설정 (메모리, 제한 시간)
“구성” 탭 > “일반 구성” 섹션에서 편집 가능합니다.
- 메모리 (MB): 함수에 할당되는 메모리 양입니다 (128MB ~ 10,240MB). 메모리가 많을수록 CPU 성능도 비례하여 향상됩니다. 함수 실행에 필요한 만큼 적절히 설정하여 비용을 최적화합니다.
- 제한 시간: Lambda 함수가 실행될 수 있는 최대 시간입니다 (최대 15분 = 900초). 이 시간을 초과하면 함수가 강제 종료됩니다. 예상 실행 시간보다 충분히 길게 설정하되, 불필요하게 길게 설정하면 오류 시 비용이 더 나올 수 있습니다.
테스트 및 배포
테스트 이벤트 구성 및 실행
Lambda 콘솔에서 직접 함수를 테스트할 수 있습니다.
- 함수 구성 페이지 상단의 “테스트(Test)” 탭 선택.
- “새 이벤트 생성” 선택.
- 이벤트 이름 입력.
- 이벤트 템플릿: 다양한 서비스의 이벤트 형식을 선택할 수 있습니다. AWS MQ for RabbitMQ의 경우, 사용 중인 AWS MQ for RabbitMQ 구성에 맞는 이벤트 템플릿을 사용하거나 직접 JSON을 작성합니다. 실제 AWS MQ for RabbitMQ 메시지 구조를 반영해야 합니다.
// AWS MQ for RabbitMQ 이벤트 예시 (여러 메시지를 포함하는 경우): // 중요: 이 구조는 매우 일반화된 예시입니다. // 실제 Lambda가 수신하는 이벤트 구조는 사용하는 AWS MQ for RabbitMQ 구성 및 Lambda 통합 방식에 따라 매우 다릅니다. // 반드시 해당 AWS MQ for RabbitMQ 구성 및 Lambda 트리거 이벤트 문서를 참조하여 // 실제 수신될 이벤트 구조를 확인하고 코드를 작성해야 합니다. { "eventSourceIdentifier": "name-or-arn-of-the-mq-source", // 예: 큐 이름, 토픽 이름 등 "messages": [ // 또는 "Records", "data" 등, 메시지 배열을 나타내는 키 { "messageId": "unique-id-for-message-1", "messageBody": "{\"jobId\": \"job-123\", \"text\": \"첫 번째 메시지 내용입니다.\"}", // 실제 메시지 페이로드 (종종 JSON 문자열) "messageAttributes": { // 사용자 정의 속성 또는 추가 메타데이터 (선택적) "contentType": "application/json", "priority": "high" }, "receivedTimestamp": "2023-10-27T12:00:00Z" // Lambda가 메시지를 받은 시간 (추정) }, { "messageId": "unique-id-for-message-2", "messageBody": "{\"jobId\": \"job-456\", \"text\": \"두 번째 메시지입니다.\"}", "messageAttributes": { "contentType": "text/plain" }, "receivedTimestamp": "2023-10-27T12:00:05Z" } // ... 추가 메시지들 ] }
- “저장” 후 “테스트” 버튼 클릭.
실행 결과, 로그 출력, 요약 정보(실행 시간, 사용 메모리 등)를 확인할 수 있습니다.
CloudWatch Logs를 통한 로깅 확인
Lambda 함수 내에서 print()
(Python) 또는 console.log()
(Node.js) 등으로 출력하는 모든 내용은 AWS CloudWatch Logs에 자동으로 기록됩니다.
- “모니터링(Monitor)” 탭 > “CloudWatch 로그 보기” 클릭.
- 해당 Lambda 함수의 로그 그룹(/aws/lambda/함수이름)으로 이동하여 로그 스트림을 확인할 수 있습니다.
- 디버깅 및 실행 추적에 매우 중요합니다.
버전 관리 및 별칭 (간략 소개)
- 버전: 함수 코드와 설정을 변경하고 “게시”하면 새 버전이 생성됩니다 (예:
$LATEST
,1
,2
). 버전은 불변입니다. - 별칭(Alias): 특정 버전을 가리키는 포인터입니다 (예:
PROD
,DEV
). 클라이언트는 별칭을 통해 함수를 호출함으로써, 코드 변경 없이 버전을 쉽게 전환할 수 있습니다 (예: 블루/그린 배포).
Covos 플랫폼에서의 AWS Lambda 활용
주요 Lambda 활용 서비스
Covos 플랫폼은 다양한 기능에서 AWS Lambda를 효과적으로 사용하고 있습니다. 대표적인 예는 다음과 같습니다:
- 보이스팩 생성 처리: 사용자가 제공한 데이터를 기반으로 커스텀 보이스팩을 생성하는 백그라운드 작업의 일부.
- 베이직 보이스 음성 합성: 사용자가 입력한 텍스트를 표준 목소리로 합성.
- AI 리포터 음성 합성: 뉴스 기사나 특정 텍스트를 AI 리포터 스타일로 변환.
- 오늘의 명언 음성 제공: 매일 선정된 명언을 음성으로 변환하여 제공.
- 리멤버 보이스 (가칭): 사용자의 소중한 기록이나 메시지를 음성으로 재생하는 기능.
핵심 아키텍처: MQ 연동을 통한 비동기 처리
앞서 Cloud Run 콜드 스타트(Cold Start) 문제 완화 및 비동기 처리에서 설명했듯이, Covos 플랫폼 Lambda 활용의 핵심은 AWS MQ for RabbitMQ와의 연동입니다.
처리 흐름 요약:
- 사용자 요청: 클라이언트(앱/웹)에서 백엔드 API(Spring Boot)로 서비스 요청 (예: 음성 합성).
- 백엔드 작업 발행: 백엔드 API는 요청을 직접 처리하는 대신, 필요한 정보(예: 사용자 ID, 텍스트 내용, 선택된 보이스팩 정보 등)를 담은 메시지를 AWS MQ for RabbitMQ의 지정된 큐에 발행합니다. 즉시 사용자에게 “처리 시작” 응답 전달.
- Lambda 실행: AWS MQ for RabbitMQ의 특정 큐를 트리거로 설정된 Lambda 함수가 새 메시지를 감지하고 실행됨.
- Lambda 작업 수행:
- AWS MQ for RabbitMQ 메시지에서 작업 데이터 파싱 (예:
userId
,textToSynthesize
,voicePackId
). - 필요시 추가 데이터 조회 (예: DB에서
voicePackId
에 해당하는 보이스팩 정보). - 실제 작업을 수행할 서비스(예: Google Cloud Run에서 실행 중인 음성 합성 모델)에 HTTP 요청.
- AWS MQ for RabbitMQ 메시지에서 작업 데이터 파싱 (예:
- 결과 저장 및 알림:
- Cloud Run 서비스는 음성 합성 결과를 S3와 같은 스토리지에 저장.
- 결과 정보(예: S3 URL)를 Lambda에 반환.
- Lambda는 작업 완료 상태 및 결과 위치를 DB에 업데이트하거나, 다른 MQ 또는 콜백을 통해 백엔드에 알릴 수 있음.
이러한 비동기 아키텍처는 사용자 경험을 향상시키고 시스템의 탄력성과 확장성을 높입니다.
예시: 음성 합성 Lambda의 작동 방식
covos-synthesis-lambda
라는 이름의 음성 합성 전용 Lambda 함수가 있다고 가정해 봅시다.
- 트리거: AWS MQ for RabbitMQ 브로커 내의 특정 큐(예:
synthesis-request-queue
)에 메시지가 도착하면 실행됩니다. - 입력 메시지 (AWS MQ for RabbitMQ 메시지 페이로드 예시, JSON 형식으로 가정):
{ "jobId": "unique-job-id-123", "userId": "user-abc", "text": "오늘 날씨 정말 좋네요.", "voiceModelId": "voice-model-xyz", "callbackUrl": "https://api.covos.com/synthesis/callback" }
- Lambda 주요 로직 (Python pseudo-code):
# lambda_function.py import json import os import requests # Layer를 통해 추가된 라이브러리 # import boto3 # S3, DynamoDB 등 사용 시 # CLOUD_RUN_SYNTHESIS_URL = os.environ.get('CLOUD_RUN_SYNTHESIS_URL') # S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME') def lambda_handler(event, context): # AWS MQ for RabbitMQ 이벤트는 보통 단일 메시지 또는 메시지 리스트를 포함할 수 있으며, 구조는 Lambda 통합 설정에 따라 다릅니다. # 예시: event.get('messages') 또는 event directamente 메시지 객체일 수 있음 # messages = event.get('rmqMessagesByQueue', {}).get('YOUR_QUEUE_NAME::YOUR_VIRTUAL_HOST', []) # 실제 이벤트 구조 확인 필요 # 위 라인은 매우 구체적인 이벤트 구조를 가정하므로, 더 일반적인 접근은 아래와 같습니다. # print("Raw event:", json.dumps(event)) # 우선 단일 메시지 또는 메시지 리스트를 처리할 수 있도록 event 구조를 확인해야 합니다. # 아래는 event가 메시지 리스트를 담고 있는 'messages' 키를 가지고 있다고 가정합니다. # 실제 RabbitMQ 이벤트 구조는 Lambda 트리거 설정 및 메시지 발행 방식에 따라 다릅니다. actual_messages = [] if isinstance(event, list): actual_messages = event # 이벤트 자체가 메시지 리스트일 경우 elif isinstance(event, dict) and 'messages' in event: actual_messages = event['messages'] # 'messages' 키에 메시지 리스트가 있는 경우 elif isinstance(event, dict) and 'rmqMessagesByQueue' in event: # AWS 콘솔 테스트 이벤트와 유사한 구조 # 이 구조는 { "queue_name::vhost": [ { "basicProperties": {...}, "data": "..." }, ... ] } 형태를 가질 수 있습니다. # 실제 사용하는 큐 이름과 가상 호스트로 대체해야 합니다. # 여기서는 첫 번째 큐의 메시지만 가져오는 예시입니다. for queue_key, msgs_in_queue in event['rmqMessagesByQueue'].items(): actual_messages.extend(msgs_in_queue) break # 하나의 큐만 처리하는 예시 elif isinstance(event, dict) and 'data' in event: # 단일 메시지 이벤트로 'data' 필드만 있는 경우 actual_messages.append(event) for message_data_obj in actual_messages: payload_str_b64 = message_data_obj.get('data') if not payload_str_b64: print("Warning: Message data is empty or not found.") continue try: import base64 payload_str = base64.b64decode(payload_str_b64).decode('utf-8') payload = json.loads(payload_str) except Exception as parse_error: print(f"Error decoding/parsing message data: {parse_error}") print(f"Original b64 data: {payload_str_b64}") continue job_id = payload.get('jobId') text_to_synthesize = payload.get('text') voice_model_id = payload.get('voiceModelId') # ... 기타 파라미터 print(f"Processing job_id: {job_id}") try: # 1. (필요시) DB에서 voiceModelId 관련 추가 정보 조회 # 2. Cloud Run 음성 합성 서비스 호출 # CLOUD_RUN_SYNTHESIS_URL_actual = os.environ.get('CLOUD_RUN_SYNTHESIS_URL') # S3_BUCKET_NAME_actual = os.environ.get('S3_BUCKET_NAME') # if not CLOUD_RUN_SYNTHESIS_URL_actual or not S3_BUCKET_NAME_actual: # print("Error: CLOUD_RUN_SYNTHESIS_URL or S3_BUCKET_NAME environment variables not set.") # continue # synthesis_request_payload = { # "text": text_to_synthesize, # "model": voice_model_id, # # ... 기타 필요한 파라미터 # } # response = requests.post(CLOUD_RUN_SYNTHESIS_URL_actual, json=synthesis_request_payload, timeout=60) # response.raise_for_status() # 오류 시 예외 발생 # synthesis_result = response.json() # 예: {"audioS3Path": "s3://bucket/path/to/audio.mp3"} # 임시 결과 (실제로는 Cloud Run 호출) audio_file_name = f"{job_id or 'unknown_job'}.mp3" # s3_client = boto3.client('s3') # s3_client.put_object(Bucket=S3_BUCKET_NAME_actual, Key=f"results/{audio_file_name}", Body=b"dummy_audio_content") # audio_s3_path = f"s3://{S3_BUCKET_NAME_actual}/results/{audio_file_name}" S3_BUCKET_FOR_URL = os.environ.get('S3_BUCKET_NAME', 'your-s3-bucket') # 환경 변수에서 S3 버킷 이름 가져오기 audio_s3_path = f"https://{S3_BUCKET_FOR_URL}.s3.amazonaws.com/results/{audio_file_name}" # Public URL 예시 # 3. (필요시) 결과 DB에 업데이트 또는 콜백 호출 # db_update_status(job_id, "SUCCESS", audio_s3_path) # if payload.get('callbackUrl'): # requests.post(payload.get('callbackUrl'), json={"jobId": job_id, "status": "SUCCESS", "resultUrl": audio_s3_path}) print(f"Job {job_id} processed successfully. Audio at {audio_s3_path}") except Exception as e: print(f"Error processing job {job_id}: {e}") # db_update_status(job_id, "FAILURE", str(e)) # if payload.get('callbackUrl'): # requests.post(payload.get('callbackUrl'), json={"jobId": job_id, "status": "FAILURE", "error": str(e)}) # DLQ로 보내기 위해 예외를 다시 발생시킬 수도 있음 raise e # 처리 실패 시 메시지를 DLQ로 보내려면 여기서 예외를 다시 발생시키거나 Lambda 구성에서 DLQ 설정
- 환경 변수 활용:
CLOUD_RUN_SYNTHESIS_URL
,S3_BUCKET_NAME
등을 환경 변수로 설정하여 유연하게 관리합니다. - Layer 활용:
requests
와 같은 외부 HTTP 클라이언트 라이브러리나boto3
의 특정 버전이 필요하다면 Layer로 관리합니다.
AWS Lambda의 주요 장점 및 Covos 플랫폼 적용 효과
AWS Lambda를 도입함으로써 Covos 플랫폼은 다음과 같은 실질적인 이점을 얻고 있습니다.
뛰어난 확장성과 재사용성
- 범용 Lambda 개발:
음성 합성 Lambda
와 같이 핵심적이고 반복적으로 사용되는 기능을 하나의 잘 정의된 Lambda 함수로 개발합니다. - 다양한 서비스에 적용: 이렇게 개발된 범용 Lambda는 ‘오늘의 명언 음성 제공’, ‘리멤버 보이스’, ‘AI 리포터’ 등 여러 신규 또는 기존 서비스에서 쉽게 재사용될 수 있습니다. 이는 새로운 기능을 추가할 때마다 유사한 로직을 중복 개발할 필요성을 없애고, 개발 생산성을 크게 향상시킵니다.
- 일관성 유지: 공통 기능을 한 곳에서 관리하므로, 기능 개선이나 버그 수정 시 모든 관련 서비스에 일관된 업데이트를 적용하기 용이합니다.
비용 효율성
Lambda는 코드가 실행되는 시간에 대해서만 밀리초 단위로 비용을 지불하고, 요청 수에 따라 과금됩니다. 유휴 상태에서는 비용이 발생하지 않습니다. Covos 플랫폼의 많은 비동기 작업들이 이러한 특성에 잘 부합합니다.
서버 관리 부담 대폭 감소
서버리스 아키텍처의 핵심 이점 중 하나는 인프라 관리에 대한 부담이 거의 없다는 것입니다. 운영체제 패치, 서버 프로비저닝, 로드 밸런싱, 자동 확장 설정 등 기존 서버 기반 환경에서 필요했던 많은 관리 작업들이 AWS에 의해 자동으로 처리됩니다. 이를 통해 개발팀은 핵심 비즈니스 로직 개발과 서비스 품질 향상에 더 집중할 수 있습니다.
독립적인 개발, 배포 및 업데이트
각 Lambda 함수는 독립적인 단위로 개발, 테스트, 배포, 업데이트될 수 있습니다. 이는 마이크로서비스 아키텍처의 장점과 유사하게, 특정 기능의 변경이 다른 시스템 영역에 미치는 영향을 최소화하고, 팀별로 기능을 나누어 병렬적으로 개발하는 것을 용이하게 합니다.
단일 책임 원칙 적용 용이
Lambda 함수는 일반적으로 특정 작업이나 기능 하나에 집중하도록 설계됩니다 (단일 책임 원칙, Single Responsibility Principle). 이는 코드의 가독성, 테스트 용이성, 유지보수성을 높입니다. 예를 들어, ‘MQ 메시지 수신 및 파싱’, ‘외부 API 호출’, ‘S3에 결과 저장’ 등의 작은 단위로 기능을 분리하여 각기 다른 Lambda로 구성하거나, 하나의 Lambda 내에서도 명확히 구분된 로직으로 구현할 수 있습니다.
Covos 플랫폼 Lambda 개발 모범 사례
Covos 플랫폼에서 AWS Lambda 함수를 개발하고 운영할 때 따르는 주요 모범 사례는 다음과 같습니다.
함수는 상태 비저장(Stateless)으로 작성
Lambda 함수는 호출될 때마다 새로운 실행 환경에서 시작될 수 있다는 가정 하에 개발해야 합니다. 즉, 함수 실행 간에 로컬 디스크나 메모리에 상태를 저장하려고 해서는 안 됩니다. 필요한 모든 상태 정보는 외부 지속성 저장소(예: DynamoDB, S3, RDS)에 보관하거나, 함수 호출 시 파라미터로 전달받아야 합니다.
IAM 역할에 최소한의 필요 권한만 부여
Lambda 함수에 연결된 IAM 역할은 해당 함수가 작업을 수행하는 데 필요한 최소한의 권한만 가져야 합니다 (최소 권한 원칙, Principle of Least Privilege). 예를 들어 AWS MQ for RabbitMQ를 VPC 내에서 사용하는 경우, Lambda 함수에는 AWSLambdaVPCAccessExecutionRole
또는 이에 준하는 VPC 접근 권한이 필요합니다. 또한, RabbitMQ 브로커에 대한 네트워크 접근(보안 그룹 설정 등)과 필요한 경우 RabbitMQ 사용자 자격 증명(Secrets Manager를 통해 안전하게 전달)이 구성되어야 합니다. 불필요한 다른 서비스 접근 권한은 포함하지 않도록 합니다. (플랫폼에서 사용하는 AWS MQ for RabbitMQ 구성에 따라 필요한 IAM 권한 정책을 적용해야 합니다.)
민감 정보 및 설정은 환경 변수 활용
API 키, 데이터베이스 접속 정보, 외부 서비스 엔드포인트와 같은 민감하거나 변경될 수 있는 정보는 코드에 직접 하드코딩하지 않고 Lambda 함수의 환경 변수를 통해 주입합니다. 필요한 경우 AWS Secrets Manager나 Parameter Store와 연동하여 더욱 안전하게 관리합니다.
강력한 오류 처리 및 상세 로깅
- 오류 처리:
try-except
(Python) 블록을 사용하여 예상되는 오류와 예기치 않은 오류를 모두 적절히 처리합니다. MQ 트리거의 경우, 처리 실패 시 메시지가 데드 레터 큐(DLQ)로 이동하도록 설정하거나, 재시도 로직을 신중하게 구현해야 합니다. - 상세 로깅:
print()
문이나 로깅 라이브러리(예: Python의logging
모듈)를 사용하여 함수의 주요 실행 단계, 입력 값, 중요한 변수, 오류 정보 등을 CloudWatch Logs에 상세히 기록합니다. 이는 디버깅 및 문제 해결에 매우 중요합니다. JSON 형식으로 구조화된 로그를 남기면 CloudWatch Logs Insights에서 분석하기 용이합니다.
Lambda Layer를 통한 의존성 효율적 관리
공통 라이브러리나 pandas
, requests
와 같이 크기가 큰 패키지는 Lambda Layer로 분리하여 관리합니다. 이는 배포 패키지 크기를 줄여 배포 속도를 높이고, 여러 함수에서 동일한 라이브러리를 공유하여 관리 효율성을 증대시킵니다.
함수 타임아웃 및 메모리 설정 최적화
- 타임아웃: 함수의 예상 실행 시간보다 약간의 여유를 두고 설정합니다. 너무 짧으면 정상적인 작업도 실패할 수 있고, 너무 길면 오류 발생 시 불필요한 비용이 발생할 수 있습니다.
- 메모리: 함수가 필요로 하는 최소한의 메모리를 할당합니다. CloudWatch Logs에서
REPORT
로그를 통해 실제 사용된 메모리 양을 확인하고 최적화합니다. 메모리 증가는 CPU 성능도 함께 향상시키므로, CPU 집약적인 작업의 경우 메모리를 늘려 실행 시간을 단축시키는 것이 비용 효율적일 수 있습니다.
MQ 트리거 사용 시 고려 사항
- 배치 처리: AWS MQ for RabbitMQ를 Lambda 트리거로 사용하는 경우, 배치 크기(Batch Size)와 배치 기간(Batch Window)을 적절히 설정하여 효율성을 높일 수 있습니다.
- 멱등성(Idempotency): 네트워크 문제 등으로 인해 동일한 메시지가 여러 번 처리될 가능성에 대비하여, 함수 로직이 멱등성을 가지도록 설계하는 것이 중요합니다. 즉, 동일한 입력으로 여러 번 호출되어도 결과가 동일하게 유지되어야 합니다. (예: 이미 처리된 작업인지 DB를 통해 확인)
- 데드 레터 큐(DLQ): 반복적으로 처리 실패하는 메시지를 격리하여, 정상적인 메시지 처리에 영향을 주지 않도록 MQ 시스템에 DLQ를 설정합니다.
결론
AWS Lambda는 Covos 플랫폼의 백엔드 아키텍처에서 핵심적인 역할을 수행하며, 특히 비동기 처리, 시스템 부하 분산, 서비스 확장성 확보 측면에서 큰 기여를 하고 있습니다. 특히 AWS MQ for RabbitMQ와의 연동을 통해 Cloud Run과 같은 다른 서비스의 단점을 보완하고, 동시에 서버리스의 이점을 최대한 활용하여 개발 및 운영 효율성을 높였습니다.
본 가이드에서 제시된 Lambda 생성 방법, 플랫폼 내 활용 사례, 그리고 개발 모범 사례들을 잘 숙지하고 따르면, 개발자들은 더욱 안정적이고 효율적인 기능을 빠르게 구축할 수 있을 것입니다. 앞으로도 Covos 플랫폼은 Lambda와 같은 현대적인 클라우드 기술을 적극적으로 도입하고 활용하여, 사용자에게 더 나은 가치를 제공하기 위해 지속적으로 발전해 나갈 것입니다.