RDSのスロークエリ増加をLambdaでSlackに通知


## 概要

Lambdaのスケジュール実行でRDS（MySQL）のスロークエリログを監視し
サイズが一定以上ならSlackに通知する仕組みを作ります。
Lambdaファンクションのランタイムには'Python 2.7'を使います。

## 事前準備

* RDS（MySQL）
* EC2（MySQLにアクセスできればOK）
* Slackチーム

## 手順

### RDSでスロークエリ出力を設定する

RDSのインスタンスに設定されているパラメータグループを変更します。
以下の3項目を変更してください。

|パラメータ|変更前|変更後|説明|
|---|---|---|---|
|slow_query_log|engine-default|1|スロークエリを出力します|
|long_query_time|engine-default|2|2秒以上のクエリをスロークエリとします|
|log_outut|TABLE|FILE|スロークエリはファイルとして出力します|

インデックスを使わないクエリを出力する`log_queries_not_using_indexes`もありますが
今回の判断軸はクエリが早いか遅いかのみなので、割愛します。

試しにスロークエリを発生させてみます。
意図的にスロークエリを発生させるために`SLEEP()`を使いました。
これで実行時間を伸ばしたクエリを自作のテーブルに投げます。

```bash:テーブルの作成（既存であれば不要）
mysql> CREATE TABLE item (id int);
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO item VALUES(1);
Query OK, 1 row affected (0.01 sec)
```

```bash:意図的なスロークエリ
mysql> SELECT id, SLEEP(5) FROM item;
+------+----------+
| id   | SLEEP(5) |
+------+----------+
|    1 |        0 |
+------+----------+
1 row in set (5.00 sec)
```

これでRDSインスタンスのLogsからmysql-slowquery.logを開くと

