Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

コスト削減成功!Amazon Auroraの監査ログをS3に保存する仕組みを構築した話

こんにちは。プロダクト本部Growth部でエンジニアをしている id:ruru8net です。
前回はこちらの記事を書かせていただきました。
tech.classi.jp

今日は前述したSRE留学中にやったことの中の「Amazon Auroraの監査ログをCloudWatch Logsを経由せずS3に保存する」を紹介したいと思います。

前提

前掲の記事にもある通り、弊社のAWSにかかっているコストを調査したところCloudWatch Logsの特にAmazon RDSの監査ログの保存にコストがかかっていることがわかりました。今回は弊社で最も使用しているAmazon AuroraのMySQLのみを対象として、監査ログをCloudWatch Logsを経由せずS3に保存する仕組みを作成しました。

作成した仕組み

こちらのオープンソースの仕組みを参考に構築、またLambdaのソースを使いました。
github.com

Lambdaを使って監査ログをS3に保存する構成図

ただLambdaはMariaDBのみの対応でAuroraに対応しておらず、MariaDBとAuroraではいくつか仕様が異なる部分があったため、Auroraで動かせるように下記の変更を加えました。

  • ログのtimestamp
  • ログファイルのprefix
  • ログファイルのローテーションタイミング

監査ログに関するMariaDB MySQLとAurora MySQLの違い

1. ログのtimestamp

  • MariaDB
    • YYYYMMDD の後、ログに記録されたイベントの HH:MM:SS (24 時間制) が続く
  • Aurora
    • 記録されたイベントの UNIX タイムスタンプ (マイクロ秒の精度)

timestampはどこまでのログをS3にエクスポートしたかを記録する際に使用するので、このtimestamp取得部分をAurora用に変更しました。

lambda/internal/parser/auditlogparser.go

// ts, err := time.Parse("20060102 15:04:05", record[0])
timestamp, _ := strconv.ParseInt(record[0], 10, 64)
epochSeconds := timestamp / 1000000
t := time.Unix(epochSeconds, 0)
formatTime := t.Format("20060102 15:04:05")
ts, err := time.Parse("20060102 15:04:05", formatTime)

2. ログファイルのprefix

  • MariaDB
    • audit/server_audit.log
  • Aurora
    • audit/audit.log

生成される監査ログのファイル名のprefixが異なるので以下の部分を変更しました。

func NewRdsLogCollector(api rdsiface.RDSAPI, httpClient HTTPClient, region string, rdsInstanceIdentifier string, dbType string) *RdsLogCollector {
  return &RdsLogCollector{
    rds:                api,
    region:             region,
    httpClient:         httpClient,
    dbType:             dbType,
-   logFile:            "audit/server_audit.log",
+   logFile:            "audit/audit.log",
    instanceIdentifier: rdsInstanceIdentifier,
  }
}

3.ログファイルのローテーションタイミング

監査ログはサーバー内でファイルに書き込まれており、一定のタイミングでログファイルの入れ替え(ローテーション)が起こります。

  • MariaDB
    • ファイルサイズがSERVER_AUDIT_FILE_ROTATE_SIZEで設定したサイズを超えたとき(デフォルト1MB)
  • Aurora
    • ファイルサイズが100MBを超えたとき
    • 100MBを超えずに最初のログの書き込みから30分が経過した時

また書き込み中のログファイルとローテーションが完了したログファイルの名前規則もそれぞれ異なります。

  • MariaDB
    • 書き込み中のログファイル名は常にaudit/server_audit.log
    • ローテーション済みのログファイル名はaudit/server_audit.log.1 のようにファイル名に数字のsuffixがつく
  • Aurora
    • audit/audit.log.A.YYYY-MM-DD-HH-MM.1のようなファイル名になる
      • A: 0-3の数字が入る
        • Auroraでは常に4つのファイルに同時に書き込みが行われているためその判別
      • YYYY-MM-DD-HH-MM: 書き込み開始時間
      • 1: 数字のsufffix。開始時間が同じファイルが複数ある場合に数字が増えていく。

今回のLambdaでのS3に送る対象のログは、Lambda実行時にすでに書き込みが完了しローテーション済みのファイルのみを送るようにしています。
MariaDBと違いAuroraはローテーション済みのファイル判別が少し複雑なため、以下のように変更をしました。

lambda/internal/logcollector/rdslogcollector.go

func (l *LogFile) IsRotatedFile() bool {
-   matched, err := regexp.Match(`\.log\.\d+$`, []byte(l.LogFileName))
+// ログファイルが100MBに達しているかどうかを確認
+   if l.Size > 100000000 {
+     return true
+   }
+// ログのファイル名のsuffixからログファイルの生成時刻を取得
+   regex := regexp.MustCompile(`audit\.log\.\d{1}\.(\d{4}-\d{2}-\d{2}-\d{2}-\d{2})`)
+   matched := regex.FindStringSubmatch(l.LogFileName)
+   if len(matched) < 2 {
+     log.Warnf("Error matching log file: %v", l.LogFileName)
+     return false
+   }
+   createdTime, err := time.Parse("2006-01-02-15-04", matched[1])
+   // ローテーション完了時刻は現在時刻(UTC)の30分前
+   rotatedTime := time.Now().Add(-30 * time.Minute)
    if err != nil {
        log.Warnf("Error matching log file: %v", err)
        return false
    }
-   return matched
+// ログファイルの作成時刻がローテーション完了時刻より前であればログファイルはローテーション済み
+   return createdTime.Before(rotatedTime)

その他変更点

Classiでは1つのクラスターに対しライターとリーダーの2つのインスタンスが存在しています。元のLambdaではRDSのインスタンスを環境変数に取っており、このままだと1つのクラスターに対し2つのLambdaが必要になってしまうので、1クラスター1Lambdaで対応できるようにしたかったため、以下の2点を変更しました。

  • RDSのクラスター名を環境変数に取るようにする
  • RDSのクラスター名からインスタンスを取得し、各インスタンスに対してS3へのエクスポートを実行する

コストの変化

S3に送るようにしたことでCloudWatch Logsへの監査ログの送信を止めたところ、CloudWatchのコストを約53%減らすことができました。(10月中旬に監査ログの送信を止めたため9月と11月を比較しています)

CloudWatchのコスト変化のグラフ

実装後の感想

監査ログという観点だけでもMariaDBとAuroraで異なる部分があり、ドキュメントにない部分は実際にインスタンスを立ててみて動かして違いを探したりしました。
また作成したインフラリソースはTerraformで管理しています。今後新しくRDSを作成した際にも楽に監査ログをS3に送れるよう、for_eachを使ってリソースを定義するなど、拡張しやすくすることも考えました。ただ仕組みを作るだけでなく、今後その仕組みを自分以外が管理、運用してすることを考えて構築していくのがとても難しく時間がかかりましたが、とてもいい勉強になりました。結果的に大きくコスト削減にも貢献することができ、より達成感を感じられました。
今回はAurora MySQLに限定したLambdaになっているのですが、弊社ではPostgreSQLを使用している部分もあるので、他のRDSにも対応したLambdaになるようアプリケーションコードの改修もしていきたいです。

© 2020 Classi Corp.