LINE ChatBot 制作④「ポケモンの名前を入力すると、おすすめのヨーヨーを教えてくれる」 -メイン処理 実装編-

ヨーヨー画像:https://yoyostorerewind.com/products/joyride

こんにちは、KimYasです。
前回は、 S3とCloudFrontの実装を行いました。
今回からはいよいよ、メインの処理となるコードの記述を行っていきます。
早速行きましょう!

(参考)
LINE ChatBot 制作①「ポケモンの名前を入力すると、おすすめのヨーヨーを教えてくれる」- 概要編 -
LINE ChatBot 制作②「ポケモンの名前を入力すると、おすすめのヨーヨーを教えてくれる」 -LINE Developers 登録編-
LINE ChatBot 制作③「ポケモンの名前を入力すると、おすすめのヨーヨーを教えてくれる」 -S3 実装編-

今回の実装箇所

今回は、Lambda, API Gateway, DynamoDBを実装していきます。
AWSをあまりよく知らない方のために、それぞれの役割を簡単に書くと次のようになります。

  • Lambda…メイン処理を行うコンピューティングリソース。「●●というポケモンの名前が入力されたら、■■というヨーヨーの名前を返す」という処理を行う。
  • API Gateway…LINEアプリ(クライアント)とLambda(サーバー)とを繋ぐ橋のようなもの。LINEアプリ上で何かしらのイベントが起こったときに、Lambdaを起動する。
  • DynamoDB…NoSQL型のデータベース。151匹分のポケモン名や説明文、画像URL、ヨーヨー名などの情報を格納する。Lambdaからの要求に応じて、必要な情報を取り出してLambdaに渡す。

つまり、図の番号を用いて処理の流れを説明すると、

②LINE上でメッセージを受信すると、API GatewayがLambdaを起動する。Lambdaはレスポンス用の情報を作る処理(メイン処理)を始める。
③メッセージをもとに、Lambdaが必要な情報をDynamoDBに問い合わせる。
④DynamoDBがテーブル内から必要な情報を取り出し、Lambdaに返却する。
⑤LambdaがAPI Gatewayに対し、ステータスコード200を返す。
⑥ API GatewayがLINEプラットフォームに対し、 ステータスコード200を返す。
⑦ LambdaがReply URLに対し、 完成したレスポンス情報をPOSTリクエストで返却する。

このようになります。
今回の実装は、DynamoDB→Lambda→API Gatewayの順で行っていきます。

DynamoDB 実装

AWSコンソールからDynamoDBを選択し、画面右上の「テーブルの作成」をクリックします。


テーブル名は自由に設定します。

パーティションキーは、テーブルの項目を一意に区別できるような属性があれば、それを指定します。
今回はポケモンの名前が一意になるので、"PokemonName"とします。

ソートキーは今回は指定しませんが、パーティションキーだけでは項目を一意に区別できないような場合に指定します。
例えば、テーブル属性に「進化前ポケモン名」と「進化数」があった場合、
パーティションキーに「進化前ポケモン名」、ソートキーに「進化数」を指定すれば、両者を組み合わせることにより、項目を一意に特定できますね。
具体的に書くと、このようなイメージです。

{"進化前ポケモン名": "ゼニガメ", "進化数": 0} # ゼニガメ
{"進化前ポケモン名": "ゼニガメ", "進化数": 1} # カメール
{"進化前ポケモン名": "ゼニガメ", "進化数": 2} # カメックス
{"進化前ポケモン名": "ピカチュウ", "進化数": 1} # ライチュウ
{"進化前ポケモン名": "バリヤード", "進化数": 0} # バリヤード

設定の欄は、こだわりがなければデフォルト設定のままで大丈夫です。

デフォルトではこのような設定になっているかと思います。
読み込み/書き込みキャパシティーはDynamoDBの料金に影響してきますが、プロビジョンドキャパシティモードならば、無料利用枠で25WCU及び25RCUを使用できます(2022年2月時点)。
他にDynamoDBのテーブルを立ち上げていなければ、デフォルトは5WCU及び5RCUなので無料利用枠内に収まります。
無料利用枠は変更になる可能性がありますので、詳細は公式ページをご覧ください。

このまま画面を下までスクロールし、右下にある「テーブルを作成」をクリックします。

テーブルを作成できたら、画面右上にある「項目を作成」ボタンをクリックします。

テーブルに追加したい項目の属性と値をJSON形式で入力します。
入力できたら、画面右下の「項目を作成」ボタンをクリックします。
これを151回繰り返すことで、151匹分のポケモンデータをDynamoDBに登録できます。気合で頑張りましょう!

…というのは大変すぎますよね。
何かいい方法は無いかなあと調べてみると、あっさり解決方法が見つかりました。
AWS公式ブログに、CSVファイルをDynamoDBへ一括で取り込む方法が掲載されていましたので、その手順に従いました。
また私の方でも手順をまとめ、近々記事をアップしたいと思います。

