Link: https://cdk.awsdemo.kr/serverless201
목차
2021-06-01 ver 3.0 업데이트 예정 내용
2021-04-08 ver 2.2 업데이트
2019-07-31 ver 2.1 업데이트
2019-03-06 ver 2.0 업데이트
|
일반적으로 텍스트를 읽는 것은 쉽지 않습니다. 애플리케이션이 문장에 있는 각각의 문자를 읽는 것만으로는 의미가 있다고 볼 수 없습니다. 예를 들어, 텍스트를 음성으로 변환하는(text-to-speech) 애플리케이션은 다음과 같은 공통적인 문제가 있습니다:
Amazon Polly는 이러한 문제를 해결하는 음성 합성 기능을 제공합니다. 따라서 위와 같이 텍스트를 음성으로 변환하는 어려움을 해결하기 위한 노력 대신 애플리케이션 개발 자체에 주력할 수 있습니다.
Amazon Polly는 텍스트를 생생한 음성으로 변환시킵니다. 자연스럽게 말하는 애플리케이션을 만들 수 있으므로 완전히 새로운 범주의 음성을 사용하는 서비스를 만들 수 있습니다. Amazon Polly는 딥 러닝 학습 기술을 사용하여 사람 목소리처럼 들리는 음성을 합성하는 Amazon AI 서비스입니다. 29 개국 언어(한국어 포함)로 63 가지(한국어 서연 목소리 포함)의 목소리가 포함되어 있어 이상적인 목소리를 선택하여 다양한 국가에서 사용할 수 있는 음성을 제공하는 애플리케이션을 제작할 수 있습니다. (2021년 04월 12일 기준)
또한 Amazon Polly는 실시간 대화를 지원하는 데 요구되는 일관성 있는 빠른 응답 시간을 제공합니다. 오프라인 재생 또는 재배포를 위해 Polly의 오디오 파일을 캐시하고 저장할 수 있습니다. (즉, 재활용이 가능하므로 추가 과금이 발생하지 않습니다.) 그리고 Polly는 사용하기 쉽습니다. 음성으로 변환하려는 텍스트를 Amazon Polly API에 실어 보내기만 하면 됩니다. Amazon Polly는 오디오 스트림을 애플리케이션에 즉시 반환하므로 애플리케이션에서 직접 재생하거나 MP3와 같은 표준 오디오 파일 형식으로 저장할 수 있습니다.
이 블로그는 Amazon Polly를 사용하여 텍스트를 음성으로 변환하는 서버리스 서비스 기반 애플리케이션을 구축하는 방법을 소개합니다. 이 애플리케이션은 텍스트를 여러 언어로 받아 들인 다음 웹 브라우저에서 재생할 수있는 오디오 파일로 변환하는 간단한 사용자 인터페이스를 제공합니다. 여기서는 블로그 게시물을 사용하지만 모든 유형의 텍스트를 사용할 수 있습니다. 예를 들어, 차를 운전하거나 자전거를 타는 동안 뉴스 기사 또는 책을 읽거나, 식사를 준비하는 동안 조리법을 읽어주는 애플리케이션에 사용할 수 있습니다.
기존 Amazon Polly를 통한 음성 읽기 서버리스 앱 개발하기 블로그와 동일한 기능을 수행하는 실습입니다
Amazon Polly(TTS) 서비스가 비동기 방식을 지원하고, 한자를 한글로 변환하는 기능이 개선되어 해당 실습에서는 삭제 하였습니다.
아래 다이어그램은 이 애플리케이션의 아키텍처를 보여 줍니다. 프로비저닝, 패치, 확장에 대해서 고민할 필요가 없는, 즉 서버 작업이 필요하지 않는 서버리스 서비스로 구성합니다. 클라우드가 자동으로 이를 관리하기 때문에 우리는 애플리케이션 개발에만 집중할 수 있습니다.
이 애플리케이션은 크게 세 가지 서비스를 제공합니다.
게시물 등록, 검색, 삭제는 Amazon API Gateway를 통해 RESTful 웹 서비스로 제공됩니다. 로직은 Lambda 서비스를 통해서 구현되며 애플리케이션이 어떻게 상호 작용 하는지 살펴 보겠습니다.
아래 실습을 직접 수행하려면 Amazon Polly 서비스가 제공되는 지역을 선택하십시오. (이 예제는 Seoul 리전에서 English 언어 설정 상태에서 진행합니다.)
실습은 다음과 같은 순서로 진행됩니다.
리전을 서울(우측 상단)로 선택하고, 언어는 영어로 진행(좌측 하단)
DynamoDB는 게시물 정보와 생성된 MP3의 URL 메타 정보를 저장합니다.
NoSQL인 DynamoDB를 사용하므로 스키마를 사전에 정의하지 않고, 사용하게 될 속성 값이 어떤 것이 있는지 살펴 보겠습니다.
key | value |
---|---|
id | 게시물 ID (UUID로 자동 생성) |
voice | 오디오 파일을 생성하는데 사용된 Amazon Polly 음성 |
pollyStatus | 처리 상태에 따라서 PROCESSING 또는 UPDATED로 구분 |
originText | 원문 텍스트 |
RequestCharacters | 요청한 문자 개수 |
mp3Url | Polly에 의해서 생성된 mp3 접근 URL |
updateDate | 수정된 시간 |
timbre | 음색 (소리의 파형) |
pitch | 음조 ( 소리의 높낮이) |
5. 테이블 생성 확인
아키텍처 다이어그램에서 알 수 있듯이 텍스트에 대한 MP3 생성 요청을 비동기 적으로 수행할 수 있도록 Amazon Polly를 호출하고, 완료 처리를 UpdateNews Lambda 함수가 처리하도록 분할했습니다. 몇 가지 이유로 이 작업을 수행했습니다.
첫째, 비동기 호출을 사용하여 PostNews Lambda 함수가 UpdateNews Lambda 함수를 실행할 때까지 기다리는 것을 피할 수 있습니다. 그래서 나중에 수행해야 할 부분을 id를 통해서 공유 할 수 있습니다. 즉, 새로운 게시물 등록 작업에 대한 응답을 빠르게 클라이언트로 응답할 수 있습니다. 작은 게시물의 경우 오디오 파일로 변환하는 데 수백 밀리 초가 걸릴 수 있지만 글이 길어지면(10만 단어 이상) 텍스트를 변환하는 데 추가적인 시간이 필요할 수 있습니다. 다만, 실시간 스트리밍을 원할 때에는 문제가 되지 않습니다. Amazon Polly는 첫 번째 바이트를 사용할 수 있다면 곧바로 읽기를 시작하기 때문입니다.
두 번째 이유는 Lambda 함수는 15분 동안 실행할 수 있습니다. 이는 게시물을 변환하기 위해 충분한 시간입니다. 미래에 더 큰 것을 변화시키고 자 할 경우 Lambda 대신 AWS Batch를 사용하고자 할 수 있습니다. 애플리케이션의 이 두 부분을 분리하면 이 변경 작업이 훨씬 쉬워집니다. 위와 같이 두 개의 컴포넌트(여기서는 Lambda 함수 두 개)가 있을 때 이를 통합할 수 있습니다. 즉, 두 번째 컴포넌트가 언제 시작할지 알아야 합니다. 여러 가지 방법으로 이 작업을 수행 할 수 있습니다. 이 경우 Amazon Polly가 작업이 완료되면, Amazon SNS를 트리거 하도록 합니다.
생성된 SNS는 Amazon Polly가 작업이 완료되면 알려주기 위해서 해당 Topic을 호출해야 합니다. 따라서 생성된 Topic에 대한 아마존 고유 리소스 이름(ARN)을 알아야 합니다.
아래와 같이 생성된 Topic의 ARN을 복사하여 메모장 에 기록해 둡니다. (사용자 별로 다르게 나옵니다. 본인의 ARN 을 복사합니다.) |
응용 프로그램에서 생성한 모든 오디오 파일을 저장하는 S3 버킷을 만들어야합니다.
Lambda 함수를 만들기 전에 함수에 대한 IAM 역할을 만들어야 합니다. 역할은 함수가 상호 작용할 수 있는 AWS 서비스(API)를 지정합니다. 세 가지 Lambda 함수 모두에 대해 하나의 역할을 만듭니다. (원래 기능별로 역할을 만들지만, 예제 구현을 위해 하나로 만듭니다.)
역할을 생성하기 위해서 역할에 부여될 정책을 JSON 포맷으로 생성하고, 생성한 정책을 해당 역할에 부여합니다.
아래 리소스에 대한 접근 정책을 JSON으로 추가합니다.
{ "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "polly:StartSpeechSynthesisTask", "dynamodb:Query", "dynamodb:Scan", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "sns:Publish", "s3:PutObject", "s3:PutObjectAcl", "s3:DeleteObject", "s3:GetBucketLocation" ], "Resource":[ "*" ] } ] } |
첫 번째로 만들 Lambda 함수는 이 애플리케이션의 시작점입니다. 오디오 파일로 변환해야하는 새 게시물에 대한 정보를 받습니다.
Lambda 함수가 정상적으로 만들어진 것을 확인할 수 있습니다. Lambda의 실행 구조는 Function Overview 메뉴 에서 볼 수 있습니다. 트리거 되면, PostNews가 실행되고, 코드 안에서 특정 서비스로 접근하기 위한 권한을 Role을 통해서 할당 받은 것을 알 수 있습니다.
Function Overview 탭을 클릭해서 접고, 아래 Code를 보겠습니다. Lambda 함수 코드 편집기는 AWS Cloud9 IDE가 내장되어 있어서 코드 편집을 웹 브라우저에서도 쉽게 할 수 있는 환경을 제공합니다. 좌측 하단의 lambda_function.py 파일을 클릭하면 기본 파이썬 소스 코드가 나오는걸 볼 수 있습니다. Runtime으로 Python 3.8이 선택되어져 있는 것을 볼 수 있습니다. Handler는 함수가 시작하기 위한 파일의 위치와 함수의 이름을 의미합니다.
아래 코드를 이 Lambda 함수의 코드로 변경합니다. 해당 함수의 로직은 Polly에 TTS 작업을 요청하고, 요청 작업에 대한 ID 값을 DynamoDB에 등록합니다.
# -*- coding: utf-8 -*- from __future__ import print_function import boto3 import os import json import uuid import datetime import re def lambda_handler(event, context): if "body" in event: parmas = json.loads(event['body']) voice = parmas["voice"] originText = parmas["text"] timbre = parmas["timbre"] pitch = parmas["pitch"] updateDate = datetime.datetime.now().strftime("%Y%m%d") polly = boto3.client('polly') removeBrackets = re.sub(r'\([^)]*\)', '', originText) repTextBlock = re.sub('[·…]', '<break time="100ms"/>', removeBrackets) ssmlBlock = "<speak><amazon:effect vocal-tract-length=\"" + timbre + "\"><prosody pitch=\"" + pitch + "\">" + repTextBlock + "</prosody></amazon:effect></speak>" ssmlBlock = ssmlBlock.replace('현수', '<amazon:effect vocal-tract-length="+80%"><prosody pitch="-70%">안녕하세요? 저는 서연 친구 현수에요.</prosody></amazon:effect>') ssmlBlock = ssmlBlock.replace('귀신', '<amazon:effect name="whispered"><amazon:effect vocal-tract-length="-30%"><prosody volume="loud">나 꿍꼬또, 기싱꿍꼬또</prosody></amazon:effect></amazon:effect>') print (ssmlBlock) response = polly.start_speech_synthesis_task( OutputFormat= 'mp3', OutputS3BucketName= os.environ['BUCKET_NAME'], # OutputS3KeyPrefix='polly/', SampleRate='22050', SnsTopicArn=os.environ['SNS_TOPIC'], Text=ssmlBlock, TextType='ssml', VoiceId=voice ) print (response) data = response['SynthesisTask'] # Creating new record in DynamoDB table dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(os.environ['DB_TABLE_NAME']) table.put_item( Item = { 'id' : data['TaskId'], 'updateDate': data['CreationTime'].strftime("%Y-%m-%d %H:%M:%S"), 'voice' : voice, 'originText': originText, 'pollyStatus' : data['TaskStatus'], 'timbre': timbre, 'pitch': pitch, 'mp3Url': data['OutputUri'], 'RequestCharacters': data['RequestCharacters'] } ) result = { 'statusCode': 200, 'body': json.dumps({'recordId': data['TaskId']}), 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } } return result |
GetNews 의 함수 코드를 아래 코드로 대체합니다. 반드시 Deploy 버튼을 눌러서 저장하세요.
from __future__ import print_function import boto3 import os import json import decimal from boto3.dynamodb.conditions import Key, Attr # https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/GettingStarted.Python.04.html # Helper class to convert a DynamoDB item to JSON. class DecimalEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, decimal.Decimal): if o % 1 > 0: return float(o) else: return int(o) return super(DecimalEncoder, self).default(o) def lambda_handler(event, context): if "queryStringParameters" in event: parmas = event['queryStringParameters'] print (parmas) postId = parmas["postId"] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(os.environ['DB_TABLE_NAME']) if postId == "*": items = table.scan() else: items = table.query(KeyConditionExpression=Key('id').eq(postId)) response = { 'statusCode': 200, 'body': json.dumps(items["Items"], cls=DecimalEncoder), 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } } return response |
PostNews와 같이 Create function 버튼을 클릭해서 UpdateNews 함수를 생성합니다. Runtime과 Role 설정은 기존과 동일합니다.
UpdateNews 의 함수 코드를 아래 코드로 대체합니다. 반드시 Deploy 버튼을 눌러서 저장하세요.
from __future__ import print_function import boto3 import os import json from contextlib import closing from boto3.dynamodb.conditions import Key, Attr import re def lambda_handler(event, context): polly_message = event["Records"][0]["Sns"]["Message"] print (polly_message) polly_response = json.loads(polly_message) # Updating the item in DynamoDB dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(os.environ['DB_TABLE_NAME']) response = table.update_item( Key={ 'id': polly_response["taskId"] }, UpdateExpression="set pollyStatus = :s", ExpressionAttributeValues={ ':s': polly_response['taskStatus'] } ) print(response) s3 = boto3.client('s3') s3.put_object_acl( ACL = 'public-read', Bucket = os.environ['BUCKET_NAME'], Key = polly_response["taskId"] + ".mp3" ) return |
Environment variables는 DB_TABLE_NAME에 NewsTable 값을 입력하고, BUCKET_NAME에는 S3 버킷 이름( polly-mp3.awsdemo.kr )을 지정합니다. Tags에 Name과 NewsApp 을 입력하고, Timeout을 1 min 으로 수정한후, 우측 상단의 Save 버튼을 클릭하여 저장합니다.
DeleteNews 의 함수 코드를 아래 코드로 대체합니다. 반드시 Deploy 버튼을 눌러서 저장하세요.
from __future__ import print_function import boto3 import os import json from boto3.dynamodb.conditions import Key, Attr def lambda_handler(event, context): if "body" in event: params = json.loads(event['body']) print (params) # Bad Request if params["postId"] is None or params["postId"] == "": response = { 'statusCode': 400, 'body': json.dumps({'message': "An unknown error has occurred. Missing required parameters."}), 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } } return response postId = params["postId"] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(os.environ['DB_TABLE_NAME']) table.delete_item(Key={"id":postId}) s3 = boto3.client('s3') s3.delete_object(Bucket=os.environ['BUCKET_NAME'], Key= postId + ".mp3") response = { 'statusCode': 200, 'body': json.dumps({'message': "item is deleted : " + postId}), 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } } return response |
Environment variables는 DB_TABLE_NAME에 NewsTable 값을 입력하고, BUCKET_NAME에는 S3 버킷 이름( polly-mp3.awsdemo.kr )을 지정합니다. Tags에 Name과 NewsApp 을 입력하고, Timeout을 1 min 으로 수정합니다.
마지막으로해야 할 일은 애플리케이션 로직을 RESTful 웹 서비스로 노출시켜 표준 HTTP 프로토콜을 사용하여 쉽게 호출 할 수 있도록 합니다. 이를 위해 Amazon API Gateway를 사용합니다.
API 설정이 완료 되었습니다. 이제 배포를 해서 애플리케이션에서 호출할 수 있는 URL을 얻습니다. Actions 에서 Deploy API 를 선택합니다.
이제 API를 state 스테이지로 배포를 합니다. 개발, 테스트, 프로덕션에 이르기까지 다양한 스테이지로 나누어서 배포가 가능합니다. 여기서는 stage 로 넣고 Deploy 합니다.
이제 API 배포까지 완료되었습니다. 해당 API를 호출 할 수 있는 URL이 생성되었음을 확인할 수 있습니다.
아래와 같이 Invoke URL을 미리 메모장에 기록해 둡니다. 해당 URL을 동적 컨텐츠 수집을 위한 URL로 활용할 예정입니다. (본인의 계정에서 생성된 URL을 이용하세요.) https://7xxxxxxxxxi6.execute-api.ap-northeast-2.amazonaws.com/stage |
Amazon S3는 정적 웹 페이지를 호스팅 할 수 있습니다. 다음의 링크를 통해 정적 웹 호스팅을 하기 위한 패키지를 다운로드 할 수 있습니다: 3개의 파일(html, css, javascript)가 포함되어져 있으며, JavaScript 를 사용하여 동적 컨텐츠 API 호출을 API Gateway로 연결합니다.
다음의 순서로 진행합니다.
아래의 정책에서 BUCKET_NAME 부분을 생성한 버킷 이름으로 변경한 후 복사합니다.
{ "Version":"2012-10-17", "Statement":[ { "Sid":"PublicReadGetObject", "Effect":"Allow", "Principal":"*", "Action":[ "s3:GetObject" ], "Resource":[ "arn:aws:s3:::BUCKET_NAME/*" ] } ] } |
모든 준비가 끝났습니다. 이제 정적 웹 사이트 호스팅 탭에서 URL을 찾아서 웹 사이트가 작동하는지 확인할 수 있습니다. 상단의 정적 웹 페이지 호스팅에 나와 있는 엔드포인트로 접속하면 아래와 같은 웹 페이지가 나타납니다.
생성시에 Tags 를 등록하면 어떤 목적으로 리소스가 만들어져서 운영되고 있는지 확인 할 수 있습니다. Resource Groups를 통해서 확인해 볼 수 있습니다. 다음과 같은 순서로 Tag 기반으로 리소스를 확인할 수 있습니다. 리소스가 나타나지 않을 경우, 상단의 실습에서 Tag를 등록하지 않았을 경우입니다. 직접 추가하면서 리소스를 등록할 수 있습니다.
해당 리소스들을 삭제는 아래와 같이 진행할 수 있습니다. 각 서비스로 이동하고 해당 리소스를 선택하고 삭제하시면 됩니다.