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

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

※WebAPIをzappa経由でAWSLambdaにデプロイする方法もまとめていますので、参考にしてください。
[AWSを利用したサーバーレスWebAPI開発環境の構築](https://qiita.com/nemutas/items/2d0d3ea584b06d054f42)

#成功条件
以下を同時にクリアすること。

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）
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/a0f316c4-a6f5-7004-b537-e1a6d9b8ac11.png)


#CloudWatch Events の設定
[Lamda]→[関数]でデプロイした関数名を選択すると以下の画面が表示されます。
![無題2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/dad221aa-c2bd-6539-b5e2-f14491245b6d.png)

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

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

![無題3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/891eaed8-4d5f-e5dd-6b42-5d553be6377c.png)

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

[追加]を押してイベントの登録が完了します。
Designer画面で、[CloudWatch Events/EventBridge]を選択すると、追加したイベントが確認できます。
![無題4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/dc032037-a6f5-d855-292c-5b858a5bcf15.png)

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

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

#ログの確認
設定したトリガーが動作しているか確認します。
ルールの設定の変更と同様に、上の画面でイベント名をクリックするか、AWSコンソールメインから[CloudWatch]を選択し、左のツリーから[ロググループ]を選択します。
ログストリームを選択して、ログを確認します。（ログストリームはインスタンスが生成するたびに追加されるようです）
![無題5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/0a8e6123-95d6-a663-5317-e95cfd5c0219.png)

ログで、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書き込む時のエラー](https://qiita.com/namkim/items/3edb9abe3871963bf0f7)

S3バケットを使用する場合も同様のようです。
[Python Read-only file system Error With S3 and Lambda when opening a file for reading
](
https://stackoverflow.com/questions/39383465/python-read-only-file-system-error-with-s3-and-lambda-when-opening-a-file-for-re)

```.py
'/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ファイルをアップロードしたり、データを読み込んだりする](https://qiita.com/nemutas/items/c3346a866fa7fe6f7d60)

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

最初は、以下で設定するのかなーって思ってたけど、ここをいじるとURIが正常に呼び出せなくなりました。:joy:
![無題6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/19c8ae37-add9-736e-2d99-654a207d3248.png)

5時間くらい調べて、以下の記事を見た時にようやく理解できました。
[Zappaのスケジューリング機能を試してみる](https://dev.classmethod.jp/articles/try-zappa-scheduling/)

これ・・・
>デプロイされた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.
                ...
```

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

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

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

#余談（zappa_settings.json でイベントの設定）
先ほどの記事で紹介されていた通り、デプロイする前に、zappa_settings.json からもトリガーを設定できます。
デフォルトで作成されたファイルに、`events`と`keep_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`も無効にできませんでした。

#参考
- [AWSを利用したサーバーレスWebAPI開発環境の構築](https://qiita.com/nemutas/items/2d0d3ea584b06d054f42)
- [aws lambda file書き込む時のエラー](https://qiita.com/namkim/items/3edb9abe3871963bf0f7)
- [Python Read-only file system Error With S3 and Lambda when opening a file for reading
](
https://stackoverflow.com/questions/39383465/python-read-only-file-system-error-with-s3-and-lambda-when-opening-a-file-for-re)
- [Zappaのスケジューリング機能を試してみる](https://dev.classmethod.jp/articles/try-zappa-scheduling/)