ということで、テーブルが完成しました。
151匹分のコメントを書くのはAWSの環境構築以上に時間がかかりましたが、楽しかったので辛さはゼロでした。
所々に深夜テンションで書いたコメントが紛れているので、よかったら探してみてください。
(完成版のLINE ChatBotには、こちらの記事のリンクから友だち登録できるので是非どうぞ!)

DynamoDBの実装はここまでです。
続いて、Lambdaを実装していきましょう。

Lambda実装

AWSコンソールからLambdaを選択します。

画面右上にある、「関数の作成」をクリックします。

関数名は好きなように入力します。
また、今回はPythonでコードを書いていくので、ランタイムはPython3.9を選びました。
アーキテクチャはデフォルトのx86_64ままでOKです。
画面右下にある、「関数の作成」をクリックします。

Lambda関数が作成できました。
作成した関数をクリックして開きます。

最初から「コード」タブが選択されており、lambda_function.pyというファイルが作成されています。
画面中央にエディタ画面があるので、ここにコードを書いていきます。

import json
import os
import urllib.request
import boto3
import logging
import random

logger = logging.getLogger()
logger.setLevel(logging.INFO)

LINE_CHANNEL_ACCESS_TOKEN   = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
CLOUDFRONT_DISTRIBUTION_URL = os.environ['CLOUDFRONT_DISTRIBUTION_URL']
REPLY_URL = os.environ['REPLY_URL']
KIMYAS_PROFILE_URL= os.environ['KIMYAS_PROFILE_URL']

def lambda_handler(event, context):
    for message_event in json.loads(event['body'])['events']:
        #イベントからユーザーの入力文字列を取得
        input_word = message_event['message']['text'] # ここでのエラーはAPIGatewayで弾く
        logger.info([input_word,message_event['source']['userId']])

        # dynamoDBからデータ取得し、タプルで変数に格納
        data_tuple = get_data_from_dynamoDB(input_word)
        
        # レスポンス用のheaderとbodyを作成(必要事項はLINEの公式ドキュメントで定義されている)
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN # Bearerの直後には空白が必要(仕様)
        }
        body = {
            'replyToken': message_event['replyToken'],
            'messages': [
                {
                    "type": "template",
                    "altText": data_tuple[4],
                    "template": {
                      "type": "buttons",
                      "thumbnailImageUrl": data_tuple[0],
                      "imageAspectRatio": "square",
                      "imageSize": "contain",
                      "imageBackgroundColor": "#FFFFFF",
                      "title": data_tuple[3] + "-" + data_tuple[4],
                      "text": data_tuple[1],
                      "defaultAction": {
                          "type": "uri",
                          "label": "詳細ページ",
                          "uri": data_tuple[2]
                      },
                      "actions": [
                          {
                            "type": "uri",
                            "label": "もっと詳しく!",
                            "uri": data_tuple[2]
                          },
                          {
                            "type": "message",
                            "label": data_tuple[5] + "を調べてみる",
                            "text": data_tuple[5]
                          },
                          {
                            "type": "uri",
                            "label": "このBotの開発者について",
                            "uri": KIMYAS_PROFILE_URL
                          }
                      ]
                    }
                }
            ]
        }
        
        req = urllib.request.Request(REPLY_URL, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers) # HTTPリクエストを作成
        with urllib.request.urlopen(req) as res: # 引数のreq(URL)をオープン(=HTTPリクエストを実行)。返り値resには、HTTPResponseクラスのオブジェクトが返送される。
            logger.info(res.read().decode("utf-8")) # 成功ならステータスコード200と空のJSONオブジェクト{}が返ってくる(LINEドキュメントより)

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
    
def get_data_from_dynamoDB(input_word):
    dynamoDB = boto3.resource('dynamodb')
    table = dynamoDB.Table('テーブル名') #'テーブル名'には、DynamoDBのテーブル名を入れる
    target_item = table.get_item(Key={'PokemonName': input_word})['Item']

  # target_itemの各値を取り出して変数に格納
    s3_url = CLOUDFRONT_DISTRIBUTION_URL + target_item['S3ObjectName']
    yoyo_maker = target_item['YoyoMaker']
    yoyo_item = target_item['YoyoItem']
    comment = target_item['Comment']
    rewind_url = target_item['RewindURL']
    next_pokemon = target_item['NextPokemon']
    
    return(s3_url, comment, rewind_url, yoyo_maker, yoyo_item, next_pokemon)# 要素の順番を替える場合は、メイン処理のタプル順も編集すること

コード内に出てくる環境変数は、「設定」タブの「環境変数」で設定できます。
他人に知られたくない情報や、将来的に変更の可能性があるURL等の文字列は、ここに格納しましょう。
セキュリティ及びコードの保守性が向上します。

12行目の"CLOUDFRONT_DISTRIBUTION_URL"には、ヨーヨーの画像を格納しているS3バケットに紐づいている、CloudFrontのディストリビューション名が入っています。つまるところ、86行目が

s3_url = "ヨーヨーの画像URL"

となるように記述できれば、環境変数を用いなくてもOKです。

