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について言葉の説明をします。必要に応じて読み飛ばしてください。

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

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

Overview

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

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

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

概要の説明の前に、このセクションで必要になるAWSの二つのサービス、Amazon EventBridgeとAmazon 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_URLhttps://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 %s\n[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 %s\n[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 %s\n[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について言葉のご説明をします。

概要は以下の通りです。 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_URLhttps://api.line.me/v2/bot/message/reply
S3_BUCKET_NAME*保存先のS3のバケット名
S3_KEY_NAME*S3のバケット内で保存されているデータの名前

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

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

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 is\n" \
            "start\n" \
            "stop\n"\
            "log\n."
            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 %s\n[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の作成をしていただければと思います。

こんな記事も読まれています