zappa経由でデプロイしたAWSLambda関数(WebAPI)にCloudWatch Eventsを設定する


やりたいこと

zappa経由でAWSLambdaにデプロイしたWebAPIに、定期的に動く(タイムスケジュールの)イベントをトリガーしたい。
用途としては、AWS上にあるなんらかのファイルを時間で自動更新するような感じです。

※WebAPIをzappa経由でAWSLambdaにデプロイする方法もまとめていますので、参考にしてください。
AWSを利用したサーバーレスWebAPI開発環境の構築

成功条件

以下を同時にクリアすること。

  1. URIを呼びだしたときに、HTMLやJSONデータが返ってくる
  2. 設定したタイムスケジュールに従って、イベントハンドラが呼び出される
  3. ファイルを更新する

テストファイル

test2/sever.py
from flask import Flask, jsonify, request

app = Flask(__name__)
app.config["JSON_AS_ASCII"] = False

@app.route("/")
def index():
    return jsonify({"language": "パイソン"})

if __name__ == "__main__":
    app.run(debug=True)
test2/lambda_function.py
import time, datetime
import os

def lambda_handler_1(event, context):
    today = datetime.datetime.fromtimestamp(time.time())
    t = today.strftime('%Y/%m/%d %H:%M:%S')
    msg = t + ':' + 'hello lambda_handler_1'
    print(msg)

def lambda_handler_2(event, context):
    today = datetime.datetime.fromtimestamp(time.time())
    t = today.strftime('%Y/%m/%d %H:%M:%S')
    msg = t + ':' + 'hello lambda_handler_2'
    print(msg)

def lambda_handler_write(event, context):
    today = datetime.datetime.fromtimestamp(time.time())
    t = today.strftime('%Y/%m/%d %H:%M:%S')
    msg = t + ':' + 'hello lambda_handler_write'

    # file_path = os.path.join('tmp', 'test_write.txt') 
    file_path = '/tmp/' + 'test_write.txt'

    with open(file_path, 'a', encoding='utf-16') as f:
        f.write(msg + '\n')

    rmsg = ''
    with open(file_path, 'r', encoding='utf-16') as f:
        rmsg = f.read()
    print(rmsg)

リクエスト/レスポンスの確認

WebAPIをzappa経由でAWSLambdaにデプロイした後に、発行されたURIを参照します。
問題なく、エンドポイントで指定したJSONデータが返ってくることを確認します。
※URIは、AWSコンソールの以下で確認できます。(ステージ名はd1にしています。デフォルトではdev)

CloudWatch Events の設定

[Lamda]→[関数]でデプロイした関数名を選択すると以下の画面が表示されます。

[+トリガーを追加]を選択して、以下のように設定します。
ここで、一番重要な設定がルール名です。以下のように設定します。

test2-d1-<スクリプトファイル名>.<関数名>
test2-d1-lambda_function.lambda_handler_1

スケジュール式は、rate(1 minute)が正解です。(画像は誤り)
1分以上を選択する場合は画像のように入力します。rate(5 minutes)など。

[追加]を押してイベントの登録が完了します。
Designer画面で、[CloudWatch Events/EventBridge]を選択すると、追加したイベントが確認できます。

test2-d1-zappa-keep-warm-handler.keep_warm_callbackは、zappa経由でデプロイをしたときにデフォルトで追加されるトリガーです。
Lambda関数のインスタンスを保持しておくためのトリガーで、rate(4 minutes)となっているので、「前回のアクセスから4分間インスタンスを保持する」ということだと思います。これがないと、起動するたびにインスタンスを生成する分、WebAPIの呼び出しが遅くなります。(may be...)

ルールの設定の変更や詳細な設定は、
上の画面でイベント名をクリックするか、[CloudWatch]→[ルール]から作成したルール名を選択し、その後、[アクション]から編集を選択します。

ログの確認

設定したトリガーが動作しているか確認します。
ルールの設定の変更と同様に、上の画面でイベント名をクリックするか、AWSコンソールメインから[CloudWatch]を選択し、左のツリーから[ロググループ]を選択します。
ログストリームを選択して、ログを確認します。(ログストリームはインスタンスが生成するたびに追加されるようです)

