🤖
IaCを使用して、Amazon Lex から本気で Lambda 関数を呼び出す
はじめに
現在Amazon Lexを使用してサービスの開発を行っている。しかし、調べても花屋のサンプルばかりで、実際にAmazon LexからLambda関数を呼び出す方法をIaCで構築するのに苦慮した。そこで、今回はAmazon LexからLambda関数を呼び出す方法を紹介する。
対象となる読者は、AWSのサービス、特にLambdaなどのサーバーレスサービスを良く利用しているが、Amazon Lexが初めて、またはLambdaとの連携について知りたい方を想定している。
アーキテクチャ構成
デプロイ構成
AWS SAMでLambda関数を作成する
今回はAWS SAMを使用してLambda関数を作成する。
コマンドライン引数は省略するが、以下言語はPythonで説明する。
# 初期化
sam init
# ビルド・デプロイ
sam build
sam deploy
CloudFormationテンプレートで利用するため、AWS SAMのtemplate.yamlで関数名を明記すると良い。
Resources:
...
MyFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName:
Fn::Sub: ${AWS::StackName}-function
...
CloudFormationでAmazon Lexを作成する
今回はCloudFormationを使用してAmazon Lexを作成する。
最初はAWS CDKを使用していたが、エラー解析が難しかったため、CloudFormationに切り替えた。
AWSTemplateFormatVersion: 2010-09-09
Description: Amazon LexのCloudFormationテンプレート。
Parameters:
BotName:
Type: String
Description: ボットの名前。
LambdaFunctionName:
Type: String
Description: Lambda関数の名前。上記のAWS SAMで作成した関数名を指定する。
VoiceId:
Description: 音声出力の声のID。
Type: String
Default: Mizuki
Resources:
LexBot:
Type: AWS::Lex::Bot
Properties:
DataPrivacy:
ChildDirected: false
IdleSessionTTLInSeconds: 60
Name: !Sub ${BotName}
RoleArn: !GetAtt LexBotRole.Arn
AutoBuildBotLocales: true
BotLocales:
- LocaleId: ja_JP
NluConfidenceThreshold: 0.5
VoiceSettings:
VoiceId: !Ref VoiceId
Intents:
- Name: IntentA
ParentIntentSignature: "AMAZON.RepeatIntent"
Description: IntentA
DialogCodeHook:
Enabled: true
- Name: IntentB
ParentIntentSignature: "AMAZON.CancelIntent"
Description: IntentB
DialogCodeHook:
Enabled: true
- Name: FallbackIntent
ParentIntentSignature: "AMAZON.FallbackIntent"
Description: FallbackIntent
TestBotAliasSettings:
BotAliasLocaleSettings:
- LocaleId: ja_JP
BotAliasLocaleSetting:
Enabled: true
CodeHookSpecification:
LambdaCodeHook:
LambdaArn: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunctionName}"
CodeHookInterfaceVersion: '1.0'
ConversationLogSettings:
TextLogSettings:
- Enabled: true
Destination:
CloudWatch:
CloudWatchLogGroupArn: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudWatchLogGroup}"
LogPrefix: TestBotAlias/
AudioLogSettings:
- Enabled: true
Destination:
S3Bucket:
S3BucketArn: !GetAtt S3AudioLogBucket.Arn
LogPrefix: TestBotAlias/
LexBotAlias:
Type: AWS::Lex::BotAlias
Properties:
BotAliasName: !Ref BotName
BotId: !Ref LexBot
BotAliasLocaleSettings:
- LocaleId: ja_JP
BotAliasLocaleSetting:
Enabled: true
CodeHookSpecification:
LambdaCodeHook:
LambdaArn: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunctionName}"
CodeHookInterfaceVersion: '1.0'
ConversationLogSettings:
TextLogSettings:
- Enabled: true
Destination:
CloudWatch:
CloudWatchLogGroupArn: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudWatchLogGroup}"
LogPrefix: !Sub ${BotName}/
AudioLogSettings:
- Enabled: true
Destination:
S3Bucket:
S3BucketArn: !GetAtt S3AudioLogBucket.Arn
LogPrefix: !Sub ${BotName}/
LexBotRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${BotName}-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lexv2.amazonaws.com
Action:
- sts:AssumeRole
# Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaRole
Policies:
- PolicyName: !Sub ${BotName}-policy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- s3:PutObject
Resource:
- !Sub "arn:aws:s3:::${S3AudioLogBucket}/*"
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudWatchLogGroup}:*"
CloudWatchLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lex/${BotName}"
RetentionInDays: 365
S3AudioLogBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${BotName}
Lambda関数を実装する
Amazon LexからLambda関数が呼び出されることを確認したら、Lambda関数を実装する。
ボットから呼び出されるLambda関数は1つのため、lambda_handler関数内でIntentごとに処理を分岐する。
from logging import getLogger
from traceback import format_exc
def lambda_handler(event: dict, context):
session_id = event['sessionId']
intent_name: str = event['sessionState']['intent']['name']
slots = event['sessionState']['intent']['slots']
logger = getLogger(intent_name)
logger.info(f'START {event=}')
if intent_name == 'IntentA':
try:
# IntentAの処理
return create_response(intent_name, slots, response_text)
except Exception as e:
logger.error(e, extra={'error': format_exc().splitlines()})
return create_response(intent_name, slots, '予期せぬエラーが発生しました。', 'Failed')
elif intent_name == 'IntentB':
try:
# IntentBの処理
return create_response(intent_name, slots, response_text)
except Exception as e:
logger.error(e, extra={'error': format_exc().splitlines()})
return create_response(intent_name, slots, '予期せぬエラーが発生しました。', 'Failed')
def create_response(intent_name: str, slots, response_text: str, state: str = 'Fulfilled') -> dict:
"""レスポンスを生成する
state: Failed | Fulfilled | FulfillmentInProgress | InProgress | ReadyForFulfillment | Waiting
"""
return {
'sessionState': {
'dialogAction': {
'type': 'Close',
},
'intent': {
'name': intent_name,
'slots': slots,
'state': state,
}
},
'messages': [{'contentType': 'PlainText', 'content': response_text}]
}
create_response関数は、Amazon Lexに返却するレスポンスを生成する関数である。関数の仕様に応じて、適宜変更する。
おわりに
この記事を通じて、Amazon LexとLambdaの連携方法で悩んでいる同志が解決の糸口を見つけられることを願っている。
Discussion