![RDS_·_AWS_Console.png](https://qiita-image-store.s3.amazonaws.com/0/43116/8e1694b8-f748-2d91-457e-fb54334bcc4e.png "RDS_·_AWS_Console.png")

以下のようにスロークエリが記録されるのがわかります。

![RDS_·_AWS_Console2.png](https://qiita-image-store.s3.amazonaws.com/0/43116/b1092e34-6966-1145-ceb0-cbbad356620f.png "RDS_·_AWS_Console2.png")


### SlackのWebhook用URLを準備する

以降の手順で使用するLambdaファンクションにおいて
以下のような準備が必要であると記載されています。

> Follow these steps to configure the webhook in Slack:
  1. Navigate to `https://<your-team-domain>.slack.com/services/new`
  2. Search for and select "Incoming WebHooks".
  3. Choose the default channel where messages will be sent and click "Add Incoming WebHooks Integration".
  4. Copy the webhook URL from the setup instructions and use it in the next section.

> Follow these steps to encrypt your Slack hook URL for use in this function:
  1. Create a KMS key - http://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html.
  2. Encrypt the event collector token using the AWS CLI.
   `$ aws kms encrypt --key-id alias/<KMS key name> --plaintext "<SLACK_HOOK_URL>"`
     Note: You must exclude the protocol from the URL (e.g. "hooks.slack.com/services/abc123").
  3. Copy the base-64 encoded, encrypted key (CiphertextBlob) to the ENCRYPTED_HOOK_URL variable.
  4. Give your function's role permission for the kms:Decrypt action.

全体を要約すると、Slackの'Incoming Webhooks'でURLを取得し
それをAWSのKMSという暗号化サービスで暗号化しましょうという流れです。

下記ページの説明がとてつもなくわかりやすいので参考にしました。
3節から5節まででURLの取得と暗号化が行われています。

[LambdaのBlueprintを使って、CloudWatchのアラームをSlackに投稿
](http://blog.serverworks.co.jp/tech/2016/02/16/lambda-cloudwatch-alarm-to-slack/)

### Lambdaファンクションを作成する

RDSを配置しているリージョンでLambdaのコンソールを開き
'Create a Lambda Function'を選択します。

ブループリントの中の`cloudwatch-alarm-to-slack-python`を選択します。
（参考のために使用しますが、cloudwatchは使いません）

![Lambda_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/43116/4afaab63-25f6-f06b-3253-69da6d0c8fa8.png "Lambda_Management_Console.png")

トリガー設定でのSNSも不要なので、削除します。
![Lambda_Management_Console2.png](https://qiita-image-store.s3.amazonaws.com/0/43116/45da9413-fabd-6a71-659b-6b90aeceb2fb.png "Lambda_Management_Console2.png")

NameとDescriptionは適当につけましょう。
Runtimeは前述の通り'Python 2.7'です。

肝心のコードの部分は以下の通りです。
大部分はブループリントのコードをそのまま使用しています。
説明はインラインで記載しました。

```python:Lambdaファンクション
import boto3
import json
import logging

from base64 import b64decode
from urllib2 import Request, urlopen, URLError, HTTPError

# スロークエリを調べたいDBのID
RDS_ID = "{id}"

# この数値を超えるログを、スロークエリ増加のトリガーとする
THRESHOLD_BYTE = {byte}

# 暗号化したSlackのWebhookURL
ENCRYPTED_HOOK_URL = '{url}'  # Enter the base-64 encoded, encrypted key (CiphertextBlob)

# 通知をしたいSlackチャンネル
SLACK_CHANNEL = '{channel}'  # Enter the Slack channel to send a message to

# 暗号化されたURLをKMSでデコード
HOOK_URL = "https://" + boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL))['Plaintext']

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


def lambda_handler(event, context):

    client = boto3.client("rds")
    
    # 対象のDBでサイズが閾値を超えるスロークエリログを取得
    log_files = client.describe_db_log_files(
        DBInstanceIdentifier=RDS_ID,
        FileSize=THRESHOLD_BYTE,
        FilenameContains='slowquery')
        
    # 取得できたログがなければここで終了
    if len(log_files['DescribeDBLogFiles']) == 0:
        logger.info("Slow query was not found.")
        return

    # Slackで通知する内容の設定
    slack_message = {
        'channel': SLACK_CHANNEL,
        'username': 'Alert from RDS',
        'icon_emoji': ':exclamation:',
        'text': 'Slow query was found.'
    }

    # Slackへのリクエスト
    req = Request(HOOK_URL, json.dumps(slack_message))
    
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)
```

ロールは新しく作成し、既に設定されている通り
`KMS decryption permission`を付与してください。
その他はすべてデフォルトの設定で、VPCも未使用です。

![Lambda_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/43116/ec7beba6-65e4-f24c-b47b-53714a4a57b9.png "Lambda_Management_Console.png")

これでLambdaファンクションが実行できるようになりましたが
このまま実行すると権限の問題でこけてしまいます。

IAMのコンソールから、先程作成したロールにRDSへのアクセス権限を追加します。

![aaa.png](https://qiita-image-store.s3.amazonaws.com/0/43116/a6f217b7-2802-d259-44c5-266312f46ff5.png "aaa.png")

追加するポリシーは`AmazonRDSReadOnlyAccess`です。

![Policy.png](https://qiita-image-store.s3.amazonaws.com/0/43116/106fd30e-7e81-f716-a9b9-a6354e4c9cf2.png "Policy.png")

## Lambdaのスケジュール実行を設定する

作成したLambdaファンクションのトリガーを作成します。
Triggersタブの'Add Trigger'をクリックします。

![Trigger.png](https://qiita-image-store.s3.amazonaws.com/0/43116/f7cc640c-2f7b-a4cd-206e-2b27ca70a70a.png "Trigger.png")

トリガータイプとして'CloudWatch Events - Schedule'を選択し
表示された項目を埋めていきます。

![setting.png](https://qiita-image-store.s3.amazonaws.com/0/43116/214ca3c2-0fb7-ba48-1e63-8b75b92ee61a.png "setting.png")


RDSのslowquery.logはデフォルトで1時間間隔で24時間分のデータが残るため
平日の1日ごとにファンクションが実行されるように設定しました。
Cronの時刻設定はUTCであることに注意が必要です。

実行間隔は運用方法に応じて（コードも含めて）変更してください。
以上の設定で、スロークエリがあればSlackに通知が行われます。

![slack.png](https://qiita-image-store.s3.amazonaws.com/0/43116/6f6c6efd-fcd8-3a1f-e36f-a9b4f12ffbc4.png "slack.png")

## まとめ

スロークエリログの増加をチャット通知する仕組みについて書きました。

なお、[`download_db_log_file_postion()`](https://boto3.readthedocs.io/en/latest/reference/services/rds.html#RDS.Client.download_db_log_file_portion)メソッドを使えば
ログの中身を直接チャット通知することもできそうですが
DBのスキーマやデータに関する情報を載せるのに抵抗があったため避けました。

そもそも通知の発生頻度は高くない（高い状態がおかしい）ので
存在さえ認知できれば十分かなと思っています。