TECHNICAL BLOG

2020/8/20 # AWS # Lambda # Python # 新人研修 2020/8 新人研修でAWS Lambda(Python)サイト監視LINE Bot作成しました

目次

  1. はじめに
  2. EventBridgeを利用したサイト監視
    2-1. サイト監視のためのAWSとLINEの準備
    【AWS側の設定】
    【LINE側の設定】
    2-2. Pythonを用いたLambda関数のコード作成
  3. LINE Bot利用者とのメッセージ送受信
    3-1. メッセージの送受信のためのAWSとLINEの準備
    【AWS側の設定】
    【LINE側の設定】
    3-2. Pythonを用いたLambda関数のコード作成
  4. 動作確認
  5. まとめ

    1. はじめに

    今回、私たちは新人研修中に実業務でAWS Lambda(Python)を利用してLINE Botの作成を行いました。そこで得た学びの内容を目次にそって紹介します。

まずはAWSとAWS Lambda、LINE Botについて言葉の説明をします。必要に応じて読み飛ばしてください。

  • AWS(Amazon Web Services)
     Amazonが提供している175以上のクラウドコンピューティングサービス(ネットワークを経由してコンピュータ資源を提供するサービス)を有するクラウドプラットフォームのことです。ストレージやデータベースなどの機能に加え、機械学習やデータ分析など様々なサービスをオンラインで利用することができます。セキュリティのレベルも非常に高くスピーディーに様々な機能を利用することができます。料金体系は、従量課金制で使用量に応じて費用がかかります。

  • AWS Lambda
     AWSが提供するサービスの一つで、サーバーレスでコードを実行できるコンピューティングサービスです。AWSのDBやストレージ等のサービスと連携して利用したり、AWS外のWebサイト、アプリケーションと連携して何らかの処理を行ったりすることも可能です。今回はLINE BotのプログラムをAWS Lambda関数として実装します。言語はPythonを利用しました。

  • LINE Bot
     LINE上でユーザーの発言に対して自動応答するプログラムのことです。LINE Botを友達登録したユーザー(以下LINE Botの利用者)は、LINE Botに何らかの発言をすることで特定のメッセージ等のリアクションを受け取ることができます。

それでは、私たちが作成したLINE Botについて説明します。
大きく次の2点を実現します。

  • 指定されたウェブサイトの死活監視をし、サイトに異常を検知した場合にLINE通知をする
  • LINE Botの利用者からのメッセージに対して応答する

概要は以下の図の通りです。

Overview

図の通り、サイトの死活監視を行うLambda関数と、LINE Botの利用者が送信する特定のメッセージに対して返事をするLambda関数が必要になります。
それでは、それぞれのLambda関数の作成と、それに伴って必要となるLINE Botの作成・設定を二つのセクションで説明していこうと思います。

2. EventBridgeを利用したサイト監視

このセクションでは、サイトの監視を行うLambda関数、そして今後利用していくLINE Botの作成と必要な設定を行います。

概要の説明の前に、このセクションで必要になるAWSの二つのサービス、Amazon EventBridgeとAmazon S3についてご説明します。

  • Amazon EventBridge(以下EventBridge)
     EventBridgeは様々なSaaS(Software as a Service)およびAWSのサービスの接続を可能にするイベントバスです。イベントバスとはイベント(何らかの要因でシステムの状態が変わったことを示す信号)を受け取り、実際に関数の起動を行う汎用システムのことです。
     EventBridgeを利用することで、関数を定期実行させたり、イベントの内容で実行する関数を変更したりすることができます。今回はLambda関数を定期実行するために利用しています。

  • Amazon Simple Storage Service (以下S3)
     S3はインターネット用ストレージサービスです。大容量のデータを高速かつ安価 に扱うことができます。今回はサイト監視の結果を保存するために利用しています。

概要は以下の図の通りです。

monitorOverview

①特定のWebサイトへアクセスし、サイトの状態(ステータスコード)を確認する。
②確認した結果をS3に保存する。
③正常なステータスコードでなかった場合のみ、LINE Botの利用者にブロードキャストメッセージ(LINE Bot利用者全員に送信されるメッセージ)を送信する。
*EventBridgeによってこの一連の流れを5分間に一度実行する

