S3 × Lambda × Cloudwatch Eventsで、簡単にバッチ処理の監視機構を導入する

コンニチハ。川野です。


[Fusic Advent Calendar 2019 - Qiita](https://qiita.com/advent-calendar/2019/fusic) 7日目の記事です。
昨日は [@ya_ma23](https://qiita.com/ya_ma23) による [Rustでcsvを読み込みDBにインポートする](https://qiita.com/ya_ma23/items/58cdbb96099c339d70bd) でした。
最近よく目にするRust、気になる存在です。

----------------

さて本日は、簡単に導入できるように設計・実装したバッチ処理の監視機構について紹介します。

本記事は「一日一回 **決まった時刻に** 動く処理が、何らかの理由で失敗していたことを検知したい」という方向けです。


# 概要
アプリケーションサーバ上で、毎朝1回決まった時刻にcronによって実行されるphpファイルがあります。
当然、毎朝このファイルが実行されないと困りますね。
そこで、ファイルの実行に失敗したことを検知し、システム管理者にSlack通知するような機構を実装しました。

「`ファイルの実行に失敗した`」といっても、その原因は色々と考えられるでしょう。
（phpファイルの実行自体が失敗していた、そもそもcrondが起動してなかった、etc...）
今回の機構では、 **「何らかの原因で、バッチ処理が動いていなかった」** ということを監視できるようにします。


# システム構成

下図のような構成です。

普段AWSを使ってアプリケーションを開発されている方にとってはカンタンでしょう。
<img width="1023" alt="monitoring-architecture.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247004/9b2b75c2-3614-fa99-2a7e-4db52569c99f.png">




# AWS各種サービスの設定 / 実装

以下の目次で進みます。

1. S3へのテキストファイルアップロード(php)
2. S3バケットの設定
3. Lambdaの設定・S3のファイル存在チェック
4. CloudWatch EventsによるLambda関数の定期実行


## 1. S3へのテキストファイルアップロード(php)

aws-sdkを使用します。 `aws-sdk-php` をインストールしましょう。

```
composer require aws/aws-sdk-php
```

テキストファイルは、「phpファイルが実行された」ことを担保するためにアップロードします。
アップロード処理は以下です。

ちなみに、アプリケーションはCakePHP3で動いています。

```php

     /**
      * 処理が実行された証拠となるファイルをS3へアップロード
      *
      * @return bool
      */
     private function evidenceFileUpload()
     {
         $filePath = 'path/to/file'; // アップロードするファイルのパス
         $date = (new FrozenTime())->format('Y-m-d'); // '2019-12-07' のように、アップロードされた日付をフォルダ名とする
         $newFileName = 'evidence.txt';
         $result = S3Manager::putObject($filePath, $date, $newFileName);  // `Aws\S3\S3Client の putObject()` を使用
         if (!$result) {
             // S3へのアップロード処理に失敗したらSlack通知(＝phpファイルが実行されたということは担保されます)
             Slack::sendSlackNotification('ファイルのS3へのアップロードに失敗しました');
         }

         return $result;
     }
```
cronによってこのphpファイルが実行されたとしても、S3へのアップロードに失敗した場合は、一応Slack通知するようにしています。

### IAMユーザーの設定
アプリケーションが使用するIAMユーザーにアタッチするポリシーには、S3への `putObject()` のみを許可するようにしておくのがセキュアですね。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::hoge-bucket/*"
        }
    ]
}
```


## 2. S3バケットの用意・設定
なにはともあれバケットを用意します。

そして、バケットにはライフサイクルルールを設定しておいた方が良いでしょう。
Lambdaによるテキストファイルの存在チェック後には、そのファイルはもう不要ですので。


## 3. Lambdaの設定・S3のファイル存在チェック
S3にファイルが存在しているかチェックするLambda関数を用意します。

### コード（Python3.7)

```python
import json
import time
import boto3
import urllib.request
from botocore.errorfactory import ClientError
from datetime import datetime, timedelta, timezone


def lambda_handler(event, _context):
    lambda_client = boto3.client("lambda")

    # 指定ファイルが存在しているかどうかのステータス取得
    status = check_file_exists()
    if not status:
        response = execute_notification(event, lambda_client)

        return response


# ファイルの存在チェック
def check_file_exists():
    s3_client = boto3.client("s3")
    # key名を指定
    jst = timezone(timedelta(hours=+9), "JST")
    jst_now = datetime.now(jst)
    date = datetime.strftime(jst_now, "%Y-%m-%d")
    key_name = date + '/' + 'evidence.txt'

	# ファイル存在チェックを実行
    try:
        s3_client.head_object(
            Bucket='bucket-name',
            Key=key_name
        )
        status = True
    except ClientError:
        status = False

    return status


# slack通知
def execute_notification(event, lambda_client):
    send_data = {
        "text": "処理の実行を確認できませんでした",
        "channel": "#slack-chennel-name"
    }
    send_text = ("payload=" + json.dumps(send_data)).encode('utf-8')
    request = urllib.request.Request(
        "", #slack webhook url指定
        data=send_text,
        method="POST"
    )
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')

    return response_body

```
[コチラ](https://qiita.com/kurakura0916/items/22f12f01a0d4fcab2c1d)を参考にさせていただきました。ありがとうございます。

S3のファイルの存在を `s3_client.head_object()` でチェックしています。
boto3の[Document](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.head_object) によると `s3:GetObject` を許可する必要があるとのことです。

> You need the s3:GetObject permission for this operation


### IAMロールの設定
LamdbaにIAMロールを設定します。
ロールにアタッチするポリシーには、S3の指定のバケットに対して `getObject()` を許可するようにしておきましょう。
（ここでは全バケットを許可しちゃってますが）

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::*"
        }
    ]
}
```


## 4. CloudWatch EventsによるLambda関数の定期実行

ルールを作成しましょう。
「スケジュール」から「Cron式」を選択します。
一つ注意点としては、スケジュールイベントが使用するタイムゾーンはUTCであるということです。
私が使用しているのは東京リージョンなので、指定したい時刻から `-9` 時間 した値にしないといけません。
(CloudWatch Events Document: [Schedule Expressions for Rules](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html) より)

ターゲットには、3. で作成したLambda関数を指定します。

![スクリーンショット 2019-12-07 19.06.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247004/a1944df3-d17d-b7f5-7381-ba48e7d9b098.png)


# S3さんのご様子
わざわざ上げる必要もないかもしれませんが、

監視により安心を得られます。
![スクリーンショット 2019-12-07 19.19.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247004/5144a2d3-140f-cba3-da08-b8b53773598b.png)


# まとめ

これで監視機構の導入は完了です。
AWS利用料も0.1(円/月)程度でした。

監視により平穏な日々を過ごすことができますね。


さて、明日のアドベントカレンダーは [@daiki7nohe](https://qiita.com/daiki7nohe) です。
どうぞよろしくおねがいします！