遅いクエリを駆逐するための第一歩！Lambda と AWS CLI v2 を使って RDS for MySQL のスロークエリを Chatwork に通知してみた

#はじめに
こんにちは。
Mikatus 株式会社でインフラ担当として、AWS を中心にインフラの構築、運用保守等を担当しています。
今回は MySQL のスロークエリを Chatwork へ通知してみた件を書いてみようと思います。

当社システムでは Amazon RDS for MySQL を使用しており、スロークエリに関しては監視ツールを利用して発生状況は監視していましたが、リアルタイムに通知をすることで継続的な改善が行えるように、Chatwork へ通知してみました。
また、先日 [AWS CLI v2 プレビュー版が発表](https://aws.amazon.com/jp/blogs/developer/aws-cli-v2-installers/)されたので、せっかくなので CLI v2 を使って AWS リソースを作成してみました。

##この記事について
この記事では、AWS CLI v2 を用いて AWS Lambda の設定及び Amazon CloudWatch Logs の設定を操作した手順を記載しています。
Amazon RDS のログエクスポート機能、Chatwork の API設定の詳細な手順については触れていません。

##実際の Chatwork 通知内容
最終的に Chatwork に通知される内容です。
関係者が参加しているグループチャットに、bot さんがスロークエリの内容を投稿してくれます。
![slowquery.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531286/cbc9daac-72b5-7801-ae0f-3c7a9c4b4988.png)
基本的には CloudWatch Logs に出力している内容をそのまま出力しています。
Time のみ UTC でスロークエリログが出力されるため、JST に変換して通知しています。

##処理概要図
![post-slowquery-to-chatwork.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531286/8f473d46-ae1f-7df7-d40b-8879971d336d.png)
RDS から CloudWatch Logs への出力は RDS の[ログのエクスポート機能](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/USER_LogAccess.Concepts.MySQL.html#USER_LogAccess.MySQLDB.PublishtoCloudWatchLogs)を利用しています。
CloudWatch Logs では[サブスクリプションフィルタ](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#LambdaFunctionExample)を使用して Lambda 関数へ送信し、Python で書かれた関数でログメッセージをパースし Chatwork へ通知しています。
RDS のパラメータグループの long_query_time にて、CloudWatch Logs へ出力するスロークエリを制御できますが、RDS の設定は比較的緩めにして、Lambda 側でしきい値を設け、制御できるようにしています。
(今回は RDS の long_query_time を「5」秒にして、Lambda のしきい値を「10」秒にしました。)

#設定内容
設定した内容を記載していきます。

##AWS CLI v2 のインストール
手元の mac で AWS CLI v2 をインストールします。
[インストール手順はこちら](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2-macOS.html)
注意点として、まだプレビュー版なので実稼働環境では使用しないでね、とのことです。
> テストのプレビューおよび評価として、AWS CLI バージョン 2 が提供されます。現時点では、これを実稼働環境では使用しないことをお勧めします。

下記バージョンがインストールされました。

```bash
$ aws2 --version
aws-cli/2.0.0dev2 Python/3.7.4 Darwin/17.7.0 botocore/2.0.0dev1
```

印象として、CLI v1 では Python のバージョン依存などがあり、若干導入時の手間がありましたが、
CLI v2 では Python がなくてもインストールできるようになっており、だいぶ楽になっていました。

##IAMロールを作成する
###設定値
- ロール名
  - post-slowquery-to-chatwork-LambdaRole
- 信頼されたエンティティ
  - lambda
- Attach アクセス権限ポリシー
  - AWSLambdaBasicExecutionRole (AWS managed policy)

###ロール作成
```bash
$ IAM_ROLE_POLICY_DOC="iam-role-policy.json"

# 信頼されたエンティティとして Lambda を設定する
$ cat <<EOF > ${IAM_ROLE_POLICY_DOC}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

$ IAM_ROLE_NAME="post-slowquery-to-chatwork-LambdaRole"
$ IAM_POLICY_ARN="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"

# IAMロールを作成する
$ aws2 iam create-role \
    --role-name ${IAM_ROLE_NAME} \
    --assume-role-policy-document file://./${IAM_ROLE_POLICY_DOC}

# 作成した IAMロールの ARN を取得する
$ IAM_ROLE_ARN=`aws2 iam get-role \
    --role-name ${IAM_ROLE_NAME} \
    --query 'Role.Arn' \
    --output text`

# IAMロールに AWSLambdaBasicExecutionRole をアタッチする
$ aws2 iam attach-role-policy \
    --role-name ${IAM_ROLE_NAME} \
    --policy-arn ${IAM_POLICY_ARN}
```

##Lambda 関数を作成する

ランタイムは python3.7 を使用します。

###コード

```python:lambda_function.py
import json
import base64
import gzip
import re
import os
import datetime
import urllib
import urllib.request

LONG_QUERY_TIME = int(os.environ['LONG_QUERY_TIME'])

chatwork_endpoint = "https://api.chatwork.com/v2"
chatwork_apikey = os.environ['chatwork_apikey']
chatwork_roomid = os.environ['chatwork_roomid']
path = "/rooms/{0}/messages".format(chatwork_roomid)

def lambda_handler(event, context):
    print(json.dumps(event))
    #Base64 でエンコードされ、gzip 形式で圧縮されているため、中身を抽出する
    log_events = json.loads(gzip.decompress(base64.b64decode(event['awslogs']['data'])))
    db_name = log_events['logStream']
    message = log_events['logEvents'][0]['message']
    #ログからクエリ時間を抽出する
    query_time = re.search(r'Query_time: ([0-9]+.[0-9]+)', message)

    #環境変数で指定した時間より大きい場合(今回は 10秒以上)に通知を行う
    if LONG_QUERY_TIME < float(query_time.group(1)):
        timestamp = re.search(r'timestamp=([0-9]+);', message)
        #timestamp を JST 時刻へ変換する
        date = datetime.datetime.fromtimestamp(int(timestamp.group(1))) + datetime.timedelta(hours=9)
        log_message = re.sub(r'# Time:.*\n', '# Time: %s(JST)\n' % str(date), message)
        post_to_chatwork({'body': '[info][title]%s[/title]%s[/info]' % (db_name, log_message)})

def post_to_chatwork(data=None):
    try:
        if data != None:
            data = urllib.parse.urlencode(data).encode('utf-8')
            headers = {"X-ChatWorkToken": chatwork_apikey}
            req = urllib.request.Request(chatwork_endpoint + path, data=data, headers=headers)
        with urllib.request.urlopen(req) as res:
            print(res.read().decode("utf-8"))
            return
    except urllib.error.HTTPError as e:
        print('Error code: %s' % (e.code))
        sys.exit('post_to_chatwork Error')
```

###環境変数
- LONG_QUERY_TIME
  - Chatwork へ通知するしきい値(秒)
  - 例：10
- chatwork_apikey
  - Chatwork の APIトークン
- chatwork_roomid
  - Chatwork のルーム ID

###Lambda 関数作成
```bash
$ LAMBDA_FUNCTION_NAME="post-slowquery-to-chatwork"
$ LAMBDA_RUNTIME="python3.7"
$ LAMBDA_ZIP_FILE="${LAMBDA_FUNCTION_NAME}.zip"
$ LAMBDA_HANDLER="lambda_function.lambda_handler"
$ LAMBDA_ENV="{\
    LONG_QUERY_TIME=10,\
    chatwork_apikey=XXXXX,\
    chatwork_roomid=XXXXX}"

# コードを zip 圧縮する
$ zip ${LAMBDA_ZIP_FILE} lambda_function.py

# Lambda 関数を作成する
$ aws2 lambda create-function \
    --function-name ${LAMBDA_FUNCTION_NAME} \
    --runtime ${LAMBDA_RUNTIME} \
    --zip-file fileb://${LAMBDA_ZIP_FILE} \
    --handler ${LAMBDA_HANDLER} \
    --environment Variables=${LAMBDA_ENV} \
    --role ${IAM_ROLE_ARN}
```

##CloudWatch Logs サブスクリプションフィルタを設定する
```bash
$ LOG_GROUP_NAME="/aws/rds/instance/[rdsinstance]/slowquery"
$ LOG_FILTER_NAME="LambdaStream_post-slowquery-to-chatwork"
$ LAMBDA_ARN=`aws2 lambda get-function \
    --function-name ${LAMBDA_FUNCTION_NAME} \
    --query 'Configuration.FunctionArn' \
    --output text`
$ LOG_ACTION="lambda:InvokeFunction"
$ LOG_PRINCIPAL="logs.ap-northeast-1.amazonaws.com"
$ SOURCE_ACCOUNT=`aws sts get-caller-identity \
    --query 'Account' \
    --output text`
$ SOURCE_ARN="arn:aws:logs:ap-northeast-1:${SOURCE_ACCOUNT}:log-group:${LOG_GROUP_NAME}:*"

# CloudWatch Logs に、関数を実行するためのアクセス権限を付与する
$ aws2 lambda add-permission \
    --function-name ${LAMBDA_FUNCTION_NAME} \
    --statement-id ${LAMBDA_FUNCTION_NAME} \
    --action ${LOG_ACTION} \
    --principal ${LOG_PRINCIPAL} \
    --source-arn ${SOURCE_ARN} \
    --source-account ${SOURCE_ACCOUNT}

# サブスクリプションフィルタを作成する。フィルターパターンは空("")にします
$ aws2 logs put-subscription-filter \
    --log-group-name ${LOG_GROUP_NAME} \
    --filter-name ${LOG_FILTER_NAME} \
    --filter-pattern "" \
    --destination-arn ${LAMBDA_ARN}
```

以上で設定が完了です。

#さいごに
今回、RDS、Lambda、CloudWatch Logs を利用して、スロークエリを通知させてみました。
これまではスロークエリが発生したら、CloudWatch Logs にログを見に行くというちょっとつらい運用を行なっていたので、これが Chatwork に通知されることで、つらさは軽減されそうです。
将来的にはログ解析ツールなどを駆使して、より詳細な可視化ができればと思っておりますが、またの機会に実装してみようと思います。
AWS CLI v2 についても、今回使用してみた範囲内においては、メリットはあまり享受できませんでしたが、積極的に使ってみようと思います。