Amazon RDSのMySQL5.7の監査ログの監視

はじめに

 こんにちは、ほんまです。先日、AWSからAmazon RDS for MySQL バージョン 5.5 のサポート終了のお知らせが届きました。2021/02/09からMySQL5.5が廃止され、MySQL5.5のインスタンスは自動的にMySQL5.7にバージョンアップするようです。
 自分が担当しているサービスがMySQL5.5を利用しているのでMySQL5.7にバージョンアップします。バージョンアップ後に問題が発生した時にロールバックできるように廃止予定の2021/02/09より前に手動でアップデートを行います。
 色々と調査と検証を行いましたが、本番環境では何が起こるかわからないのでロールバックの判断基準の1つとしてクエリの失敗数を設定しました。今回はクエリの失敗を監視するために行ったことを紹介します。

概要

MySQLの監査ログをCloudWatchLogsに出力し、Lambdaでサブスクリプションして監視します。エラーが発生したらSlackに通知します。

f:id:AdwaysEngineerBlog:20210115105947p:plain

監査ログとは

MariaDB監査プラグインを使用して、接続、切断、クエリ、クエリされたテーブルなどのイベントをキャプチャすることができます。
クエリはプレーンテキストで記録します (構文またはアクセス権限エラーで失敗したエラーを含む)。

MySQLの監査ログをCloudWatchLogsに出力する

MariaDB監査プラグインを有効にしCloudWatchLogsに監査ログを出力する設定を行います。

1. オプショングループの作成

デフォルトのオプショングループはオプションを追加できないのでオプションを追加するためのオプショングループを作成します。

f:id:AdwaysEngineerBlog:20210115110022p:plain

2. オプションの追加

作成したオプショングループのMariaDB監査プラグインを有効にします。

f:id:AdwaysEngineerBlog:20210115110047p:plain

オプションの詳細はこちらを参照してください。 変更したオプション設定

Key Value Description
SERVER_AUDIT_ROTATE_SIZE 1000000 デフォルト(値なし)だとインスタンス側のデフォルトで1000000だったので明示的に指定
SERVER_AUDIT_FILE_TORATIONS 9 デフォルト(値なし)だとインスタンス側のデフォルトで9だったので明示的に指定
SERVER_AUDIT_QUERY_LOG_LIMIT 10240 デフォルト(1024)だと足りないかもしれないので念のため10240に変更

3. RDSインスタンスにオプショングループの適用

RDSインスタンスの変更から作成したオプショングループを適用させます。

f:id:AdwaysEngineerBlog:20210115110120p:plain

4. 監査ログをCloudWatchLogsに出力

3の手順と同じセッションでCloudWatchLogsに出力するように設定します。
https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home#logsV2:log-groupsの/aws/rds/instance/{DBインスタンス名}/auditにログが出力されるようになります。

f:id:AdwaysEngineerBlog:20210115110158p:plain

Lambdaでサブスクリプションして監視する

メトリクスフィルタによるログ監視では簡易的な条件指定しかできないのでCloudWatchLogsのサブスクリプションフィルタを使ってLambdaで監視します。

1. Lambda関数を作成

Lambdaの関数を作成します。ここでは関数名のみ指定します。

f:id:AdwaysEngineerBlog:20210115110237p:plain

2. CloudWatchLogsをトリガーに設定する

必要な権限はこのとき自動で設定されます。
ロググループには/aws/rds/instance/{DBインスタンス名}/auditを指定します。

全てのログが対象になるようにフィルターは名前のみ指定してパターンは指定しません。

f:id:AdwaysEngineerBlog:20210115110304p:plain

3. 関数コードのデプロイ

監査ログのフォーマットは「,」区切りで末尾がretcode(記録されたオペレーションのリターンコード、0は成功)になっているので正規表現「/.+[^(,0)]$/」で成功(末尾が0)以外のログがあった場合にSlackに通知するコードをデプロイします。

コード

// CloudWatchLogsのパース 参考にしたページ
// https://github.com/watanabeshuji/logs2sns/blob/60645ac3d73ead4ae6c25d44cd0af6aedf2511cf/logs2sns.yml
// Slack通知 参考にしたページ
// http://qiita.com/tmtysk/items/7161b11e20ac5e2dfc01
var zlib = require('zlib');
var aws = require('aws-sdk');
var sns = new aws.SNS({ region: 'ap-northeast-1' });

const https = require('https');
const url = require('url');
const slack_url = 'SlackのWebhookURL';
const slack_req_opts = url.parse(slack_url);
slack_req_opts.method = 'POST';
slack_req_opts.headers = {'Content-Type': 'application/json'};
exports.handler = function(input, context, callback) {
  var data = new Buffer(input.awslogs.data, 'base64');
  zlib.gunzip(data, function(e, result) {
    if (e) {
      callback(e);
    } else {
      result = JSON.parse(result.toString('utf-8'));
      var logs = result['logEvents']
                     .filter(function(evt) { return evt['message'].match(/.+[^(,0)]$/) ;})
                     .map(function(evt) { return evt['message'] });
      console.log('processing' + logs.length + '/' + result['logEvents'].length + ' events.');
      if (logs.length === 0) {
        callback();
        return;
      }
      var req = https.request(slack_req_opts, function (res) {
        if (res.statusCode === 200) {
          context.succeed('posted to slack');
        } else {
          context.fail('status code: ' + res.statusCode);
        }
      });

      req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
        context.fail(e.message);
      });
 
      var color = "#F35A00";
      var title = 'RDSでエラー発生!'
      var str = "<!channel>" + '\n' +
                'Log: ' + result['logGroup'] + ' - ' + result['logStream'] + '\n' +
                'Filter: /.+[^(,0)]$/\n' +
                'Messages:\n' +
                logs.join('\n---\n');
                
      req.write(JSON.stringify({
          attachments: [{
              title: title,
              color: color,
              text: str
          }]
      }));
      req.end();
    }
  });
};

さいごに

MySQLのエラーを各アプリケーションのログからではなく一箇所(MySQLのログ)で検知できるのでロールバックの判断を素早くできるようなったと思います。願わくはこの監視の通知を見ずに何事もなくバージョンアップが完了してほしいです。