...
Cloud9 IDE 환경 생성
- AWS 콘솔 환경에서 Cloud9 서비스로 이동합니다.
- Create environment 버튼을 클릭하여 Cloud9 환경을 생성합니다.
- Cloud9 이름을 NewsWebApp 이라고 생성합니다. 설명(News Web Application using Serverless Service)은 옵션이기 때문에 넣지 않아도 됩니다.
- Cloud9의 환경설정을 합니다. 개발환경은 EC2 인스턴스로 선택하고 인스턴스 타입은 t2.micro를 선택합니다. 프리티어로 사용할 수 있습니다. Cloud9 IDE를 30분간 사용하지 않으면, 자동으로 EC2 인스턴스가 Stop 되어 비용을 절감할 수 있는 옵션을 기본적으로 제공합니다.
- Cloud9 최종 점검을 합니다. 모든 검수가 완료되면 Create environment 버튼을 클릭합니다.
- Cloud9 IDE가 준비중인 것을 확인할 수 있습니다. 몇 분이 지나면 IDE가 활성화 됩니다.
- Cloud9에서 IDE에 대해서 환경 설정을 할 수 있습니다. 화면과 같이 서비스 배포를 위한 리전을 별도로 지정할 수 있습니다. 여기서는 Singapore 리전을 그대로 사용합니다.
- 개발 환경이 설치되고 서버리스 애플리케이션 개발을 할 준비가 완료되었습니다.
- AWS 콘솔 환경에서 Cloud9 서비스로 이동합니다.
Application 및 "PostNews" Lambda 함수 생성
새로운 애플리케이션 및 람다 함수를 생성합니다. Cloud9 IDE 우측 네비게이션의 AWS Resources 탭을 클릭하고, Lambda 아이콘을 클릭하면 새로운 함수를 생성할 수 있습니다. 앞으로 추가할 나머지 3개의 함수도 동일한 방법으로 생성합니다.
- 화면 중앙에 팝업 창이 표시됩니다. 하단의 Application name에는 WebApp을 상단의 Function name에는 PostNews를 입력하고, Next 버튼을 클릭합니다. 앞으로 추가할 나머지 3개의 함수도 Application name은 동일하게 WebApp을 사용합니다.
- Runtime은 Python 2.7을 선택하고, 하단의 Select blueprint는 hello-world-python을 선택하고, Next 버튼을 클릭합니다.
- PostNews 함수를 호출하는 주체는 API Gateway입니다. 따라서, Function trigger는 API Gateway를 선택합니다. API 호출에 사용할 리소스 경로는 /news 를 입력합니다. API 호출을 위한 보안 메커니즘은 NONE으로 선택하고, Next 버튼을 클릭합니다.
- PostNews 함수를 실행하는 Memory는 128MB로 지정합니다. Role은 Automatically generate role을 선택하고, Next 버튼을 클릭합니다. Role에 들어갈 정책(Policy)는 SAM 템플릿에서 직접 설정할 예정입니다.
- PostNews 함수 구성을 확인합니다. 별도의 이상이 없을 경우, Finish 버튼을 클릭 합니다.
- 다음과 같이 Application과 Function이 세팅되는 것을 확인할 수 있습니다.
- 좌측 Environment 탭에 WebApp Application 폴더를 생성합니다. 그리고 PostNews 함수에서 사용하는 폴더를 생성합니다.
- SAM 템플릿 파일인 template.yaml 파일이 생성되는것을 확인할 수 있습니다.
- Lambda 함수가 blueprint에 의해서 자동으로 만들어집니다. 해당 코드는 아래에서 제공하는 PostNews 함수 코드로 대체합니다.
- 우측 AWS Resources 탭에 Local Functions에 현재 작업중인 WebApp 폴더와 Application과 PostNews 함수를 생성하는 것을 확인할 수 있습니다.
- 자동으로 template을 기반으로 Lambda 서비스에 배포할 수 있습니다. 배포할 경우, 이름은 Cloud9- 접두사와 Application 이름인 WebApp-이 접두사로 붙는 것을 확인할 수 있습니다.
PostNews 함수는 아래에서 제공하는 코드로 대체하고 Ctrl+S를 눌러서 저장합니다. 코드 설명은 주석을 기반으로 생략합니다.
코드 블럭 language py theme RDark title PostNews linenumbers true # -*- coding: utf-8 -*- from __future__ import print_function import boto3 import os import json import uuid import datetime def lambda_handler(event, context): if "body" in event: event = json.loads(event['body']) print (event) recordId = str(uuid.uuid4()) voice = event["voice"] originText = event["text"] timbre = event["timbre"] pitch = event["pitch"] updateDate = datetime.datetime.now().strftime("%Y%m%d") print('Generating new DynamoDB record, with ID: ' + recordId) # Create the item in DynamoDB table dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(os.environ['DB_TABLE_NAME']) table.put_item( Item={ 'id' : recordId, 'originText': originText, 'postDate': int(updateDate), 'pollyVoice' : voice, 'pollyStatus' : "PROCESSING", 'pollyTimbre': timbre, 'pollyPitch': pitch } ) # Sending notification about new post to SNS client = boto3.client('sns') client.publish( TopicArn = os.environ['SNS_TOPIC'], Message = recordId ) response = { 'statusCode': 200, 'body': json.dumps({'recordId': recordId}), 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } } return response
이번에는 SAM Template을 변경합니다. 먼저 좌측 소스코드 영역에서 template.yaml 파일을 더블클릭하여 편집 창을 열고, 아래 Template 코드로 대체하고, Ctrl+S를 눌러서 저장합니다.
코드 블럭 language yml theme RDark title PostNewsTemplate_01 linenumbers true AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: >- Building Serverless development environment and CI/CD process for DevOps based on Cloud9 Globals: Function: Runtime: python2.7 Handler: lambda_function.lambda_handler MemorySize: 128 Timeout: 60 Resources: PostNews: Type: 'AWS::Serverless::Function' Properties: CodeUri: PostNews Description: Post news text to convert from text to speech Events: PostNewsApi: Type: Api Properties: Path: /news Method: POST Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:PutLogEvents' - 'logs:CreateLogStream' - 'dynamodb:PutItem' - 'sns:Publish' Resource: '*'
SAM 템플릿 구성은 다음과 같습니다.
Globals는 전역으로 사용합니다. 함수(Function)이나 API에서 반복해서 사용하는 것을 선언할 수 있습니다.
- Resources는 AWS 서비스를 정의합니다. 각 서비스에 대해서 이름을 선언하고, Type을 정할 수 있습니다.
- Runtime은 Function에서 사용합니다. 4개의 Lambda 함수가 모두 Python 2.7 기반이기 때문에 python2.7을 선언합니다.
- Type은 서비스 타입을 의미합니다. Lambda 함수, API Gateway, DynamoDB, SNS, S3 등을 표기할 수 있습니다.
CodeUri는 Lambda 함수가 위치한 폴더를 의미합니다. template.yaml 파일 기준으로 Lambda 함수가 있는 폴더의 이름을 지정합니다. - Handler는 Lambda 함수가 시작하는 함수의 파일명.함수명 에 대한 구성을 의미 합니다. 모두 lambda_function.py 파일에 선언되어 있는 lambda_handler 함수가 시작 함수가 됩니다. 각각의 함수는 폴더가 서로 다릅니다.
- MemorySize는 128MB로 모두 구성되어 있으며, Lambda 함수의 최대 구동 시간은 Timeout 값을 사용하고 60초로 설정합니다.
- Events는 Lambda 함수를 트리거하는 이벤트를 의미합니다. API Gateway일 경우 Type은 Api가 됩니다. SNS에 의해서 트리거 될 경우, Type은 Sns가 됩니다.
- Policies는 해당 함수가 접근할 수 있는 서비스에 대한 권한을 줄 수 있습니다. 각각의 함수는 최소 권한의 원칙에 의거하여 필요한 정책만을 연결합니다.
PostNews 함수는 동작 로그를 남기기 위해서 logs:PutLogEvents와 logs:CreateLogStream을, DynamoDB에 항목을 추가하기 위해서 dynamodb:PutItem을 SNS Topic에 게시하기 위해서 sns:Publish를 설정합니다.
SAM 설정이 완료되면 해당 template.yaml 파일을 Cloud9에서 바로 배포할 수 있습니다.
우측의 해당 Lambda 함수에서 마우스 우측 버튼을 클릭하고 Deploy 명령을 수행합니다. 해당 SAM template이 CloudFormation Stack에 업데이트 되는 것을 확인할 수 있습니다.
SAM 기반 서버리스 리소스 추가
SAM 템플릿을 이용해서 S3 Bucket, SNS, DynamoDB 테이블을 배포할 수 있습니다. 다음과 같이 template.yaml 파일에 리소스에 추가합니다.
Environment: 환경 변수를 선언합니다. 각각의 Lambda 함수에는 DynamoDB, SNS, S3 버킷에 접근하기 위해서 ARN이 필요합니다. SAM에서 자동으로 만들어준 각각의 리소스 ARN을 참조할 수 있도록 설정합니다.
NewsTable: SAM에서 SimpleTable은 DynamoDB를 의미합니다. 여기서 DynamoDB를 기본 세팅인 WCU 5, RCU 5 값으로 생성합니다.
NewsTopic: PostNews에 들어온 텍스트 값은 비동기적으로 처리를 위하여 ConvertAudio 함수의 처리를 필요로 합니다. ConvertAudio의 처리를 위해서 사용할 SNS Topic을 하나 생성합니다.
PollyMp3Bucket: Amazon Polly를 이용해서 생성한 MP3 파일을 저장할 S3 버킷을 생성합니다.
StaticWebBucket: S3는 객체 스토리지이면서 정적 웹 호스팅을 위한 서비스로 활용할 수 있습니다. 여기서는 정적 웹 호스팅을 위한 설정까지 함께 구성하여 SAM에 서비스를 추가합니다.
변경된 SAM 템플릿을 반영하기 위해서 우측 Lambda 함수에서 마우스 우측 버튼을 클릭하고 Deploy 명령을 수행합니다.
코드 블럭 language yml theme RDark title PostNewsTemplate_02 linenumbers true AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: >- Building Serverless development environment and CI/CD process for DevOps based on Cloud9 Globals: Function: Runtime: python2.7 Handler: lambda_function.lambda_handler MemorySize: 128 Timeout: 60 Environment: Variables: DB_TABLE_NAME: Ref: NewsTable SNS_TOPIC: Ref: NewsTopic BUCKET_NAME: Ref: PollyMp3Bucket Resources: NewsTable: Type: 'AWS::Serverless::SimpleTable' Properties: PrimaryKey: Name: id Type: String ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 NewsTopic: Type: 'AWS::SNS::Topic' Properties: DisplayName: NewsTopic PollyMp3Bucket: Type: 'AWS::S3::Bucket' StaticWebBucket: Type: 'AWS::S3::Bucket' Properties: AccessControl: PublicRead WebsiteConfiguration: IndexDocument: index.html ErrorDocument: error.html PostNews: Type: 'AWS::Serverless::Function' Properties: CodeUri: PostNews Description: Post news text to convert from text to speech Events: PostNewsApi: Type: Api Properties: Path: /news Method: POST Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:PutLogEvents' - 'logs:CreateLogStream' - 'dynamodb:PutItem' - 'sns:Publish' Resource: '*'
- AWS 관리 콘솔에서 CloudFormation 서비스로 이동하면 다음과 같이 Stack을 확인할 수 있습니다.
- Cloud9 환경을 배포했기 때문에 해당 Stack(aws-cloud9-NewsWebApp-xxxxxxxxxx)이 등록되어 있는 것을 확인할 수 있습니다.
- Cloud9 환경에서 만든 WebApp이 배포된 Cloud9-WebApp Stack을 확인할 수 있습니다.
- 아래와 같이 Cloud9에서 SAM template을 변경하고 Delpoy 하면, 해당 Stack이 업데이트 되는 것을 확인할 수 있습니다.
- Cloud9-WebApp을 클릭하고 들어가면, Stack에 대해서 자세히 확인할 수 있습니다. 현재 추가로 배포한 리소스가 배포 완료된 것을 확인할 수 있습니다.
"ConvertAudio" Lambda 함수 생성
- 우측 AWS Resources 탭에 있는 Lambda 아이콘을 클릭하여 두 번째 함수인 ConvertAudio 함수를 생성합니다. Application name은 WebApp이고, Function name은 ConvertAudio를 입력하고 우측 하단의 Next 버튼을 클릭합니다.
- Runtime은 Python2.7을 선택하고 blueprint는 hello-world-python을 선택하고, Next 버튼을 클릭합니다.
- ConvertAudio 함수는 trigger가 SNS에 의하여 됩니다. PostNews 설정과 동일하게 API Gateway로 설정했지만, SAM의 Events에서 SNS를 사용하도록 변경할 예정입니다. 다음 함수 설정을 위해서 API Gateway를 Function trigger로 선택하고, 리소스 경로는 /news , 보안은 NONE 을 동일하게 선택합니다.
- ConvertAudio 함수를 실행하는 Memory는 128MB로 지정합니다. Role은 Automatically generate role을 선택하고, Next 버튼을 클릭합니다. Role에 들어갈 정책(Policy)는 SAM 템플릿에서 직접 설정할 예정입니다.
- ConvertAudio 함수 구성을 확인합니다. 별도의 이상이 없을 경우, Finish 버튼을 클릭 합니다.
ConvertAudio 함수 코드를 다음과 같이 변경하고 저장합니다. 소스 코드의 설명은 주석으로 대체합니다.
코드 블럭 language py theme RDark title ConverAudio linenumbers true # -*- coding: utf-8 -*- from __future__ import print_function import boto3 import os from contextlib import closing from boto3.dynamodb.conditions import Key, Attr import re def lambda_handler(event, context): postId = event["Records"][0]["Sns"]["Message"] print ("Text to Speech function. Post ID in DynamoDB: ", postId) # Retrieving information about the post from DynamoDB table dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(os.environ['DB_TABLE_NAME']) postItem = table.query( KeyConditionExpression=Key('id').eq(postId) ) text = postItem["Items"][0]["originText"] voice = postItem["Items"][0]["pollyVoice"] timbre = postItem["Items"][0]["pollyTimbre"] pitch = postItem["Items"][0]["pollyPitch"] rest = text # Because single invocation of the polly synthesize_speech api can # transform text with about 3,000 characters, we are dividing the # post into blocks of approximately 2,900 characters. textBlocks = [] while (len(rest) > 3000): begin = 0 end = rest.find(".", 2900) if (end == -1): end = rest.find(" ", 2900) textBlock = rest[begin:end] rest = rest[end:] textBlocks.append(textBlock) textBlocks.append(rest) #For each block, invoke Polly API, which will transform text into audio polly = boto3.client('polly') for textBlock in textBlocks: removeBrackets = re.sub(r'\([^)]*\)', '', textBlock) repTextBlock = re.sub('[·…]', '<break time="100ms"/>', removeBrackets) #repTextBlock = re.sub('["·\'…]', '<break time="100ms"/>', removeBrackets) ssmlBlock = "<speak><amazon:effect vocal-tract-length=\"" + timbre + "\"><prosody pitch=\"" + pitch + "\">" + repTextBlock + "</prosody></amazon:effect></speak>" #print (ssmlBlock) response = polly.synthesize_speech(OutputFormat='mp3', Text = ssmlBlock, VoiceId = voice, TextType = 'ssml') #Save the audio stream returned by Amazon Polly on Lambda's temp # directory. If there are multiple text blocks, the audio stream # will be combined into a single file. if "AudioStream" in response: with closing(response["AudioStream"]) as stream: output = os.path.join("/tmp/", postId) with open(output, "a") as file: file.write(stream.read()) s3 = boto3.client('s3') s3.upload_file('/tmp/' + postId, os.environ['BUCKET_NAME'], postId + ".mp3") s3.put_object_acl(ACL='public-read', Bucket=os.environ['BUCKET_NAME'], Key= postId + ".mp3") location = s3.get_bucket_location(Bucket=os.environ['BUCKET_NAME']) region = location['LocationConstraint'] if region is None: url_begining = "https://s3.amazonaws.com/" else: url_begining = "https://s3-" + str(region) + ".amazonaws.com/" \ url = url_begining \ + str(os.environ['BUCKET_NAME']) \ + "/" \ + str(postId) \ + ".mp3" #Updating the item in DynamoDB response = table.update_item( Key={'id':postId}, UpdateExpression= "SET #statusAtt = :statusValue, #urlAtt = :urlValue", ExpressionAttributeValues= {':statusValue': 'UPDATED', ':urlValue': url}, ExpressionAttributeNames= {'#statusAtt': 'pollyStatus', '#urlAtt': 'mp3Url'}, ) return
아래와 같이 자동으로 template.yaml 파일에 ConvertAudio 함수 설정이 추가되는 것을 확인할 수 있습니다. 하지만, 해당 SAM 설정 대신 아래의 SAM 코드로 대체합니다.
코드 블럭 language yml theme RDark title CoverAudio Template linenumbers true Resources: ... ConvertAudio: Type: 'AWS::Serverless::Function' Properties: CodeUri: ConvertAudio Description: Convert Audio using Amazon Polly Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:PutLogEvents' - 'logs:CreateLogStream' - 'dynamodb:Query' - 'dynamodb:UpdateItem' - 's3:GetBucketLocation' - 's3:PutObject' - 's3:PutObjectAcl' - 'polly:SynthesizeSpeech' Resource: '*' Events: ConvertResource: Type: SNS Properties: Topic: Ref: NewsTopic
- 다음과 같이 SAM template을 수정합니다. 이미 중복되는 설정은 Globals에 선언되어 있기 때문에 간단하게 선언할 수 있습니다.
- Policies 에서, DynamoDB에 등록된 News 정보를 읽기 위해서 dynamoDb:Query 를 추가하고, S3에 업로드한 MP3 URL 정보를 업데이트 하기 위해서 dynamodb:UpdateItem 정책을 추가합니다.
- S3 버킷에 MP3를 업로드 하기 전, 리전 정보를 검색하기 위해서 s3:GetBucketLocation 을 추가하고, S3에 객체를 추가하기 위해서 s3:PutObject 를 추가하고, 퍼블릭하게 접근할 수 있는 권한 설정을 위해서 s3:PutObjectAcl 정책을 추가합니다.
- 텍스트를 MP3로 TTS 변환하기 위해서는 Aamzon Polly 서비스를 사용할 수 있습니다. 해당 Lambda 함수에서 접근할 수 있도록 polly:SynthesizeSpeech 정책을 추가합니다.
- ConvertAudio 함수는 SNS의 Topic에 의해서 트리거 됩니다. 따라서 리소스에서 생성한 NewsTopic 에 의해서 트리거 되도록 설정합니다.
- 우측 AWS Resources 탭에 있는 Lambda 아이콘을 클릭하여 두 번째 함수인 ConvertAudio 함수를 생성합니다. Application name은 WebApp이고, Function name은 ConvertAudio를 입력하고 우측 하단의 Next 버튼을 클릭합니다.
"GetNews" Lambda 함수 생성
- PostNews와 ConvertAudio 함수처럼 GetNews 함수를 생성합니다.
코드는 다음을 참고합니다. 함수에 대한 설명은 주석으로 대체합니다.
코드 블럭 language py theme RDark title GetNews linenumbers true 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: event = event['queryStringParameters'] print (event) postId = event["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
SAM 템플릿을 GetNews 함수에 맞게 기존 설정을 지우고 아래와 같이 추가합니다.
코드 블럭 language yml theme RDark title GetNews Template linenumbers true Resources: ... GetNews: Type: 'AWS::Serverless::Function' Properties: CodeUri: GetNews Description: Gather information from Ajax calls from web pages Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:PutLogEvents' - 'logs:CreateLogStream' - 'dynamodb:Query' - 'dynamodb:Scan' Resource: '*' Events: GetNewsApi: Type: Api Properties: Path: /news Method: GET
- PostNews와 ConvertAudio 함수처럼 GetNews 함수를 생성합니다.
"DeleteNews" Lambda 함수 생성
- 마지막으로 DeleteNews 함수를 생성합니다.
코드는 다음을 참고합니다. 함수에 대한 설명은 주석으로 대체합니다.
코드 블럭 language py theme RDark title DeleteNews linenumbers true 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: event = json.loads(event['body']) print (event) # Bad Request if event["postId"] is None or event["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 = event["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
DeleteNews를 위해서 SAM 템플릿을 아래와 같이 추가합니다.
코드 블럭 language yml theme RDark title DeleteNews Template linenumbers true Resources: ... DeleteNews: Type: 'AWS::Serverless::Function' Properties: CodeUri: DeleteNews Description: Delete news item in DynamoDB Table and mp3 file in S3 bucket. Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:PutLogEvents' - 'logs:CreateLogStream' - 'dynamodb:DeleteItem' - 's3:DeleteObject' Resource: '*' Events: DeleteNewsApi: Type: Api Properties: Path: /news Method: DELETE
삭제 이벤트에 대해서 DynamoDB의 항목을 삭제하기 위해서 dynamodb:DeleteItem 정책과 S3에 생성된 MP3 파일을 삭제하기 위해서 s3:DeleteObject 정책을 추가합니다.
- 마지막으로 DeleteNews 함수를 생성합니다.
SAM에서의 Outputs 설정
SAM 템플릿에서 생성된 리소스를 확인하기 위해서 CloudFormation의 Stack 상세에서 Outputs으로 출력을 만들 수 있습니다. 여기서는 3개의 출력물을 생성합니다.
코드 블럭 language yml theme RDark title Template_Outpuit linenumbers true Outputs: S3WebBucket: Description: S3 Bucket Name for web hosting Value: Ref: StaticWebBucket WebsiteURL: Description: Name of S3 bucket to hold website content Value: 'Fn::Join': - '' - - 'https://' - 'Fn::GetAtt': - StaticWebBucket - DomainName - '/index.html' APIEndpointURL: Description: URL of your API endpoint Value: 'Fn::Sub': >- https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/news
- S3WebBucket : 정적 웹 호스팅을 하는 S3 버킷 이름
- WebsiteURL : 정적 웹 호스팅에 접속하기 위한 URL 정보, 정적 웹 호스팅을 하기 위한 정적 컨텐츠 파일은 추후 업로드 합니다.(html, css, js)
- APIEndpointURL : 동적 API를 활용하기 위한 API Gateway가 배포한 Endpoint URL 정보
SAM에서의 CORS 설정
S3에서 제공하는 정적 웹 호스팅을 하는 버킷에서 제공하는 URL의 도메인과 API Gateway 에서 배포한 API Endpoint URL의 도메인은 서로 상이합니다. 따라서 브라우저에서 발생하는 보안 이슈를 해결하기 위해서는 CORS 설정을 해야합니다. 이 설정 역시 SAM에 모든 Lambda 함수에 추가할 수 있습니다. 따라서 Globals에 API 영역에 다음과 같이 추가합니다.
코드 블럭 language yml theme RDark title Template_API_CORS linenumbers true Globals: ... Api: # enable CORS; to make more specific, change the origin wildcard # to a particular domain name, e.g. "'www.example.com'" Cors: AllowMethods: "'*'" AllowHeaders: "'*'" AllowOrigin: "'*'"
정적 웹 호스팅을 위한 파일 업로드하기
- 다음과 같이 정적 컨텐츠 파일을 다운로드 받습니다. Scripts.js 파일에 API Endpoint URL을 넣어주고, S3 버킷에 업로드 합니다.
정적 웹 호스팅 파일 다운로드 받기지금까지의 SAM을 정리하면 다음과 같습니다.
코드 블럭 language bashyml theme RDark title Final SAM linenumbers true wget https://s3.ap-northeast-2.amazonaws.com/polly.awsdemokr.com/301_static_web.zip
압축 풀고 폴더 이동
코드 블럭 language bash theme RDark linenumbers true unzip 301_static_web.zip cd 301_static_web
Cloud9에서 scripts.js 파일 열어서 CloudFormation Stack에 배포된 Output의 APIEndpointURL 값을 소스코드에 반영 (WebsiteURL이 아니므로 주의)
코드 블럭 language js theme RDark linenumbers true var API_ENDPOINT = "https://xxxxxxxxxx.execute-api.ap-southeast-1.amazonaws.com/Prod/news/"; if (API_ENDPOINT === "") { alert("scripts.js 파일의 상단에 API Gateway에 배포한 URL을 등록하고 실행하세요."); }
정적 웹 포스팅하고자 하는 S3 버킷에 public-read 권한으로 파일을 업로드 (CloudFormation Stack에 배포된 Output의 S3WebBucket 값을 아래에 대체)
웹 브라우저로 정적 웹 페이지에 접속 (CloudFormation Stack에 배포된 Output의 WebsiteURL 값을 웹 브라우저 주소창에 입력)코드 블럭 language bash theme RDark linenumbers true aws s3 sync . s3://cloud9-webapp-staticwebbucket-xxxxxxxxxxxx --acl public-read
서비스 동작 테스트
가나다SAM을 CloudFormation 스택에 직접 반영하기
Lab2. Code* 서비스를 이용한 서버리스 CI/CD 배포 프로세스 구축
소스 리포지토리를 위해서 CodeCommit 생성
CodeBuiild를 위한 buildspec.yaml 파일 생성
CodePipeline 구축하기
소스 리포티토리에 코드 체크인하기
배포 결과 확인 (S3 정적 웹 페이지는 다루지 않습니다.)
API를 이용해서 결과 확인
Canary 배포를 위한 설정하기
CloudFormation에서 CodeDeploy 할 수 있도록 IAM 정책 설정 적용
코드를 변경하여 배포하고 CodeDeploy 중에 API를 호출하여 적용되는지 확인하기
기존 배포 버전으로 롤백하기
Lab3. X-ray를 이용한 서버리스 서비스 모니터링 및 디버깅 (9월 예정)
Lab4. LocalStack을 이용한 로컬 테스트 환경 구축 및 테스트 (미정)
Lab5. ElasticSearch를 이용한 검색 서비스 구축 (미정)
Application 및 "PostNews" Lambda 함수 생성
...
필수 리소스를 SAM에 추가하고 리소스 확인
...
"ConvertAudio" Lambda 함수 생성
"GetNews" Lambda 함수 생성
...
"DeleteNews" Lambda 함수 생성
...
S3 정적 컨텐츠 업로드
Lab2. CodePipeline을 생성하여 CI/CD 프로세스 만들기
- CodeCommit 생성
- CoudBuild를 위한 nuildspec.yml 만들기
- CodePipeline 생성하기
- CodeDeploy 배포 (카나리 배포)
- 롤백하기 (Lambda - API Gateway)
...
language | yml |
---|---|
theme | RDark |
title | Final SAM |
linenumbers | true |
...
AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: >- Building Serverless development environment and CI/CD process for DevOps based on Cloud9 Globals: Function: Runtime: python2.7 Handler: lambda_function.lambda_handler MemorySize: 128 Timeout: 60 Environment: Variables: DB_TABLE_NAME: Ref: NewsTable SNS_TOPIC: Ref: NewsTopic BUCKET_NAME: Ref: PollyMp3Bucket Api: # enable CORS; to make more specific, change the origin wildcard # to a particular domain name, e.g. "'www.example.com'" Cors: AllowMethods: "'*'" AllowHeaders: "'*'" AllowOrigin: "'*'" Resources: NewsTable: Type: 'AWS::Serverless::SimpleTable' Properties: PrimaryKey: Name: id Type: String ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 NewsTopic: Type: 'AWS::SNS::Topic' Properties: DisplayName: NewsTopic PollyMp3Bucket: Type: 'AWS::S3::Bucket' StaticWebBucket: Type: 'AWS::S3::Bucket' Properties: AccessControl: PublicRead WebsiteConfiguration: IndexDocument: index.html
...
ErrorDocument:
...
error.html
...
PostNews: Type:
...
'AWS::Serverless::Function' Properties: CodeUri: PostNews
...
...
...
Description:
...
Post
...
news
...
text to convert from text to speech
...
Events:
...
...
PostNewsApi:
...
...
...
...
Type: Api
...
Properties:
...
Path: /news
...
...
...
Method:
...
POST
...
Policies: -
...
Version:
...
'2012-10-17'
...
...
...
Statement:
...
...
...
-
...
Effect:
...
Allow
...
...
...
...
...
Action:
...
...
...
-
...
'logs:PutLogEvents'
...
...
-
...
'logs:CreateLogStream'
...
...
...
...
-
...
'dynamodb:PutItem'
...
...
...
...
...
...
...
...
...
...
...
- 'sns:Publish'
...
Resource: '*'
...
ConvertAudio:
...
...
Type: 'AWS::Serverless::Function' Properties:
...
CodeUri:
...
ConvertAudio Description: Convert Audio using Amazon
...
Polly Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:PutLogEvents' - 'logs:CreateLogStream' - 'dynamodb:
...
Query' - '
...
dynamodb:
...
UpdateItem'
...
- '
...
s3:GetBucketLocation'
...
...
...
...
...
-
...
's3:PutObject'
...
...
...
...
...
...
-
...
's3:PutObjectAcl'
...
...
...
...
- 'polly:SynthesizeSpeech'
...
...
Resource:
...
'*' Events:
...
ConvertResource: Type:
...
SNS
...
...
Properties:
...
...
Topic:
...
Ref: NewsTopic
...
...
GetNews:
...
Type: 'AWS::Serverless::Function' Properties:
...
CodeUri: GetNews Description: Gather information from Ajax calls from web pages Policies: - Version: '
...
2012-10-17' Statement: -
...
Effect: Allow Action: - '
...
logs:
...
PutLogEvents' - '
...
logs:
...
CreateLogStream'
...
- '
...
dynamodb:Query'
...
-
...
'dynamodb:Scan'
...
...
Resource: '*'
...
Events: GetNewsApi:
...
Type: Api
...
...
...
Properties: Path: /news Method: GET DeleteNews: Type: 'AWS::Serverless::Function' Properties: CodeUri:
...
DeleteNews Description:
...
Delete news item in DynamoDB Table and mp3 file in S3 bucket. Policies: - Version: '2012-10-17' Statement: - Effect: Allow Action: - 'logs:PutLogEvents' - 'logs:CreateLogStream' - 'dynamodb:
...
DeleteItem' - '
...
s3:
...
DeleteObject' Resource: '*' Events:
...
DeleteNewsApi: Type: Api Properties: Path: /news Method:
...
DELETE Outputs:
...
S3WebBucket:
...
Description: S3 Bucket Name for web hosting
...
Value:
...
Ref:
...
StaticWebBucket WebsiteURL: Description:
...
Name
...
of
...
S3
...
bucket
...
to
...
hold
...
website content
...
Value:
...
'Fn::Join': -
...
''
...
- - '
...
https:
...
//'
...
- '
...
Fn::
...
GetAtt':
...
-
...
StaticWebBucket -
...
DomainName
...
Cloud9 Code 창 하단의 Terminal로 접근
정적 웹 호스팅 파일 다운로드 받기
코드 블럭 | ||||||
---|---|---|---|---|---|---|
| ||||||
wget https://s3.ap-northeast-2.amazonaws.com/polly.awsdemokr.com/301_static_web.zip |
압축 풀고 폴더 이동
코드 블럭 | ||||||
---|---|---|---|---|---|---|
| ||||||
unzip 301_static_web.zip
cd 301_static_web |
Cloud9에서 scripts.js 파일 열어서 CloudFormation Stack에 배포된 Output의 APIEndpointURL 값을 소스코드에 반영 (WebsiteURL이 아니므로 주의)
코드 블럭 | ||||||
---|---|---|---|---|---|---|
| ||||||
var API_ENDPOINT = "https://xxxxxxxxxx.execute-api.ap-southeast-1.amazonaws.com/Prod/news/";
if (API_ENDPOINT === "")
{
alert("scripts.js 파일의 상단에 API Gateway에 배포한 URL을 등록하고 실행하세요.");
} |
정적 웹 포스팅하고자 하는 S3 버킷에 public-read 권한으로 파일을 업로드 (CloudFormation Stack에 배포된 Output의 S3WebBucket 값을 아래에 대체)
코드 블럭 | ||||||
---|---|---|---|---|---|---|
| ||||||
aws s3 sync . s3://cloud9-webapp-staticwebbucket-xxxxxxxxxxxx --acl public-read |
...
- https://cloud9-webapp-staticwebbucket-xxxxxxxxxxxx.s3.amazonaws.com
웹 페이지 동작 확인
- 텍스트를 등록
- 검색
- 재생
- 텍스트를 옵션을 주어서 등록
- 검색
- 재생
- 삭제
CloudWatch Logs를 통해서 로그를 확인
로컬 환경에서 테스트하는 방법:
- LocalStack - A fully functional local AWS cloud stack
- DynamoDB 로컬 설정(다운로드 버전)
압축 풀기
스크립 파일 수정하기
정적 웹 호스팅 파일 S3에 업로드 하기
SAM(template.yml)에 DynamoDB, SNS, S3(Web, Mp3) 리소스 추가하기
"ConvertAudio" Lambda 함수 생성
"GetNews" Lambda 함수 생성
"DeleteNews" Lambda 함수 생성
SAM의 Output 설정
정적 웹 호스팅을 위한 파일 업로드하기
서비스 동작 테스트
SAM을 CloudFormation 스택에 직접 반영하기
1. DynamoDB 테이블 만들기
DynamoDB는 posts 와 관련된 게시물 정보와 생성된 MP3의 URL을 저장합니다.
...
NoSQL인 DynamoDB를 사용하므로 스키마를 사전에 정의하지는 않겠지만, 사용하게 될 어트리뷰트가 어떤 것이 있는지 살펴 보겠습니다.
...
2. mp3 저장을 위한 S3 버킷 만들기
응용 프로그램에서 생성한 모든 오디오 파일을 저장하는 S3 버킷을 만들어야합니다.
- S3 콘솔로 이동하여 새로운 버킷을 생성합니다. 예제에서는 polly-mp3.awsdemokr.com 이라는 버킷 이름으로 작성하지만, 실습에서는 전 세계적으로 고유한 다른 이름으로 작성합니다.
3. SNS 주제 만들기
아키텍처 다이어그램에서 알 수 있듯이 텍스트에 대한 MP3 생성 요청을 오디오 파일로 변환하는 로직을 두 개의 Lambda 함수로 분할했습니다(한자 변환 로직 제외). 몇 가지 이유로 이 작업을 수행했습니다.
첫째, 비동기 호출을 사용하여 애플리케이션에 새 게시물 정보를 보내는 사용자가 새 DynamoDB 항목의 id를 수신하도록 합니다. 이는 "New Post" Lambda 함수가 "Convert to Audio" Lambda 함수를 실행하는 동안 기다리는 것을 피할 수 있습니다. 그래서 나중에 수행해야 할 부분을 id를 통해서 공유 할 수 있습니다. 즉, 새로운 게시물 등록 작업에 대한 응답을 빠르게 클라이언트로 응답할 수 있습니다. 작은 게시물의 경우 오디오 파일로 변환하는 데 수백 밀리 초가 걸릴 수 있지만 글이 길어지면(10만 단어 이상) 텍스트를 변환하는 데 추가적인 시간이 필요할 수 있습니다. 다만, 실시간 스트리밍을 원할 때에는 문제가 되지 않습니다. Amazon Polly는 첫 번째 바이트를 사용할 수 있다면 곧바로 읽기를 시작하기 때문입니다.
두 번째 이유는 Lambda 함수는 5분 동안 실행할 수 있습니다. 이는 게시물을 변환하기 위해 충분한 시간입니다. 미래에 더 큰 것을 변화시키고 자 할 경우 Lambda 대신 AWS Batch를 사용하고자 할 수 있습니다. 애플리케이션의 이 두 부분을 분리하면 이 변경 작업이 훨씬 쉬워집니다. 위와 같이 두 개의 컴포넌트(여기서는 Lambda 함수 두 개)가 있을 때 이를 통합할 수 있습니다. 즉, 두 번째 컴포넌트가 언제 시작할지 알아야 합니다. 여러 가지 방법으로 이 작업을 수행 할 수 있습니다. 이 경우 Amazon SNS를 사용합니다. 새 게시물에 대한 메시지를 첫 번째 함수에서 두 번째 함수으로 보냅니다.
- 그렇기에 간단한 SNS 주제를 만들어 보겠습니다. SNS 대시보드에서 주제 생성을 할 수 있습니다. 아래와 같이 new_posts 라는 새로운 주제을 생성합니다.
4. IAM 역할 만들기
Lambda 함수를 만들기 전에 함수에 대한 IAM 역할을 만들어야 합니다. 역할은 함수가 상호 작용할 수 있는 AWS 서비스(API)를 지정합니다. 세 가지 Lambda 함수 모두에 대해 하나의 역할을 만듭니다. (원래 기능별로 역할을 만들지만, 예제 구현을 위해 하나로 만듭니다.)
역할을 생성하기 위해서 역할에 부여될 정책을 JSON 포맷으로 생성하고, 생성한 정책을 해당 역할에 부여합니다.
- 콘솔의 서비스에서 IAM을 찾은 다음 역할 메뉴를 선택하고, 역할 만들기 버튼을 눌러 새 역할을 만들기 위한 마법사를 엽니다. 이 역할을 사용할 서비스인 AWS 서비스 의 Lambda 를 선택하고 다음:권한 버튼을 클릭합니다.
- 역할은 정책과 연결되거나 inline으로 정책을 작성하여 연결할 수 있습니다. 여기서는 새로운 정책을 생성하기 위해서 정책 생성 버튼을 클릭 합니다.
정책 생성 새 탭이 나타나면, JSON 편집기 탭을 선택하고 아래와 같이 AWS 권한을 정의하는 코드 를 붙여 넣고, Review policy 버튼을 클릭합니다.
코드 블럭 title Lambda 함수를 위한 정책 JSON 코드 linenumbers true { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":[ "polly:SynthesizeSpeech", "dynamodb:Query", "dynamodb:Scan", "dynamodb:PutItem", "dynamodb:UpdateItem", "sns:Publish", "s3:PutObject", "s3:PutObjectAcl", "s3:GetBucketLocation", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "lambda:InvokeFunction" ], "Resource":[ "*" ] } ] }
- 정책 이름을 LambdaPostsPolicy 로 등록하고, Create policy 버튼을 클릭합니다.
- 다시 역할 만들기 화면으로 이동한 다음, 새로 고침 버튼으로 정책을 갱신해준 다음 새롭게 만들어진 정책( LambdaPostsPolicy )을 검색해서 선택 하고, 다음:검토 버튼을 클릭합니다.
- 역할 이름은 LambdaPostsRole 로 지정하고 설정된 정책을 확인한 후 역할 만들기 를 실행합니다.
생성된 LambdaPostsRole 역할은 앞으로 만들 Lambda 함수에 연결하여, 정책에 정의되어 있는 서비스들에 접근하여 읽기 또는 쓰기 작업을 수행할 수 있는 권한을 부여하는데 사용합니다.
5. "New Post" Lambda 함수 만들기
첫 번째로 만들 Lambda 함수는 이 애플리케이션의 시작점입니다. 오디오 파일로 변환해야하는 새 게시물에 대한 정보를 받습니다.
...
아래 코드를 이 Lambda 함수의 코드로 변경합니다. 한자를 한글로 변환할 수 있는 다른 Lambda 함수를 호출(invoke)할 수 있습니다. 전처리가 필요할 경우, 함수에 추가 하거나 별도의 함수를 작성하여 호출하도록 구성할 수 있습니다. 항상 해당 함수를 호출하지 않아도 되거나, 다른 언어(여기서는 Java로 구현된 한자 변환 함수)로 구현한 함수를 연결할 수 있습니다.
코드 블럭 | ||||||
---|---|---|---|---|---|---|
| ||||||
import boto3
import os
import json
import uuid
import datetime
def lambda_handler(event, context):
recordId = str(uuid.uuid4())
voice = event["voice"]
originText = event["text"]
hanja = event["hanja"]
updateDate = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print('Generating new DynamoDB record, with ID: ' + recordId)
print('Input Text: ' + originText)
print('Selected voice: ' + voice)
# Hanja to Korean
if hanja:
lambda_client = boto3.client('lambda')
invoke_response = lambda_client.invoke(
FunctionName = "HanjaToKorean",
InvocationType = 'RequestResponse',
Payload = json.dumps({"inputText": originText})
)
data = invoke_response['Payload'].read()
resultText = json.loads(data)
replaceText = resultText['outputText']
print('Hanja to Korean Text: ' + replaceText)
else:
replaceText = originText
# Creating new record in DynamoDB table
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['DB_TABLE_NAME'])
table.put_item(
Item={
'id' : recordId,
'voice' : voice,
'text': originText,
'replaceText': replaceText,
'status' : "PROCESSING",
'updateDate': updateDate
}
)
# Sending notification about new post to SNS
client = boto3.client('sns')
client.publish(
TopicArn = os.environ['SNS_TOPIC'],
Message = recordId
)
return recordId |
...
- 세 개의 입력 매개 변수를 검색합니다.
- voice - Amazon Polly에서 지원하는 목소리 중 하나
- text - 오디오 파일로 변환하려는 게시물의 텍스트
- hanja - 한자를 한글로 변경할지 여부 확인 플래그
- 한자를 한글로 번역해야 할 경우 한글로 변환합니다.
- 새 게시물에 대한 정보가 있는 DynamoDB 테이블에 새 레코드를 만듭니다.
- 새 게시물에 대한 정보를 SNS에 게시합니다 (DynamoDB 항목의 id인 게시물 id가 메시지로 게시됩니다)
- 사용자에게 DynamoDB 항목의 id를 반환합니다.
...
- DB_TABLE_NAME - DynamoDB 테이블의 이름 (여기에서는 posts )
- SNS_TOPIC - 우리가 만든 SNS 주제의 Amazon Resource Name, ARN은 SNS 서비스의 주제에서 찾을 수 있습니다.
코드 바로 아래에 환경 변수에서 값을 넣어 줍니다.
...
실행 역할 에서 LambdaPostsRole 이 지정되어 있는지 확인하고, 제한 시간은 1분 으로 변경합니다.(Hanja to Korean 실행에서 발생할 수 있는 지연시간 고려)
" PostReader_NewPost " Lambda 기능이 준비되었습니다. 만약 테스트를 하려면, 다음 입력 데이터를 입력 데이터로 호출합니다.
코드 블럭 | ||||||
---|---|---|---|---|---|---|
| ||||||
{
"voice": "Seoyeon",
"text": "안녕, 난 서연이야. Polly 서비스에서 텍스트를 읽어주는 서비스를 제공하고 있어.",
"hanja": false
} |
...
6. "Hanja to Korean" Lambda 함수 만들기
이 예제에서는 한자를 한글로 변환하는 Lambda 함수는 Java로 제작합니다. 동작 로직은 텍스트에서 한자 코드가 발견될 경우, 첨부된 hanjatohangle.xml 파일의 매핑 정보를 이용하여 한자를 한글로 변환시켜 줍니다. Java 파일은 컴파일을 하고, Jar 파일을 생성하여 Lambda에 직접 업로드하여 배포할 수 있습니다.
여기서는 Maven을 사용하여 Java 코드를 빌드하고, 배포 패키지를 Lambda 함수에 배포하는 방법을 살펴 보겠습니다. (참고: IDE 없이 Maven을 사용하여 .jar 배포 패키지 만들기(Java))
...
Linux 일 경우 패키지 관리자를 이용해서 다음과 같이 설치합니다.
코드 블럭 |
---|
sudo apt-get install maven |
만약 Homebrew를 사용하는 경우에는 아래와 같이 설치합니다.
코드 블럭 |
---|
brew install maven |
...
프로젝트를 빌드 하기 위해서는 HanjaToKorean 폴더에서 다음의 명령을 수행합니다. Maven을 수행하기 전 JDK가 설치되어 있지 않다면, JDK를 설치합니다.
코드 블럭 |
---|
mvn package |
...
- 함수의 모든 설정이 완료되면 저장하고 테스트를 합니다. 테스트 이벤트 구성은 아래와 같습니다.
- 테스트 이벤트를 구성합니다. (식사 前後의 운동 효과)
- 테스트 결과를 확인 합니다. 한자가 한글로 정상적으로 변환되는 것을 확인합니다.
- PostReader_NewPost Lambda 함수 테스트로 다시 돌아가서 한자가 정상적으로 한글로 변환되는지 테스트 합니다.
- 다음의 문장을 테스트 합니다. (식사 前後의 운동 효과에 대해서 알아 봅시다.)
- 한자가 한글로 정상적으로 변환이 되는지 확인합니다.
7. "Convert to Audio" Lambda 함수 만들기
DynamoDB 테이블에 저장된 텍스트를 오디오 파일인 "Convert to Audio"로 변환하는 Lambda 함수를 만들어 보겠습니다.
...
Lambda 함수 코드를 작성하기 위해서 다시 PostReader_ConvertToAudio 를 클릭하고 함수 코드를 아래에 있는 코드로 대체합니다.
코드 블럭 | ||||||
---|---|---|---|---|---|---|
| ||||||
import boto3
import os
from contextlib import closing
from boto3.dynamodb.conditions import Key, Attr
def lambda_handler(event, context):
postId = event["Records"][0]["Sns"]["Message"]
print "Text to Speech function. Post ID in DynamoDB: " + postId
#Retrieving information about the post from DynamoDB table
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['DB_TABLE_NAME'])
postItem = table.query(
KeyConditionExpression=Key('id').eq(postId)
)
text = postItem["Items"][0]["replaceText"]
voice = postItem["Items"][0]["voice"]
rest = text
#Because single invocation of the polly synthesize_speech api can
# transform text with about 1,500 characters, we are dividing the
# post into blocks of approximately 1,000 characters.
textBlocks = []
while (len(rest) > 1100):
begin = 0
end = rest.find(".", 1000)
if (end == -1):
end = rest.find(" ", 1000)
textBlock = rest[begin:end]
rest = rest[end:]
textBlocks.append(textBlock)
textBlocks.append(rest)
#For each block, invoke Polly API, which will transform text into audio
polly = boto3.client('polly')
for textBlock in textBlocks:
response = polly.synthesize_speech(
OutputFormat='mp3',
Text = textBlock,
VoiceId = voice
)
#Save the audio stream returned by Amazon Polly on Lambda's temp
# directory. If there are multiple text blocks, the audio stream
# will be combined into a single file.
if "AudioStream" in response:
with closing(response["AudioStream"]) as stream:
output = os.path.join("/tmp/", postId)
with open(output, "a") as file:
file.write(stream.read())
s3 = boto3.client('s3')
s3.upload_file('/tmp/' + postId,
os.environ['BUCKET_NAME'],
postId + ".mp3")
s3.put_object_acl(ACL='public-read',
Bucket=os.environ['BUCKET_NAME'],
Key= postId + ".mp3")
location = s3.get_bucket_location(Bucket=os.environ['BUCKET_NAME'])
region = location['LocationConstraint']
if region is None:
url_begining = "https://s3.amazonaws.com/"
else:
url_begining = "https://s3-" + str(region) + ".amazonaws.com/" \
url = url_begining \
+ str(os.environ['BUCKET_NAME']) \
+ "/" \
+ str(postId) \
+ ".mp3"
#Updating the item in DynamoDB
response = table.update_item(
Key={'id':postId},
UpdateExpression=
"SET #statusAtt = :statusValue, #urlAtt = :urlValue",
ExpressionAttributeValues=
{':statusValue': 'UPDATED', ':urlValue': url},
ExpressionAttributeNames=
{'#statusAtt': 'status', '#urlAtt': 'mp3Url'},
)
return |
- '/index.html' APIEndpointURL: Description: URL of your API endpoint Value: 'Fn::Sub': >- https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/news
정적 웹 호스팅을 위한 파일 업로드하기
- 다음과 같이 정적 컨텐츠 파일을 다운로드 받습니다. Scripts.js 파일에 API Endpoint URL을 넣어주고, S3 버킷에 업로드 합니다.
정적 웹 호스팅 파일 다운로드 받기
코드 블럭 language bash theme RDark linenumbers true wget https://s3.ap-northeast-2.amazonaws.com/polly.awsdemokr.com/301_static_web.zip
압축 풀고 폴더 이동
코드 블럭 language bash theme RDark linenumbers true unzip 301_static_web.zip cd 301_static_web
Cloud9에서 scripts.js 파일 열어서 CloudFormation Stack에 배포된 Output의 APIEndpointURL 값을 소스코드에 반영 (WebsiteURL이 아니므로 주의)
코드 블럭 language js theme RDark linenumbers true var API_ENDPOINT = "https://xxxxxxxxxx.execute-api.ap-southeast-1.amazonaws.com/Prod/news/"; if (API_ENDPOINT === "") { alert("scripts.js 파일의 상단에 API Gateway에 배포한 URL을 등록하고 실행하세요."); }
정적 웹 포스팅하고자 하는 S3 버킷에 public-read 권한으로 파일을 업로드 (CloudFormation Stack에 배포된 Output의 S3WebBucket 값을 아래에 대체)
코드 블럭 language bash theme RDark linenumbers true aws s3 sync . s3://cloud9-webapp-staticwebbucket-xxxxxxxxxxxx --acl public-read
- 웹 브라우저로 정적 웹 페이지에 접속 (CloudFormation Stack에 배포된 Output의 WebsiteURL 값을 웹 브라우저 주소창에 입력)
- 다음과 같이 정적 컨텐츠 파일을 다운로드 받습니다. Scripts.js 파일에 API Endpoint URL을 넣어주고, S3 버킷에 업로드 합니다.
서비스 동작 테스트
- Chrome 웹 브라우저를 통해서 CloudFormation의 Stack에서 제공하는 WebsiteURL로 접속합니다. 아래와 같은 사이트가 S3를 통해서 웹 호스팅 되는 것을 확인합니다.
- PostNews를 호출하기 위해서 음성으로 변환하고자 하는 텍스트를 입력합니다.
- 음성 변환 시작 버튼을 클릭하여 텍스트를 음성으로 변환합니다.
- 텍스트가 음성으로 변환되면, 검색을 통해서 등록번호가 생성되는 것을 확인할 수 있습니다.
- 아래 검색 버튼을 클릭하면 변환되는 Text를 음성으로 변경한 정보를 확인할 수 있습니다. MP3가 제작 되기 전이라면, 다시 한 번 검색 버튼을 클릭합니다.
- 새롭게 만들어진 ID를 확인할 수 있습니다. 게시물 검색은 해당 ID를 이용할 수 있습니다. ElasticSearch를 이용하면, Text와 ID 정보를 등록하고 검색할 수 있게 서비스를 구축할 수 있습니다.
- 옵션을 조정해서 MP3를 만들 수 있습니다. 아래와 같이 정치적 기사는 SSML 옵션으로 음색과 음높이를 조절하여 텍스트에 적합한 목소리 형태로 변경할 수 있습니다. ConvertAudio 함수에 SSML 태그로 이루어진 것을 확인할 수 있습니다.
- 생성된 MP3를 재생하면서 옵션 변화에 따른 음성 변화를 확인할 수 있습니다.
- 생성된 MP3를 재생하면서 옵션 변화에 따른 음성 변화를 확인할 수 있습니다.
- Chrome 웹 브라우저를 통해서 CloudFormation의 Stack에서 제공하는 WebsiteURL로 접속합니다. 아래와 같은 사이트가 S3를 통해서 웹 호스팅 되는 것을 확인합니다.
SAM을 이용한 새로운 스택에 직접 배포
- SAM은 코드 기반의 AWS의 서버리스 클라우드 인프라에 대한 템플릿을 생성한 결과물입니다. 즉, 해당 SAM을 이용하면 서비스 배포를 할 수 있습니다. 이 작업은 CLI에서 진행하겠습니다.
먼저 SAM 템플릿 배포를 위해서 S3 버킷을 하나 생성(예: template-deploy-hyouk2 이름을 변경)합니다. 해당 S3 버킷에 패키지를 업로드 합니다.
코드 블럭 aws s3 mb s3://template-deploy-hyouk2
Cloud9 IDE 하단의 ~/environment 디렉토리에서 하단의 작업을 진행할 수 있습니다.
코드 블럭 sam package --template-file WebApp/template.yaml --output-template-file webapp-output.yaml --s3-bucket template-deploy-hyouk2
template-depoly-hyouk 버킷에는 배포에 필요한 아티팩트가 들어가 있습니다. 각 람다 함수별로 패키징된 임의의 파일들이 생성되는 것을 확인할 수 있습니다.
각각의 파일은 ZIP 파일로 바로 Lambda 함수에 배포할 수 있는 코드의 묶음입니다. 직접 다운로드 해서 unzip을 하면 확인할 수 있습니다.다음과 같이 WebAppTest 라는 새로운 스택으로 배포를 진행할 수 있습니다. webapp-outpuy.yaml 파일에는 Function의 CodeUri가 S3로 변경되어져 있는 것을 확인할 수 있습니다.
코드 블럭 sam deploy --template-file /home/ec2-user/environment/webapp-output.yaml --stack-name WebAppTest --capabilities CAPABILITY_IAM
- CloudFormation의 WebAppTest Stack으로 이동하여 정상 배포가 되는 것을 확인합니다.
- 이를 통해서 추후 CodeBuild에서 Build 단계에서도 동일한 작업이 이뤄지는 것을 확인할 수 있습니다.
- SAM은 코드 기반의 AWS의 서버리스 클라우드 인프라에 대한 템플릿을 생성한 결과물입니다. 즉, 해당 SAM을 이용하면 서비스 배포를 할 수 있습니다. 이 작업은 CLI에서 진행하겠습니다.
실습 자료 삭제
- CloudFormation에서 배포되어 있는 Stack을 삭제합니다.
- 11번에서 수동 배포한 Stack: WebAppTest
- Cloud9 IDE에서 배포한 Stack: Cloud9-WebApp
- Cloud9 IDE 환경을 생성한 Stack: aws-cloud9-NewWepApp-xxxxxxxxxx
- 삭제시 S3 버킷에 파일이 있을 경우, 삭제가 안될 수 있습니다. 관리 콘솔에서 S3 서비스로 이동한 다음 관련 버킷을 직접 삭제하시면 됩니다.
- CloudFormation에서 배포되어 있는 Stack을 삭제합니다.
...
입력 메시지 (SNS 이벤트)에서 오디오 파일로 변환해야하는 DynamoDB 항목의 ID (게시물 ID)를 검색합니다.
- DynamoDB에서 변환에 필요한 텍스트를 추출합니다. synthesize_speech API의 입력 텍스트 크기 제한이 1,500 자이기 때문에 1,000 자의 블록으로 나누어서 호출합니다. 각각의 블록은 오디오 스트림으로 변환 한 후 다시 결합합니다.
- 텍스트를 오디오 스트림으로 변환합니다.
- 오디오(MP3) 파일을 S3 버킷에 배포합니다.
- S3 버킷 및 새 상태에 대한 참조로 DynamoDB 테이블을 업데이트합니다.
...
- DB_TABLE_NAME – DynamoDB 테이블의 이름 (이 경우에는 posts 입니다)
- BUCKET_NAME – MP3 파일을 저장하기 위해 만든 S3 버킷의 이름 (이 예제에서의 버킷 이름은 polly-mp3.awsdemokr.com 입니다.)
- 변환하려는 게시물이 상당히 클 수 있으므로 단일 코드 실행의 최대 길이인 5분 으로 연장합니다.
...
8. "Get Post" Lambda 함수 만들기
세 번째 Lambda 함수는 데이터베이스에서 게시물에 대한 정보를 검색하는 메소드를 제공합니다.
...
이번 코드는 매우 짧습니다. 이 함수는 게시물 id (DynamoDB 항목의 id)를 얻고 이 id를 기반으로 모든 정보(오디오 파일이있는 경우 S3 링크 포함)를 반환합니다. 입력 매개 변수가 별표(*)인 경우 좀 더 사용자 친화적인 것으로 만들기 위해 Lambda 함수는 데이터베이스에서 모든 항목을 반환합니다. (항목이 많을 경우 성능이 저하 될 수 있고 오랜 시간이 걸릴 수 있으므로 테스트용으로 사용하지만, 이 방법을 추천하지 않습니다.)
코드 블럭 | ||||||
---|---|---|---|---|---|---|
| ||||||
import boto3
import os
from boto3.dynamodb.conditions import Key, Attr
def lambda_handler(event, context):
postId = event["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))
return items["Items"] |
...
다음을 입력 데이터로 함수를 실행하여 함수를 테스트하십시오.
코드 블럭 |
---|
{
"postId": "*"
} |
...
9. Lambda 함수를 RESTful 웹 서비스로 만들기
마지막으로해야 할 일은 애플리케이션 로직을 RESTful 웹 서비스로 노출시켜 표준 HTTP 프로토콜을 사용하여 쉽게 호출 할 수 있도록 합니다. 이를 위해 Amazon API Gateway를 사용합니다.
- API Gateway 콘솔에서 시작 버튼을 눌러서 API를 생성을 시작합니다.
- 새 API 를 생성할 것이고 API의 리소스 이름은 PostReader 로 할당하고, API 생성 버튼을 클릭합니다.
- API가 생성 된 후, 우리는 두 개의 HTTP 메소드(작업 버튼을 클릭 후 메서드 생성)를 생성하고 CORS 설정을 합니다.
- POST 메서드는 PostReader_NewPost Lambda 함수를 호출합니다.
- GET 메소드의 경우 API는 PostReader_GetPost Lambda 함수를 호출합니다.
- 마지막은은 CORS 활성화(교차 출처 자원 공유) 설정입니다. 이 메소드를 사용하면 다른 호스트 이름이 있는 웹 사이트에서 API를 호출 할 수 있습니다.
- POST 메서드는 PostReader_NewPost Lambda 함수를 호출합니다.
- 반환해야하는 게시물의 ID에 대한 정보를 제공하는 쿼리 매개 변수인 postId에 대해 GET 메서드를 구성합니다.
- GET 메서드를 클릭하고 메서드 요청을 클릭합니다.
- GET 메서드 요청에서 URL 쿼리 문자열 파라미터를 클릭해서 postId 대한 쿼리 문자열 정보를 추가합니다.
- Lambda 함수(PostReader_GetPost)는 입력 데이터를 JSON 형식으로 수신하기 때문에 쿼리 문자열 변수를 JSON 형식으로 매핑하도록 API를 구성해야 합니다. 이를 위해 통합 요청 을 클릭합니다.
쿼리 문자열 파라미터인 postId를 Lambda가 인식할 수 있는 JSON 형태로 다음과 같이 매핑 테이블을 작성하고 저장합니다.
코드 블럭 language js title 쿼리 문자열 변수를 JSON으로 매핑 linenumbers true { "postId" : "$input.params('postId')" }
- GET 메서드를 클릭하고 메서드 요청을 클릭합니다.
API 설정이 완료 되었습니다. 배포를 해서 애플리케이션에서 호출할 수 있는 URL을 얻습니다. 작업 에서 API 배포 를 선택합니다.
API를 Dev 스테이지로 배포를 합니다. 개발, 테스트, 프로덕션에 이르기까지 다양한 스테이지로 나누어서 배포가 가능합니다. 여기서는 dev 스테이지로 배포합니다.
API 배포까지 완료되었습니다. 해당 API를 호출 할 수 있는 URL이 생성되었음을 확인할 수 있습니다.
배포가 완료되면 해당 API를 호출할 수 있는 URL이 표시됩니다. 앞으로 동적 컨텐츠 API 호출은 이 URL로 호출할 것이기에 해당 URL을 메모합니다.
10. 정적 웹 서비스를 위한 S3 버킷 생성 및 배포
Amazon S3는 정적 웹 페이지를 호스팅 할 수 있습니다. 다음의 링크를 통해 정적 웹 호스팅을 하기 위한 패키지를 다운로드 할 수 있습니다: 3개의 파일(html, css, javascript)가 포함되어져 있으며, javascript를 사용하여 동적 컨텐츠 API 호출을 API Gateway로 연결합니다.
다음의 순서로 진행합니다.
...
마지막 단계는 우리 웹 사이트에 모든 사람이 액세스 할 수 있도록 버킷의 권한을 변경하는 것입니다. 권한 탭에서 다음 정책을 추가하여 버킷 정책을 편집하십시오. 12번째 줄에 BUCKET_NAME 을 방금 생성한 S3 버킷의 이름으로 교체 하십시오.
코드 블럭 | ||||
---|---|---|---|---|
| ||||
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"PublicReadGetObject",
"Effect":"Allow",
"Principal":"*",
"Action":[
"s3:GetObject"
],
"Resource":[
"arn:aws:s3:::BUCKET_NAME/*"
]
}
]
} |
11. 최종 테스트
모든 준비가 끝났습니다. 정적 웹 사이트 호스팅 탭에서 URL을 찾아서 웹 사이트가 작동하는지 확인할 수 있습니다. 상단의 정적 웹 페이지 호스팅에 나와 있는 엔드포인트로 접속하면 아래와 같은 웹 페이지가 나타납니다.
- 먼저 음성으로 변환하고자 하는 목소리를 선택합니다. 한국어 지원이 되는 Seoyeon을 선택합니다. 그리고 텍스트를 입력합니다. 한자 변환이 필요할 경우 체크 합니다. 마지막으로 음성 변환 시작 버튼을 누릅니다. API Gateway를 통해서 POST 메서드로 등록 메시지가 전달 됩니다. Lambda 함수에서 바로 인식할 수 있도록 POST Data는 JSON 형태로 전송합니다.
- 게시물이 등록되면 게시물 등록 번호가 자동으로 생성됩니다. 등록된 게시물의 텍스트는 SNS 주제가 트리거 되면서 실행되는 Lambda 함수에 의해 MP3를 생성하고 다음과 같이 게시물 등록 번호로 조회가 가능합니다. 입력하는 텍스트의 크기에 따라 오디오 파일로 변환하는 데 몇 초 또는 몇 분이 걸릴 수 있습니다. 제공한 게시물 정보를 검색하려면 게시물 등록 번호 검색에 게시물 ID 또는 *를 입력하십시오. 조회를 하면 텍스트를 확인하거나 음성으로 변환된 MP3를 재생할 수 있습니다.
결론
이 게시물에서 텍스트를 수십 개의 언어로 음성으로 변환하고 그 텍스트를 훨씬 더 많은 목소리로 말할 수있는 애플리케이션을 만들었습니다. 블로그 게시물을 음성으로 변환하는 애플리케이션을 만들었지만 웹 사이트에서 텍스트를 변환하거나 웹 애플리케이션에서 음성 기능을 추가하는 등의 다른 목적으로 블로그 게시물을 사용할 수 있습니다. 그리고 서버리스 서비스들만 이용해서 구축했습니다. 즉, 유지 관리하거나 패치해야 하는 서버들이 존재하지 않습니다. 기본적으로 AWS Lambda, Amazon API Gateway, Amazon S3 및 Amazon DynamoDB는 다수의 가용 영역을 사용하기 때문에 애플리케이션은 고가용성(HA)을 가집니다. 뿐만 아니라, Wordpress를 사용하고 있다면, Wordpress Polly Plugin을 이용하면 손쉽게 Polly를 사용할 수 있습니다. 그럼 이 다음에는 무엇을 할 수 있을까요? 이런 접근법을 사용하면 이전에 가능했던 것보다 더 나은 사용자 경험을 제공하는 새로운 애플리케이션을 상상하고 구축할 수 있습니다.
...
...
이 글은 Tomasz Stachlewski가 작성한 Build Your Own Text-to-Speech Applications with Amazon Polly 의 블로그를 한국어 콘솔에 맞게 번역 및 편집 하였습니다.
- 김현수 솔루션스 아키텍트는 회사가 디지털 변환을 가속화 할 수 있게 해주는 서버리스(serverless) 아키텍처와 같은 혁신적인 기술과 인공지능(AI/ML)에 관심이 많습니다.
기타 자료
- 가나다
...