Help us understand the problem. What is going on with this article?

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

はじめに

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

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

この記事について

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

実際の Chatwork 通知内容

最終的に Chatwork に通知される内容です。
関係者が参加しているグループチャットに、bot さんがスロークエリの内容を投稿してくれます。
slowquery.png
基本的には CloudWatch Logs に出力している内容をそのまま出力しています。
Time のみ UTC でスロークエリログが出力されるため、JST に変換して通知しています。

処理概要図

post-slowquery-to-chatwork.png
RDS から CloudWatch Logs への出力は RDS のログのエクスポート機能を利用しています。
CloudWatch Logs ではサブスクリプションフィルタを使用して 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 をインストールします。
インストール手順はこちら
注意点として、まだプレビュー版なので実稼働環境では使用しないでね、とのことです。

テストのプレビューおよび評価として、AWS CLI バージョン 2 が提供されます。現時点では、これを実稼働環境では使用しないことをお勧めします。

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

$ 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)

ロール作成

$ 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 を使用します。

コード

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 関数作成

$ 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 サブスクリプションフィルタを設定する

$ 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 についても、今回使用してみた範囲内においては、メリットはあまり享受できませんでしたが、積極的に使ってみようと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした