pythonで定時実行するAWS Lambda処理を作った

# はじめに

相変わらず競馬関係のプログラムを書いています。
これまではHeroku上で開発していたのですが、仕事でAWSを使っていることもあり、個人開発でもAWSを使い始めました。

Herokuだと毎日決まった時間に動作する処理を書く場合、Heroku Schedulerを使う形になりますが、AWSだとLambdaをCloudWatchイベントでキックする形…とはいうもののひとまとめになっている情報が見当たらなかったので、やってみたことを残しておきたいと思います。

※2019年7月時点の情報です。AWSはサービス改善のスピードが速すぎてWebにまとめた情報があっという間に陳腐化するので、そこはご注意を…

# AWS SAMのテンプレート

AWS SAMを使ってみたのですが、元になるテンプレートを作成するためにsam initコマンドを使うとAPIゲートウェイからキックされる処理のテンプレートになります。

定時実行する処理用のテンプレートは…ということで調べたところ、以下にありました。
[CloudWatch イベント アプリケーションの AWS SAM テンプレート](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-scheduledevents-example-use-app-spec.html)

だけど、これは定時実行ではなくて定期実行、かつ今回必要ないようなリソースも使っていたりだったので、定期実行する最低限のテンプレートを作りました。

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  notify_hitokuchi

  Sample SAM Template for notify_hitokuchi

Globals:
  Function:
    Timeout: 10

Resources:
  NotifyHitokuchiFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: notify_hitokuchi/
      Handler: app.lambda_handler
      Runtime: python3.7
      Events:
        NotifyHitokuchi:
          Type: Schedule
          Properties:
            Schedule: cron(30 12 * * ? *)
```

一番下の行のScheduleの行が定時実行する時刻の指定。式の記述方法は以下を参照。
[ルールのスケジュール式](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html)

# ハンドラでもらえるイベント情報の時刻

先程のテンプレートだと、notify_hitokuchi/app.py内のlambda_handler関数が実行時に呼び出される関数になります。

```python:app.py
def lambda_handler(event, context):
```

eventには辞書形式でイベント情報が入ってきます。
今回はCloudWatchイベントでキックされた時刻が欲しかったのですが、実行時刻はevent['time']に[ISO 8601](https://ja.wikipedia.org/wiki/ISO_8601)形式で入ってきます。
それをそのまま「datetime.fromisoformat()」関数に渡せばdatetimeが入手できる…と思いきや、「2019-07-10T12:30:00Z」という形だと「datetime.fromisoformat()」は処理できないようでして…
以下のようなコードでdatetimeを入手しました。

```python
# event['time']に'2019-07-10T12:30:00Z'形式の文字列が入ってくる
datetime_str = event['time'].replace('Z', '+00:00')

# '2019-07-10T12:30:00+00:00'形式だとfromisoformatで処理できる
datetime_utc = datetime.fromisoformat(date_str)
```

# pip等でインストールしたライブラリ

pip等でインストールしたライブラリをAWS Lambdaで使う場合、ライブラリを1箇所にまとめてZIPで固めて…という話をよく見かけますが、samを使っている分にはsam buildで済むようです。

```
$ sam build
2019-07-19 07:42:34 Building resource 'NotifyHitokuchiFunction'
2019-07-19 07:42:34 Running PythonPipBuilder:ResolveDependencies
2019-07-19 07:42:44 Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Package: sam package --s3-bucket <yourbucket>
```

↓↓↓2019-07-20追記↓↓↓
sam buildの前にrequirements.txtをCodeUriで指定したパスに設置が必要です。
自分はpipenvを使っているので、以下でrequirements.txtを生成しました。

```
$ $ pipenv lock -r > notify_hitokuchi/requirements.txt 
$ cat notify_hitokuchi/requirements.txt 
-i https://pypi.org/simple
beautifulsoup4==4.7.1
bs4==0.0.1
certifi==2019.6.16
chardet==3.0.4
decorator==4.4.0
html5lib==1.0.1
idna==2.8
mojimoji==0.0.9.post0
py==1.8.0
python-dotenv==0.10.3
requests==2.22.0
retry==0.9.2
six==1.12.0
soupsieve==1.9.2
urllib3==1.25.3
webencodings==0.5.1
```

また、上記requirements.txtに記載のある[mojimoji](https://pypi.org/project/mojimoji/)のようにC/C++で作られているライブラリは動作環境でコンパイルする必要がありますが、sam buildに--use-containerオプションを付けることでdockerコンテナ上でビルドが行われ、Lambda上で動くライブラリを作ることができます。素晴らしい!

```
$ sam build --use-container
2019-07-20 09:43:51 Starting Build inside a container
2019-07-20 09:43:51 Building resource 'NotifyHitokuchiFunction'

Fetching lambci/lambda:build-python3.7 Docker container image......
2019-07-20 09:43:55 Mounting /Users/masaminh/develop/notify_hitokuchi_aws/notify_hitokuchi as /tmp/samcli/source:ro,delegated inside runtime container

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Package: sam package --s3-bucket <yourbucket>
    
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource
```

↑↑↑2019-07-20追記↑↑↑


# ローカルでの動作確認

まずハンドラに渡されるイベント情報を作っておく必要がありますが、その元はsam local generate-eventを使って取得できます。
今回はCloudWatchのイベントですが、以下で作成しました。

```
$ sam local generate-event cloudwatch scheduled-event > event.json
$ cat event.json
{
  "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c",
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "account": "",
  "time": "1970-01-01T00:00:00Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:events:us-east-1:123456789012:rule/ExampleRule"
  ],
  "detail": {}
}
```

実際にローカルで動作確認を行う際には、

```
$ sam local invoke -e event.json NotifyHitokuchiFunction
2019-07-19 08:03:10 Invoking app.lambda_handler (python3.7)
2019-07-19 08:03:10 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:python3.7 Docker container image......
```

という感じで呼び出すことができます。Dockerコンテナ上で動くので起動するのに少し時間がかかります。

# デプロイ

まずパッケージを作成。

```
$ sam package --s3-bucket バケット名 --output-template-file package.yaml

Successfully packaged artifacts and wrote output template to file package.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /Users/masaminh/develop/notify_hitokuchi_aws/package.yaml --stack-name <YOUR STACK NAME>
```

引き続き、メッセージに従ってデプロイ...

```
$ aws cloudformation deploy --template-file package.yaml --stack-name notify-hitokuchi

Waiting for changeset to be created..

Failed to create the changeset: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state Status: FAILED. Reason: Requires capabilities : [CAPABILITY_IAM]
```

メッセージのままだと上記のように怒られるので、capabilitiesにCAPABILITY_IAMを設定します。

```
$ aws cloudformation deploy --template-file package.yaml --stack-name notify-hitokuchi --capabilities CAPABILITY_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - notify-hitokuchi
```

デプロイできました。

# ログ出しに関して

```python:app.py
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    logger.info('start: event.time=%s', event['time'])
```

こんな感じで普通にloggerから出力すれば、CloudWatchで見られます。

```
[INFO] 2019-07-18T12:30:28.336Z 51865e95-cacd-4f9d-80e9-6e1f148357b9 start: event.time=2019-07-18T12:30:00Z
```

# おわりに

今回は競馬的なことは書かずにノウハウ的なところのみでした。(Lambda関数名からして競馬系の処理だなぁという感じではありますが・笑)
引き続き競馬系の個人開発はやってますので、またそのうち競馬開発系の記事を書きたいな〜と思います(笑)