2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Railsログ基盤をCloudWatch LogsからS3 + Athenaへ移行した話

Posted at

Railsログ基盤をCloudWatch LogsからS3 + Athenaへ移行した話

Railsアプリケーションのログ基盤を、CloudWatch LogsからS3 + Athenaに移行しました。本構成は現在ステージングと本番環境の両方に適用済みです。弊社ではAmazonのマネージドサービスを活用しつつ、Terraformでインフラを構築・管理しています。

image.png

背景と目的

本ログ基盤の刷新は、以下の目的に基づいています:

  • プロダクト改善施策の立案に必要な傾向分析
  • システム構成やワーカー数の最適化に向けた実測データの把握
  • 利用状況の可視化に基づくコスト見積もりやリソース配分
  • AI活用を見据えたログデータセットの整備と統一フォーマット化

これらをコスト効率よく実現するため、CloudWatch Log Insightsベースの構成からS3 + Athenaを活用した構成へ移行しました。


全体構成

ログの流れは以下の通りです:

CloudWatch Logs
 → Subscription Filter
   → Kinesis Data Firehose (+ Glue Data Catalog)
     → S3 (Parquet形式)
       → Athena

CloudWatch Logsに集約されたアプリケーションの標準出力から、Railsの構造化ログのみをSubscription Filterで抽出し、Firehose経由でS3に転送・保存しています。


検討したが導入しなかったもの

FireLens

FireLens(Fluent Bit)を使ってログを直接Firehoseへ転送する構成も検討しましたが、以下の理由から見送りました:

  • Fluent Bitのconf記述に関する学習コストが高く、運用負荷を避けたかった
  • WAFをすり抜けるPOST Flood攻撃などに即時対応するのに、CloudWatch Logsによる即時可視化が必要だった
  • 冗長性の観点から、Firehose障害時にもCloudWatchにログが残る構成の方が望ましい

参考:

Embedded Metrics Format

カスタムメトリクスを取る際、アプリ側のAPI呼び出しに付随するレイテンシや非同期処理の実装を避ける目的でEmbedded Metrics Formatの導入も検討しました。
しかし最終的にすべてのログをCloudWatch Logs経由にしたため、CloudWatch Logsのメトリクスフィルター機能を活用する方が構成としてシンプルで明快と判断しました。

参考:


Railsログの整備と活用

ログの構造化と秘匿情報のフィルタリング

RailsのログをLogrageでJSON形式に変換し、Athenaで扱いやすい形に整備しています。特にPOSTパラメータに含まれる長文や秘匿情報に対しては、トランケート処理とフィルタリングを行っています。

config.lograge.custom_options = ->(event) do
  exceptions = %i[controller action format authenticity_token]
  {
    datetime: Time.current.strftime('%F %T'),
    params: ActiveSupport::ParameterFilter.new(
      config.filter_parameters +
      [->(_k, v) { v.replace(v.truncate(256, omission: '...')) if v.is_a?(String) }]
    ).filter(event.payload[:params].except(*exceptions)),
    request_queue_time_ms: event.payload[:request_queue_time_ms],
    bot: !!event.payload[:bot]
  }
end

参考:

Pumaのキュー時間(request_queue_time_ms)の記録と可視化

RailsのコントローラでX-Request-Startヘッダーを元にPumaのキュー時間を計算し、ログに含めています。これはCloudWatch Logs上でメトリクスフィルターを使い、可視化・監視可能としています。
Railsパフォーマンスアポクリファでpumaのワーカー数を決定するための指標として紹介されていたため取得するようにしました。

def set_request_queue_time
  x_request_start = request.headers['X-Request-Start']
  if x_request_start
    start_time = Time.at(x_request_start.sub('t=', '').to_f / 1000)
    request.env['request_queue_time_ms'] = ((Time.current - start_time) * 1000).round(2)
  end
end
resource "aws_cloudwatch_log_metric_filter" "request_queue_time" {
  name           = "RequestQueueTimeMs"
  log_group_name = aws_cloudwatch_log_group.web_app.name
  pattern        = "{ $.request_queue_time_ms = * }"

  metric_transformation {
    name      = "RequestQueueTimeMs"
    namespace = "ServiceName/Staging"
    value     = "$.request_queue_time_ms"
    unit      = "Milliseconds"
  }
}

参考:


AWS側での設計ポイント

Firehose受信時刻でのパーティション化

CloudWatch LogsからのSubscription Filter経由で届くログは圧縮されたDataMessage形式のため、FirehoseのMetadata Extractionを使ってログ内のtimestampでパーティションを切ることができません。そのため、Firehose受信時のUTCを使ってパーティションを作成しました。

参考:

resource "aws_glue_catalog_table" "rails_log" {
  name          = "rails_log"
  database_name = aws_glue_catalog_database.log.name
  table_type    = "EXTERNAL_TABLE"

  parameters = {
    "projection.enabled"        = "true"
    "projection.year.type"      = "integer"
    "projection.year.range"     = "2023,2030"
    "projection.month.type"     = "integer"
    "projection.month.range"    = "1,12"
    "projection.month.digits"   = "2"
    "projection.day.type"       = "integer"
    "projection.day.range"      = "1,31"
    "projection.day.digits"     = "2"
    "storage.location.template" = "s3://${aws_s3_bucket.logs.bucket}/rails/year=$${year}/month=$${month}/day=$${day}/"
  }

  storage_descriptor {
    location      = "s3://${aws_s3_bucket.logs.bucket}/rails/"
    input_format  = "org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat"
    output_format = "org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat"

    serde_info {
      serialization_library = "org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe"
    }
  }
}

Firehoseのバッファ設定とParquet化

Athenaの推奨に従い、Firehoseのバッファサイズは128MB、間隔は15分に設定。Parquet形式で保存することで、クエリ時の読み取り効率と圧縮効率を高め、クエリコストも抑制しています。

参考:

S3バケットの分離とIAM設計

RailsログとAthenaクエリ結果を格納するS3バケットを分離することで、サービス本体からのアクセス権限(読み取り専用)を明確に制御できるようになり、IAMポリシーの設計が簡素化されました。


詰めきれなかったところ

  • Firehoseの変換エラーはCloudWatch Logsにしか出力されないため、CloudWatch Alarmを組んで通知する運用が必要
  • ログ内のdatetimeはJSTである一方、パーティションはUTC基準のため、Athenaで絞り込みを行う場合はパーティションを1日広めに取る必要がある

今後やりたいこと

  • QuickSightを使用したビジネス側のダッシュボード整備
  • サービス利用者へのアナリティクス機能提供
  • Firehoseやその他AWSリソース側のエラー通知

おわりに

CloudWatch LogsからAthenaへ移行するにあたって、構成はシンプルながらも、細かい仕様や制約に向き合う必要がありました。

運用開始後は、Athenaを用いたアクセス傾向分析や、API利用実態の調査・コスト見積もりに既に活用されており、移行の価値は十分にあったと実感しています。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?