また、11行目の"LINE_CHANNEL_ACCESS_TOKEN"には、前回の記事で登録したLINE Developersのチャネルアクセストークンを格納します。
LINE Developers にログインし、作成したチャネルのMessaging API設定からチャネルアクセストークンをコピーし、Lambdaの環境変数に設定しましょう。
(詳しくは、LINE Developers登録編の記事をご覧ください。)

Lambda実装はここまでです。
続いて、API Gatewayを実装します。

API Gateway 実装

AWSコンソールからAPI Gatewayを選択し、画面右上の「APIを作成」をクリックします。

今回はREST APIで作成したいので、REST APIの「構築」ボタンをクリックします。


任意のAPI名を入力します。
エンドポイントタイプはデフォルトの「リージョン」のままでOKです。
画面右下の「APIの作成」ボタンをクリックします。

画面左メニューバーの「リソース」を開き、画面上部「アクション」から「リソースの作成」をクリックします。

任意のリソース名を入力します。ここでは"sample"としました。
画面右下の「リソースの作成」をクリックします。

作成したリソースを選択した状態で、画面上部の「アクション」から「メソッドの作成」をクリックします。

ドロップダウンリストからPOSTを選択し、リスト右横のチェックマーク✅をクリックしてメソッドを作成します。

作成したPOSTメソッドを選択して、設定を行います。
「統合タイプ」はLambda関数を選択します。
「Lambdaプロキシ統合の使用」のチェックボックスはデフォルトで入っていないので、チェックします。
「Lambda関数」には、作成したLambda関数のARNまたは関数名を入力します。
「デフォルトタイムアウトの使用」はチェックを入れたままでOKです。
画面右下の「保存」ボタンをクリックします。

このような画面が出るので、OKをクリックします。

(参考)
ここでOKを押したことにより、Lambdaに次のようなリソースベースポリシーが追加されます。
興味のある方は、作成したLambda関数の「設定」→「アクセス権限」→「リソースベースのポリシー」→「ポリシードキュメントを表示」を確認してみてください。

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "xxxxxxxx-1111-2222-3333-44444444444",
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:111111111111:function:hoge_function",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:execute-api:ap-northeast-1:111111111111:abcdefghijk/*/POST/hoge"
        }
      }
    }
  ]
}



少し脱線しましたが、API Gatewayに戻ります。

画面上部の「アクション」から「APIのデプロイ」をクリックします。

このような画面が出るので、「デプロイされるステージ」は「新しいステージ」を選択します。
「ステージ名」に任意の名前を入力します。ここではtest_stageとしました。
画面右下の「デプロイ」をクリックします。

デプロイが完了しました。
画面上部にある「URLの呼び出し」に表示されているURLが、このAPI Gatewayに外部からアクセスする際のURLあたります。
つまり、このURLをLINEアプリのWebhook URLに指定することで、LINEアプリとAPIGatewayとを繋ぐことができます。

ということで、このURLをコピーし、LINE Developersの管理画面を開いてWebhookURLに設定します。
※LINE Developersについてはこちらの記事で詳しく解説しています。



以上でAPI Gatewayの設定は完了です。
これでChatBotが動作するための設定が一通り完了しました。
ここまでお疲れ様でした!

動作確認

トークルームでポケモンの名前を入力すると、おすすめのヨーヨーが画像と共に返ってきます。初めてエラー無く動いた瞬間は感動しました。

1回目のレスポンスには1秒強ほどかかりますが、2回目からは1秒以内に返ってきますね。
これはLambdaのコールドスタートという仕様であり、初回起動時に実行環境を構築する分どうしても時間を要します。

対策はいくつかあり、オーソドックスなのはLambdaの同時実行数をプロビジョニング(確保)しておくことです。
別料金が発生するので、どの程度の遅延までなら許されるのか考慮して、予算が許されるのであれば導入してみると良いですね。
今回は個人利用の範囲なので、このままで十分だと考えています。

また、青紫色のヨーヨーの写真では、トークルーム下部にメニューボタンがありますね。これはLINEのリッチメニューという機能で、実装することでユーザーが入力しやすい画面を作ることができます。
この作り方については、また記事をアップしたいと思います。

まとめ

ここまで4回にわたってChatBotの制作過程を解説してきました。
上図のうちLightsail(DNS Zone)とCertigicate Managerはまだ導入していませんが、一通りChatBotとして動作する機能は実装しました。
Lightsail(DNS Zone)とCertigicate Managerについては番外編という形で、スポット的に解説をしていきたいと思います。

AWSをバックグラウンドにしたChatBot制作は大変な分、完成したときには大きな達成感を感じられます。
また、本やネット上の解説を読んでAWSの勉強をするだけでなく、コンソールを触ったり構成図を書くなど実際に手を動かすことで、AWSのサービスに対する理解が格段に深まりました。
これからAWSのサービスを使ってみたいという方は、是非ChatBot制作をしてみてはいかがでしょうか。
私の記事が参考になれば幸いです。

それでは次の記事でお会いしましょう!

KimYas