2-1. サイト監視のためのAWSとLINEの準備

これからサイト監視の機能を実装していきますが、まずはLambdaの関数とLINE Botの設定から始めます。なお、それらの設定を行うために必要なアカウントを保持しているという前提で以後の話を進めます。

【AWS側の設定】

1. プロバイダーの作成

まず、AWSへログインします。そして、Lambdaのサービスにアクセスし関数の作成を押下します。

mkMonitorFunction1

必要な情報を記入していきます。
関数名は好きな名前で、ランタイムはPython3.8(最新のもの)を、実行ロールは新しいロールで作成します。(*ロールとは、AWSのほかのサービスを利用したりする際の権限を管理しているものです。利用するサービスや必要な処理に応じて権限を追加する必要があります。)
ロールには、S3への書き込み権限を付与させておきましょう。

mkMonitorFunction2

今回はmonitorという名前の関数を作成しました。

2. EventBridgeの設定

今回作成するLambda関数は、5分に1回実行する必要があるので、その設定を行います。まずはトリガーの追加を押下します。

mkMonitorFunction3

そして、EventBridgeを選択してください

mkMonitorFunction4

新規ルールの作成を押下し、入力項目を入力します。

mkMonitorFunction

今回はルール名を「5minutes_exe」、ルールの説明を「5分に一度実行する」としました。ルールタイプは、スケジュール式を選択し、「rate(5 minutes)」と入力します。最後に、トリガーの有効化のチェックを外して、追加を押下します。(*トリガーの有効化にチェックがされたままだと、追加を押下した瞬間から5分ごとに一度実行されてしまいます。のちに有効化するので、今は有効化せずに追加しておきます。)

AWSの設定はここまでです。Lambda関数のコードは2-2. Pythonを用いたLambda関数のコード作成に記述してあります。

【LINE側の設定】

1. プロバイダーの作成

まずLINE Developersにアクセスし、ログインします。
そして、プロバイダーを作成します。作成ボタンを押下し、好きな名前でプロバイダーを作成してください。プロバイダーとはLine Developersにおけるフォルダのようなもので、このプロバイダー内でチャネル(LINE Bot)を管理しています。
今回はCVCTESTという名前で作成しました。

mkMonitorLine1

2. チャネルの作成

プロバイダーにアクセスすると以下の画面に遷移します。ここで新しいチャネルを作成していきます。

mkMonitorLine2

新規チャネル作成を押下後、以下の画面でMessaging APIを選択します。

mkMonitorLine

その後の画面で、チャネル名・チャネル説明・大業種・小業種・メールアドレス等の必要事項を記入し、規約に同意し作成ボタンを押下することでチャネルを作成することができます(プライバシーポリシーURL・サービス規約URLは任意での入力)。ここで入力したチャネル名が、LINE Botの名前になります。
今回はMessage_TESTというチャネルを作成しました。

3. チャネルの設定

最後に、チャネルアクセストークンを発行しておきましょう。サイト監視のLambdaで利用するブロードキャストメッセージは、このチャネルアクセストークンを利用して、Line Messaging APIにアクセスすることで送信することができます。このチャネルアクセストークンは後で利用しますので、どこかにコピー保存しておきます。

mkMonitorLine

これでLINE Botの作成、設定も終わりました。それでは次からLambda関数のコードを作成していきます。

2-2. Pythonを用いたLambda関数のコード作成

コード内で環境変???という言葉が出てきます。環境変数とは、環境によって変化する変数のことで、lambda関数の画面で設定することができます。メッセージを送信するLINE Botの情報やサイト監視を行うURLなどは、実行の環境で変化するので環境変数として利用しています。以下今回作成した環境変数を書き出しています。

環境変数名
CHANNEL_ACCESS_TOKEN *LINE Bot設定時に保存したもの
LINE_BROADCAST_URL https://api.line.me/v2/bot/message/broadcast
MONITORING_URL *サイト監視したいURL
S3_BUCKET_NAME *保存先のS3のバケット名
S3_KEY_NAME *S3のバケット内で保存されるデータの名前

*コード作成の前にS3でバケットを作成しておいてください。(名前以外はデフォルトで大丈夫です。) コードがうまく動いた場合、環境変数で設定したS3_KEY_NAMEの名前のデータがS3のバケットに作成されます。

