LoginSignup
6
4

More than 5 years have passed since last update.

Serverless Frameworkを使ってAWS Lambda + API GatewayでLINE botを作る

Last updated at Posted at 2018-03-24

なんとなくAPI gatewayとLambdaを使ってみたかったので、遊びとしてServerless Frameworkを使ってLINE botを作った。
どんなbotかと言うと

  • 画像を送るとgoogle cloud vision APIを使って文章を検出
  • 検出された文章をgoogle cloud translate APIを使って日本語に翻訳して返す

準備

awsのアカウントとかaws cliとかの準備、設定は割愛。
google cloud visionとかのgoogle cloud translateの使い方とかも割愛。

Serverless Frameworkインストール

$ npm install -g serverless
$ serverless --version
1.26.1

serviceの作成

今回はpython3を使います。

$ serverless create --template aws-python3 --name translate_bot --path translate_bot
$ cd translate_bot

Serverless Frameworkのプラグインインストール

今回、google cloud visionやgoogle cloud translateのパッケージの中でAmazon linuxの環境でコンパイルしないといけないモジュールが一部あった(具体的には grpcio)ので、その解決方法としてserverless-python-requirementsを使います。

$ npm init
$ npm install --save serverless-python-requirements

pythonの必要パッケージのインストール

$ pip install requests google-cloud-vision google-cloud-translate line-bot-sdk

LINE botのコードを書く

handler.py
import os
import json
import hmac
import base64
import hashlib
import requests
import logging.config
from google.cloud import vision
from google.cloud import translate
from google.cloud.vision import types
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage

# load Line API setting
with open('./conf/line.json', 'r') as f:
    line_conf = json.loads(f.read())
# set env
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = './conf/google_service.json'

# logging setting
logging.config.fileConfig('./conf/logging.conf')
logger = logging.getLogger()
logger.setLevel(10)

def translate_from_image(url):
    vision_client    = vision.ImageAnnotatorClient()
    translate_client = translate.Client()
    target_language  = 'ja'

    headers = {'Content-Type':  'application/json',
               'Authorization': 'Bearer ' + line_conf.get('channelAccessToken')}

    try:
        res = requests.get(url, headers=headers)
    except:
        error_message = 'Failed to fetch the image resource: ' + str(url)
        logger.error(error_message)
        return (False, 500, error_message)

    content = res.content
    image = types.Image(content=content)

    try:
        res = vision_client.text_detection(image=image)
    except:
        error_message = 'Failed to access to google cloud vision API'
        logger.error(error_message)
        return (False, 500, error_message)

    text = res.full_text_annotation.text

    if not text:
        return (True, 200, 'Can not detect any texts from the image')

    try:
        translated_text = translate_client.translate(text, target_language=target_language).get('translatedText')
        return (True, 200, translated_text)
    except:
        error_message = 'Failed to access to google cloud translate API'
        logger.error(error_message)
        return (False, 500, error_message)

def lambda_handler(request, context):
    body                 = request.get('body',    '')
    headers              = request.get('headers', '')
    channel_access_token = line_conf.get('channelAccessToken')
    channel_secret       = line_conf.get('channelSecret')
    line_bot_api         = LineBotApi(channel_access_token)
    handler              = WebhookHandler(channel_secret)
    hash                 = hmac.new(channel_secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest()
    signature            = base64.b64encode(hash).decode('utf-8')

    if signature != headers.get('X-Line-Signature'):
        logger.info('Invalid header: X-Line-Signature: ' + headers.get('X-Line-Signature'))
        return {'statusCode': 400, 'body': '{}'}

    event           = json.loads(body).get('events')[0]
    message_type    = event.get('message').get('type')
    message_id      = event.get('message').get('id')
    reply_token     = event.get('replyToken')
    image_url       = 'https://api.line.me/v2/bot/message/' + str(message_id) + '/content'

    if message_type != 'image':
        logger.info('The message type is not image')
        return {'statusCode': 400, 'body': '{}'}

    response = translate_from_image(image_url)
    ret      = response[0]
    status   = response[1]
    message  = response[2]

    if ret:
        try:
            line_bot_api.reply_message(reply_token, TextSendMessage(message))
            return {'statusCode': status, 'body': '{}'}
        except:
            error_message = 'Can not reply to a talk channel'
            logging.error(error_message)
            return {'statusCode': 500, 'body': '{}'}
    else:
        return {'statusCode': status, 'body': '{}'}

上記のコードではLINEとgoogle APIを使用するためのAPI keyが書かれたファイルを下記の通り作る必要がある。

  • LINE API key
conf/line.json
{
    "channelAccessToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "channelSecret":      "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
  • GCPサービスアカウントキー
conf/google.json
{
  "type": "service_account",
  "project_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "private_key_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "private_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "client_email": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "client_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "auth_uri": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "token_uri": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "auth_provider_x509_cert_url": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "client_x509_cert_url": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

自分で作っておいてですが、このシークレットの管理の仕方はものすごくダメですね。
コードと一緒にこれらがs3にアップロードされてしまうので、万が一bucketのアクセス公開範囲とか権限周りを間違ってたらと思うとちょっと危ない。
ssmのparameter storeとかlambdaの環境変数で管理する方がいいと思います。
各APIの使い方とかは公式ドキュメントさんに丸投げします。
- LINE developers
- google cloud vision
- google cloud translate

デプロイ

デプロイに必要なファイルを作成

$ vim serverless.yml
service: translateBot
provider:
  name: aws
  runtime: python3.6
functions:
  hello:
    handler: handler.lambda_handler
    events:
      - http:
          path: woobhook
          method: post
plugins:
    - serverless-python-requirements
custom:
    pythonRequirements:
        dockerizePip: non-linux
$ pip freeze > requirements.txt
$ cat requirements.txt
cachetools==2.0.1
certifi==2018.1.18
chardet==3.0.4
future==0.16.0
google-api-core==1.1.0
google-auth==1.4.1
google-cloud-core==0.28.1
google-cloud-translate==1.3.1
google-cloud-vision==0.30.1
googleapis-common-protos==1.5.3
grpcio==1.10.0
idna==2.6
line-bot-sdk==1.5.0
protobuf==3.5.2.post1
pyasn1==0.4.2
pyasn1-modules==0.2.1
pytz==2018.3
requests==2.18.4
rsa==3.4.2
six==1.11.0
urllib3==1.22

準備が整ったのでデプロイします!
リージョンはap-northeast-1(東京)を指定します。

$ serverless deploy -r ap-northeast-1

Serverless: Installing required Python packages with python3.6...
Serverless: Docker Image: lambci/lambda:build-python3.6
Serverless: Linking required Python packages...
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Unlinking required Python packages...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (20.82 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
........................
Serverless: Stack update finished...
Service Information
service: translateBot
stage: dev
region: ap-northeast-1
stack: translateBot-dev
api keys:
  None
endpoints:
  POST - https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/dev/webhook
functions:
  hello: translateBot-dev-hello

AWSのコンソールからも新しいAPI gatewayのエンドポイントとLambda関数が作成されたことが確認できます。
また、上記の出力の中のendpointsの値をwebhookのURLとしてLINE developersのコンソールから設定する。

スクリーンショット 2018-03-24 17.13.06.png

ここまでできたら動くはず。
試しに画像を送ってみる。
これはAWSのブログの一部のスクリーンショット
スクリーンショット 2018-03-24 17.19.34.png
これはネットで拾った適当なロシア語の文章
スクリーンショット 2018-03-24 17.20.03.png

うまく動いてるようです。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4