【入門編①】Serverless Framework で 「おうむ返し」LINE Bot を作る

記事タイトルとURLをコピーする

こんにちは。技術4課の河野です。
今回は、Serverless Framework を使用してLINEBot を作成する方法について紹介します。

作成するもの

LINEBot 入門編では定番の「おうむ返しBot」を作成していきます。

f:id:swx-go-kawano:20200903165805p:plain

構成図

以下のようなAWSサービスを使用してLINEBotを実現します。

f:id:swx-go-kawano:20200903174204p:plain

①:利用者がLINEからメッセージを送信すると、LINEプラットフォームでWebhookイベントが発生します。
Webhookには、メッセージ内容やメッセージを返信するために使用するreplyTokenなどが含まれます。
オブジェクトの詳細は公式ドキュメントに記載されています。

②:Webhook のリクエストは、Amazon API Gateway(以下API Gateway) のエンドポイントに送信されます。

③:API Gateway は Webhook の HTTP リクエストを受けて、AWS Lambda(以下Lambda) を発火します。

④:Lambda は Webhook のオブジェクト内容をそのまま Amazon SQS(以下SQS) にキューイングし、即座にLINEプラットフォーム側にステータス200を返却します。

⑤:SQSにメッセージがキューイングされたタイミングで、後続の Lambda が発火します。

⑥:Lambda は SQSのメッセージ内容を取得し、LINEのトーク画面に返信します。

メッセージの受信と送信は、SQSを使用して非同期構成にしている点がポイントです。
公式ドキュメントも非同期構成を推奨されています。

HTTP POSTリクエストの処理が後続のイベントの処理に遅延を与えないよう、イベント処理を非同期化することを推奨します。

また、法人向け開発ガイドラインにも以下の記載があります。

ボットサーバーでリクエストを受信したら、まずは迅速にステータスコード200を返却するようにしてください。 ステータスコードの返却は、1000ミリ秒(1秒)以内が目安です。1秒を超過した場合はエラーメールが自動で送信されます。

環境

  • LINE Messaging API Channel は作成済とします。
  • Python 3.8.2
  • pipenv
  • aws-cli 1.18.3
  • serverless Framework Core 1.81.1

実装

Serverless プロジェクトの作成

Serverless Framework を使用して、構成図で示したリソースをプロビジョニングします。

serverless create でServerless プロジェクトを作成します。

serverless create --template aws-python3 --path sls-line-oumu-app

Serverless Framework で使用するプラグインをインストールします。

serverless plugin install --name serverless-python-requirements

serverless.yml を開き、以下のように編集します。

service: line-oumu-bot

provider:
  name: aws
  runtime: python3.8
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}
  timeout: 60
  environment:
    LINE_CHANNEL_SECRET: XXXXXXXXXX
    LINE_CHANNEL_ACCESS_TOKEN: XXXXXXXXX

plugins:
  - serverless-python-requirements
custom:
  defaultStage: dev