ログで、1分ごとに2020/06/04 09:05:09:hello lambda_handler_1と表示されていることを確認します。
lambda_function.py - lambda_handler_1 のprintの内容が出力されます。
(時間はおそらくUSとかになっているんだと思います。デプロイしたサーバーがUS?)

ファイルの更新

AWSLambda関数(デプロイしたWebAPI)上でファイルを更新する場合、以下のようにファイルパスを設定する必要があります。
aws lambda file書き込む時のエラー

S3バケットを使用する場合も同様のようです。
Python Read-only file system Error With S3 and Lambda when opening a file for reading

'/tmp/' + filename

# テストファイルだと以下のように設定してます
file_path = '/tmp/' + 'test_write.txt'

テストファイルのlambda_handler_writeをトリガーに設定して、ログを確認したらちゃんと動作していました。

ちなみに/tmp/を指定しなかった場合、ログで以下のようなエラーがでます。

IOError: [Errno 30] Read-only file system: 'test_write.txt'

【追記】
/tmp/フォルダはキャッシュとなっているため、時間が経つとフォルダごと消去されます。
ファイルを更新したりアップロードする場合は、S3バケットを利用します。
【Python】AWS S3バケットにCSVファイルをアップロードしたり、データを読み込んだりする

はまったポイント

トリガーの設定で、自分で作成したイベントハンドラーをどうやって指定していいのかがわかりませんでした。
どこかしらで指定するんだろうとは思っていましたが、指定する場所が見当たらないんです。

最初は、以下で設定するのかなーって思ってたけど、ここをいじるとURIが正常に呼び出せなくなりました。

5時間くらい調べて、以下の記事を見た時にようやく理解できました。
Zappaのスケジューリング機能を試してみる

これ・・・

デプロイされたLambda Functionの中身を覗いてみると、zappa_settings.jsonで指定した関数の名前を、CloudWatch Eventのリソース名から特定して実行していることがわかります。

handler.py

class LambdaHandler(object):
    ...
    def handler(self, event, context):
        if event.get('detail-type') == u'Scheduled Event':
            whole_function = event['resources'][0].split('/')[-1].split('-')[-1]

            # This is a scheduled function.
            if '.' in whole_function:
                app_function = self.import_module_and_get_function(whole_function)

                # Execute the function!
                return self.run_function(app_function, event, context)

                # Else, let this execute as it were.
                ...

・・・つまり、リソース名(ルール名)でイベントハンドラを識別してるのかー!って・・・
-でsplitしてるから、先ほど設定した以下のルール名は、<スクリプトファイル名>.<関数名>だけでもいいです。

test2-d1-<スクリプトファイル名>.<関数名>
test2-d1-lambda_function.lambda_handler_1(lambda_function.lambda_handler_1 でもいい)

ちなみにzappa経由でデプロイするとアップロードファイルの容量がでかくなって、Lambda Functionの中身を覗けない...

余談(zappa_settings.json でイベントの設定)

先ほどの記事で紹介されていた通り、デプロイする前に、zappa_settings.json からもトリガーを設定できます。
デフォルトで作成されたファイルに、eventskeep_warmを追加しています。
"keep_warm": falseにすることで、test2-d1-zappa-keep-warm-handler.keep_warm_callbackトリガーが生成されなくなります。デフォルトではtrueとなっています。(個人的にはtureでいいと思います)

zappa_settings.json
{
    "d1": {
        "app_function": "server.app",
        "profile_name": "zappa-exec-user",
        "project_name": "test2",
        "runtime": "python3.7",
        "s3_bucket": "zappa-*******",
        "events": [{
            "function": "lambda_function.lambda_handler_1",
            "expression": "rate(1 minute)"
        }
        ],
        "keep_warm": false
    }
}

ただし、こちらで設定したトリガーは、AWSコンソール上からは無効にできませんでした。
自動で生成された、test2-d1-zappa-keep-warm-handler.keep_warm_callbackも無効にできませんでした。

参考