以下が作成したコードです。

import urllib.request
import os
import json
import boto3

#AWSのほかのサービスを利用するための読み込み。
s3 = boto3.resource("s3")

def lambda_handler(event, context):

    #環境変数"MONITORING_URL"を取得する。
    url = os.environ["MONITORING_URL"]

    #S3に保存するテキストを格納するリスト。
    #URLとステータスコードを格納する。
    tmp_txt = []
    tmp_txt.append(url)

    #今回はサイトのステータスコードが200番ではない場合のみ、メッセージを送るための論理値。
    isUnhealthy = True

    try:
        #サイトにアクセスする
        response = urllib.request.urlopen(url, timeout = 10)

        #ステータスコードを取得する。
        status_code = response.getcode()

        #ステータスコードが正常(200)な時の処理。
        If status_code == 200:
            print("[success] %s is healthy. n" % url)

            #ステータスコードが200番なのでメッセージを送らないように論理値をfalseに変更する。
            isUnhealthy = False

        else:
            print("[failure] %s is unhealthy. n" % url)
            print("[status-code] : %s. n" % status-code)

    except (urllib.error.HTTPError) as error:
        #アクセスに失敗してしまったので、errorからステータスコード(理由)を取得
        status_code = error.code

        print("[warn]Data not retrieved because %sn[warn]URL: %s" %(error,url))
        print("[failure] %s is unhealthy. n" % url)

    except (urllib.error.URLError) as error:
        #URLErrorの場合ステータスコードが取得できない為、"HTTP通信の不可"という文字列を変数status-codeへ
        status_code = "HTTP通信の不可"

        print("[warn]Data not retrieved because %sn[warn]URL: %s" %(error,url))
        print("[failure] %s is unhealthy. n" % url)

    finally:
        #アクセスの成功の有無に限らず、S3保存用の変数"txt"にステータスコードを格納する。
        tmp_txt.append("status code : " + status_code)

    #サイトのステータスコードが200番でなかった場合はその旨をブロードキャストメッセージで送信する。
    if isUnhealthy:
        message = url + "nのサイト監視にてn[status-code]:"+ status_code + "nを確認しました。"
        linemessage_broadcast(message)

    #環境変数から、S3のバケット(ストレージ)名とkeyを取得し、S3を使用できる状態にする。
    bucket = os.environ["S3_BUCKET_NAME"]
    key = os.environ["S3_KEY_NAME"]
    obj = s3.Object(bucket, key)

    #変数"tmp_txt"はリストなので、S3に保存するために、strに変換する。
    txt = "n".join(tmp_txt)

    #S3に保存し、処理を終える。
    obj.put( Body=txt )
    return