package:
  exclude:
    - .mypy_cache/**
    - .venv/**
    - node_modules/**
    - .gitignore
    - package-lock.json
    - package.json
    - Pipfile
    - Pipfile.lock

functions:
  enqueue:
    handler: bot_enqueue.lambda_handler
    description: lineからのwebhookをsqsにキューイングします
    timeout: 60
    memorySize: 256
    role: BotEnqueueRole
    environment:
      QUEUE_URL:
        Ref: BotJobQueue
    events:
      - http:
          path: /
          method: post
          response:
            headers:
              Content-Type: "'application/json'"
            template: $input.path('$')
            # カスタムレスポンスコードの設定
            statusCodes:
                200:
                    pattern: ''
                    template: $input.path("$.body")
  execute:
    handler: bot_execute.lambda_handler
    description: sqsメッセージを解析して、LINEに返信します(line-bot-sdk-python)
    timeout: 60
    memorySize: 256
    role: BotExecuteRole
    environment:
      QUEUE_URL:
        Ref: BotJobQueue
    events:
      - sqs:
          arn: !GetAtt BotJobQueue.Arn
          batchSize: 1

resources:
  Resources:
    BotJobQueue:
      Type: AWS::SQS::Queue
      Properties:
        KmsMasterKeyId: alias/aws/sqs
        MessageRetentionPeriod: 72000
        Tags:
        - Key: AppName
          Value: ReserveBot
        VisibilityTimeout: 60
    BotEnqueueRole:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        Policies:
          - PolicyName: enqueuePolicy
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - sqs:SendMessage
                    - sqs:SendMessageBatch
                  Resource:
                    - !GetAtt BotJobQueue.Arn
    BotExecuteRole:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        Policies:
          - PolicyName: executePolicy
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - sqs:ReceiveMessage
                    - sqs:DeleteMessage
                    - sqs:DeleteMessageBatch
                    - sqs:GetQueueAttributes
                  Resource:
                    - !GetAtt BotJobQueue.Arn

LINE_CHANNEL_SECRETLINE_CHANNEL_ACCESS_TOKENは作成したChannel画面から参照して記載してください。

Lambda(メッセージ受信)を作成

まずは、必要なライブラリをインストールします。

pipenv install boto3

bot_enqueue.pyを新規で作成し、以下のように編集します。

import os
import boto3
import json
import logging

sqs = boto3.client('sqs')
queue_url = os.getenv('QUEUE_URL')

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# Lambda Response
ok_response = {
    "isBase64Encoded": False,
    "statusCode": 200,
    "headers": {},
    "body": ""
}
error_response = {
    "isBase64Encoded": False,
    "statusCode": 401,
    "headers": {},
    "body": ""
}


def bot_job_enqueue(sqs_client, target_queue_url, message_body):
    json_str = json.dumps(message_body)
    sqs_client.send_message(
            QueueUrl=target_queue_url,
            MessageBody=json_str
        )


def lambda_handler(event, context):
    try:
        logger.info('event: %s', event)
        bot_job_enqueue(sqs, os.getenv('QUEUE_URL'), event)

    except Exception as e:
        logger.info(e)
        return error_response

    return ok_response

Lambda(メッセージ送信)を作成

まずは、必要なライブラリをインストールします。

pipenv install line-bot-sdk

bot_execute.pyを新規で作成し、以下のように編集します。

import os
import json
import boto3
import logging
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage

channel_secret = os.getenv('LINE_CHANNEL_SECRET')
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN')

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# LineBotAPI
line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)

sqs = boto3.client('sqs')
queue_url = os.getenv('QUEUE_URL')

# Lambda Response
ok_response = {
    "isBase64Encoded": False,
    "statusCode": 200,
    "headers": {},
    "body": ""
}
error_response = {
    "isBase64Encoded": False,
    "statusCode": 401,
    "headers": {},
    "body": ""
}


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text)
    )


def lambda_handler(event, context):
    try:
        for record in event['Records']:
            record_body = json.loads(record['body'])
            signature = record_body["headers"]["X-Line-Signature"]
            event_body = record_body['body']

        handler.handle(event_body, signature)

    except InvalidSignatureError as e:
        logger.error(e)
        return error_response
    except Exception as e:
        logger.error(e)
        return error_response

    return ok_response

デプロイ

上記で作成したリソースをAWSにデプロイします。

serverless deploy --stage dev
Service Information
service: line-oumu-bot
stage: dev
region: ap-northeast-1
stack: line-oumu-bot-dev
resources: 16
api keys:
  None
endpoints:
  POST - https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
functions:
  enqueue: line-oumu-bot-dev-enqueue
  execute: line-oumu-bot-dev-execute
layers:
  None

エンドポイントhttps://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/ をWebhookURLに設定します。

「vertify」を押して、Successが出ればOKです。

f:id:swx-go-kawano:20200903165947p:plain

動作確認

LINEでQRコードから友達追加し、メッセージを送ってみます。

f:id:swx-go-kawano:20200903170050p:plain

おうむ返しになっていますね!

さいごに

今回は、入門編ということで「おうむ返しBot」を紹介しました。
次回は、よりインタラクティブな振る舞いを目指して、 トーク上でボタンを表示したり、ボタンの選択によってメッセージを変えたりする方法について紹介できればと思います。