🤖

IaCを使用して、Amazon Lex から本気で Lambda 関数を呼び出す

2025/03/03に公開

はじめに

現在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