"""
ブロードキャストメッセージを送信するためのメソッド。
ブロードキャストメッセージを送るLINE Messaging APIのURLとチャネルアクセストークンを環境変数から取得している。
APIについては次のセクションで詳しく述べます。
"""
def linemessage_broadcast(msg):
    url = os.environ["LINE_BROADCAST_URL"]
    access_token = os.environ["CHANNEL_ACCESS_TOKEN"]
    method = "POST"
    headers = {
        "Authorization": "Bearer " + access_token,
        "Content-Type": "application/json"
    }
    payload = {
        "messages":[
            {
                "type":"text",
                "text": msg
            }
        ]
    }
    request = urllib.request.Request(url, json.dumps(payload).encode("utf-8"), method=method, headers=headers)
    try:
        response = urllib.request.urlopen(request,timeout=10)
    except (urllib.error.HTTPError, urllib.error.URLError) as error:
        print("[warn]LINE message data not retrieved because %sn[warn]URL: %s" %(error, url))
        status_code = error.code
    else:
        print("[line-access-success] %s" % response.geturl())
        status_code = response.getcode()

    if (status_code != 200):
        print("[error]An error occurred while sending the LINE message.")
        print("[error]LINE Messaging API"s status-code: %d" % status_code)
        return
    return

3. LINE Bot利用者とのメッセージ送受信

このセクションでは、LINE Botの利用者がメッセージを送信することでさまざまな処理を行う、かつLINE Botの利用者へリプライメッセージを送信するLambda関数を作成します。
まず、今回利用したAWSのサービスとWebhookについて言葉のご説明をします。

  • AWS API Gateway(以下API Gateway)
     AWSの提供するサービスの一つで、APIを作成することができます。APIとはソフトウェアコンポーネント同士が互いに情報をやり取りするのに使用するインターフェースの仕様のことです。
     API GatewayはAPIのなかでもWebAPIを利用しており、適当な情報をHTTP/HTTPS通信することで、様々なソフトウェア(アプリケーション)の機能を利用することができます。作成されるAPIは同時に発行されるURLを通じて利用することができます。

  • Webhook
     Webhookとは、Webアプリケーションで何らかの変化(イベント)が起きた際に外部サービスにHTTP/HTTPSで通知する仕組みのことです。この際のHTTP/HTTPS通信先のことをWebhook URLといいます。今回はLINE Botの利用者がLINE Botにメッセージを送った(イベントが発生した)際に、Webhook URL (APIGateway作成時に発行されるURL)にそのメッセージの情報を通知しています。

概要は以下の通りです。

MessageOverview

①LINE Botの利用者がメッセージを送る。
②LINE Botに登録されているWebhookにメッセージに関する情報をPOST通信する。
③API Gatewayが受け取った情報をLambda関数に送信する。
④チャネル、個人特定に必要な情報とメッセージの内容などの情報をLine Messaging APIにPOST通信する。
⑤受け取った情報からLINE Botにメッセージを送る。
⑥LINE BotからLINE Botの利用者にリプライメッセージ(LINE Botの利用者が送ったメッセージに対して、一度のみ送り返すことができるメッセージ)が送られる。

3-1. メッセージの送受信のためのAWSとLINEの準備

これから、LINE Botの利用者からのメッセージに対して応答を行う機能を実装していきます。一つ目のセクション内でAWS LambdaとLINE Botの作成・設定をしましたが、今回は新たなLambda関数の作成とLINE Botに追加の設定を行います。

【AWS側の設定】

1. 関数の作成

一つ目のセクションの1.関数の作成を参照してLambdaの関数を作成します。作成する際のロールは、monitor関数を作成したときのロールを使用してください。作成した関数名はline_botにしました。

2. API Gatewayの追加

LINE Botから送られてきたメッセージを受け取るためにAPIを設定します。まずは「トリガーを追加」を押下します。

mkMessageFunction1

API Gatewayを選択します。

mkMessageFunction2

API Gatewayの設定を行います。今回は新しくAPIを作成するので、Create an APIを選択します。今回においては、API typeはHTTP APIをセキュリティはオープンを選択し追加します。
API Gatewayで作成できるAPIには、HTTP APIとREST APIが存在します。AWSによると以下のように述べられています。

HTTP API は、低レイテンシーでコスト効率が良い AWS Lambda プロキシ API および HTTP プロキシ API を提供するよう設計されています。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-vs-rest.html

Lambdaを実行する環境下において、HTTP APIがREST APIと比べ低レイテンシー(低遅延)かつ低コストであるため、特別複雑な処理を必要としない場合はHTTP APIの使用で問題ないと考えます。

mkMessageFunction3

AWSの設定はここまでです。Lambda関数のコードは3-2. Pythonを用いたLambda関数のコード作成で記述してあります。

【LINE側の設定】

1.チャネルの設定

チャネルと、Lambdaのline_bot関数を接続するための設定を行います。
一つ目のセクションと違い、LINE Botの利用者からのメッセージをLINE BotからLambdaに送る必要があります。そのためLINE BotのWebhookを作成者が登録しなければなりません。まずはWebhookの機能が使えるように「Webhookの利用」をONにします。
そして、Webhook URLにAWSで作成したHTTP APIのURLを入力します。先ほど作成したline_bot関数のAPIGatewayを押下した際に、表示されるAPIの詳細の項目の、APIエンドポイントを入力します。

mkMessageFunction4
mkMessageFunction5

最後に、応答メッセージを無効にします。詳細設定の、応答メッセージをオフにするだけです。

mkMessageFunction6

応答メッセージがオンのままだと、LINE Botの利用者が送るメッセージ毎にテンプレートの文章がLINE Botから送られてきます。今回は必要ないためオフにしておきます。
これでLINEの設定も終わりました。それでは次からLambda関数のコードを作成していきます。

3-2. Pythonを用いたLambda関数のコード作成

このLambda関数では、以下のような環境変数を利用しました。

環境変数名
CHANNEL_ACCESS_TOKEN *LINE Bot設定時に保存したもの
LINE_REPLY_URL https://api.line.me/v2/bot/message/reply
S3_BUCKET_NAME *保存先のS3のバケット名
S3_KEY_NAME *S3のバケット内で保存されているデータの名前

今回は、LINE Botの利用者が送るコマンドに対して以下のような処理をしています。

  • start
    2. EventBridgeを利用したサイト監視で作成した関数”monitor”のEventBridge(5minutes_exe)を有効にすることで、5分間に一度のサイト監視を開始する。
  • stop
    関数”monitor”のEventBridge(5minutes_exe)を無効化する。
  • log
    S3に保存されているサイト監視の結果をメッセージとして送る。
  • 上記以外
    対応するコマンドを送るように促すメッセージを送る。

コード内の条件分岐で、上記の処理についても確認してください。

import urllib.request
import os
import json
import boto3
import time

#AWSのほかのサービスを利用するための読み込み。
s3 = boto3.resource("s3")
events = boto3.client("events")

#LINE Botから送られるデータは引数である"event"に格納され受け取る。
def lambda_handler(event, context):
    #変数"event"はJSONという形式で送られてくるため、辞書へ変換する。
    #LINE Botの利用者が送信したものが何か(画像・メッセージなど…)を変数"type"に格納する。
    try:
        event_json = json.loads(event["body"])
        type = event_json["events"][0]["type"]
    except TypeError:
        print("typeError occured")
        type = ""
    except KeyError:
        print("keyError occured")
        type = ""

    print("request type is [%s]." % type)
    #LINE Botの利用者がmessageを送信していた場合の処理。
    if (type == "message"):
        # messageの内容と、リプライメッセージを送る為に必要になるreplyTokenを保持しておく
        reply_token = event_json["events"][0]["replyToken"]
        recv_msg = event_json["events"][0]["message"]["text"]
        print("message content is [%s]." % recv_msg)

        #以下メッセージの内容に応じた処理の条件分岐
        #内容がstartの際は、monitor関数を起動する。
        #(実際は関数と結びついているEventBridgeを有効にする)。
        if (recv_msg == "start"):
            #startのメッセージを受け取り、起動していることを知らせる
            msg = "starting monitor..."
            linemessage_reply(reply_token, msg)

            #monitor関数を5分に一度実行するEventBridgeを有効にする
            response = events.enable_rule(
                Name="5minutes_exe"
            )

        #messageの内容がstopの際は、monitor関数を停止(終了)する
        #(関数と結びついているEventBridgeを無効にする)
        #startの時とほぼ同じ
        elif (recv_msg == "stop"):
            msg = "stopping monitori..."
            linemessage_reply(reply_token, msg)
            response = events.disable_rule(
                Name="5minutes_exe"
            )

        #messageの内容がlogの際は、monitor関数で保存されているサイト監視の結果を送信する。
        elif (recv_msg == "log"):
            bucket = os.environ["S3_BUCKET_NAME"]
            key = os.environ["S3_KEY_NAME"]
            obj = s3.Object(bucket, key)
            dat = obj.get()["Body"].read().decode("utf-8")
            msg = "the latest monitoring result is [" + dat + "]."
            linemessage_reply(reply_token, msg)

        #start/stop/log以外のメッセージの場合。
        #対応するコマンドを送るように促すメッセージを送る。
        else:
            msg = "Oops! the command is not supported.>_<n"
            "the supported command isn"
            "startn"
            "stopn"
            "logn."
            linemessage_reply(reply_token, msg)

    #そもそもmessageではない情報が送信されてきた場合、その旨をCloudWatch Logに保存
    else:
        print("[warn]:Data type is not message.")

    #APIエンドポイントに以下の情報を残す
    return {
        "statusCode": 200,
        "body" : json.dumps("hello lambda.")
    }

"""
リプライメッセージを送信するためのメソッド。
リプライメッセージを送るLINE Messaging APIのURLとチャネルアクセストークンを環境変数から取得している。
リプライトークンを利用して、どのLINE Botの利用者がメッセージを送ってきたのか特定している。
このリプライトークンは各メッセージにつき毎回発行される。
そのため、一つのメッセージに対して複数のリプライメッセージを送ることはできない。
"""
def linemessage_reply(reply_token, msg):
    url = os.environ["LINE_REPLY_URL"]
    access_token = os.environ["CHANNEL_ACCESS_TOKEN"]
    method = "POST"
    headers = {
        "Authorization": "Bearer " + access_token,
        "Content-Type": "application/json"
    }
    payload = {
        "replyToken": reply_token,
        "messages":[
            {
                "type":"text",
                "text": msg
            }
        ]
    }
    request = urllib.request.Request(url, json.dumps(payload).encode("utf-8"), method=method, headers=headers)
    try:
        response = urllib.request.urlopen(request,timeout=10)
    except (urllib.error.HTTPError, urllib.error.URLError) as error:
        print("[warn]LINE Message data not retrieved because %sn[warn]URL: %s" %(error, url))
        status_code = error.code
    else:
        print("[line-access-success] %s" % response.geturl())
        status_code = response.getcode()

    if (status_code != 200):
        print("[error]An error occurred while sending the LINE message.")
        print("[error]LINE Messaging API’s status-code: %d" % status_code)
        return

    return

LINE Bot(Lambda)が送受信するデータはJSONという形式です。詳しくはLineDeveloppersに記してあるため、確認してください。どのkeyにどんなデータが格納されているのか確認しておくと、より複雑な処理を行うことも可能になります。

4. 動作確認

動作確認をするにあたって、自分の指定したステータスコードを返すcreateStatuscodeという名前のLambda関数を作成しました。コードは以下の通りです。

import json

def lambda_handler(event, context):
    status_code = 408
    return {
        "statusCode": status_code,
        "body": json.dumps("statuscode is " + str(status_code))
    }

トリガーには新規API Gateway(Type:HTTP, セキュリティ:オープン)を追加します。このAPIエンドポイントのステータスコードは、コード内の変数”status_code”に格納された整数になります。

そして、作成時に無効にしていた関数”monitor”のEventBridge(5minutes_exe)を有効にしておきます。

enableEvent

*こちらを有効化した瞬間から、サイト監視(5分間隔での実行)も始まるため、最初は以下のような動作をしない可能性があります。

では、作成したエンドポイントを利用した実際の動作を見ていきます。

operationCheck

①startとLINE Botに入力することでサイト監視が始まります。今回はステータスコードが200でなかった場合にその旨のメッセージが送られるか確認するため、関数”monitor”の環境変数の“MONITORING_URL”に先ほど作ったcreateStatuscode関数のエンドポイント (変数”status_code”は408)を設定し、サイト監視しました。5分に一度、URLとステータスコードが出力されているのがわかります。

②stopとLINE Botに入力することで、サイト監視を停止することができます。実際にメッセージが送られてないことが確認できます。

③次は正常なステータスコードのサイトを環境変数に設定して、startとLINE Botに入力した場合です。今回は、google(https://www.google.co.jp/)を設定しました

④時間を確認してもらえばわかりますが、③から約15分間リアクションがありません。この場合、ステータスコードに異常がないか、サイト監視が機能していないということが考えられます。そこでlogとLINE Botのメッセージを送り直近のサイト監視の結果を確認すると、「the latest monitoring result is https://www.google.co.jp status code : 200」とメッセージが返ってきましたので、サイト監視はきちんと機能しており、正常なステータスコードの場合メッセージが送られないことを確認できました。

⑤Lambdaは冒頭で説明した通り、 従量課金制で使用量に応じて費用がかかります。基本的に使用しないときはstopとLINE Botに入力しサイト監視を停止させておきます。

5. まとめ

AWSやPython、LINEの技術に触れることができ、良い経験をすることができました。
実際の業務では、よりセキュアで複雑な処理も加えました。
もし興味持っていただけましたら、こちらの内容を参考にLINE Botの作成をしていただければと思います。