メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

監査ログの保管先をRDBからS3に移行する

こんにちは。サーバーサイドエンジニアの @atolix_です。

今回はメドピアで運用しているアプリケーションのkakariの監査ログをDB管理からS3管理に移行したので、その方法と手順について紹介したいと思います。

kakari.medpeer.jp

背景

従来kakariではAuditedを用いて、監査ログを専用のauditsテーブルに保管する処理を行っていました。

github.com

# application_record.rb

class ApplicationRecord < ActiveRecord::Base
  ...
  include Auditable
# auditable.rb

module Auditable
  extend ActiveSupport::Concern

  included do
    audited
    ...
  end

しかしレコードの変更の度にauditsテーブルへの書き込みが走る為、DBマイグレーションを行なった際に書き込みのロックが発生して、アプリケーションの動作に影響が出るといったインシデントが発生してしまいました。

今回はこの恒久対応としてDBへの書き込みを廃止してS3に監査ログを保管できるように変更を加えていきます。

実装概要

監査ログを通常のアプリケーションログと分別してS3に保管したいので、以下のような構成を組みます。

アプリケーションログの出力は従来通りCloudWatch Logsに送信、一方で監査ログはKinesis Firehoseを経由してS3に格納できるようにFireLensを間に置いて二箇所に振り分ける想定です。

AuditLoggable

今回は食べチョクさんが作成してくださったAuditLoggableをインストールして、監査ログをファイルに出力するように変更します。 tech.tabechoku.com

Auditedと同様にapplication_record.rbに追記をすることでレコードの変更を追跡することが出来ます。

# application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include Auditable
  ...
  extend AuditLoggable::Extension
  log_audit

initializersでaudit.logの書き込み場所を指定します。

# config/initializers/audit_loggable.rb

AuditLoggable.configure do |config|
  if Rails.env.test? || Rails.env.development?
    config.auditing_enabled = false
  else
    config.audit_log_path = Rails.root.join("..", "..", "opt", "audit.log")
  end
end

DBへの書き込みをしていた時と同様のリアルタイム性を保つ為に、audit.logへの書き込みを標準出力に吐き出すようにシンボリックリンクを貼ります。

# Dockerfile
...
RUN ln -sf /dev/stdout /opt/audit.log

標準出力された監査ログを確認すると、以下のようなjsonで出力されました。

{
  "timestamp": "2024-08-06T18:00:59.981+09:00",
  "record": {
    "auditable": {
      "id": 1,
      "type": "ModelType"
    },
    "user": {
      "id": 1,
      "type": "Admin::Account"
    },
    "action": "update",
    "changes": "{\"name\":[\"xxx\",\"yyy\"]}",
    "remote_address": "xxx.xxx.xxx.x",
    "request_uuid": "xxxxxxxxxx"
  }
}

これで監査ログが標準出力に出るようになったので、現在Railsからはアプリケーションログと監査ログの2種類が混ざって出力されることになります。

次はFireLensを通して2種類のログを振り分けて送信できるようにインフラ構成を修正します。

FireLens

AWSではFireLensを使用することでコンテナのログルーティング設定が簡単に実装出来ます。

docs.aws.amazon.com

コンテナ定義内でRailsのログドライバーにFireLensを指定した後、カスタム設定のファイル(今回はfluent-bit/etc/extra.conf)と送信先のKinesis Firehoseを設定します。

# container_definition.json

 {
    "name": "rails",
     ...
    "logConfiguration": {
      "logDriver": "awsfirelens" # FireLensを指定
    },
...
},
...
{
    "essential": true,
    "image": "${fluentbit_image}",
    "name": "fluentbit",
    "firelensConfiguration": {
      "type": "fluentbit",
      "options": {
        "config-file-type": "file",
        "config-file-value": "/fluent-bit/etc/extra.conf"
      }
    },
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${log_group}",
        "awslogs-region": "${region}",
        "awslogs-stream-prefix": "firelens"
      }
    },
    "environment": [ # 送信先を外部から指定できるようにロググループやリージョンの情報を環境変数に格納する
      {
        "name": "LOG_GROUP",
        "value": "${log_group}"
      },
      {
        "name": "REGION",
        "value": "${region}"
      },
      {
        "name": "TARGET_FIREHOSE",
        "value": "${firehose_name}"
      }
    ]
  }

次にログをCloudWatch LogsとKinesis Firehoseの二箇所に分岐して流せるようにFluent Bitの設定を加えていきます。 先にDockerfileに必要な設定ファイルを記述しておきます。

# Dockerfile

FROM amazon/aws-for-fluent-bit:2.32.2 

COPY extra.conf /fluent-bit/etc/extra.conf
COPY parsers.conf /fluent-bit/etc/parsers.conf
COPY categorize_logs.lua /fluent-bit/etc/categorize_logs.lua
COPY stream-processor.conf /fluent-bit/etc/stream-processor.conf
COPY output.conf /fluent-bit/etc/output.conf

Fluent Bit内の設定詳細

ここからはログの大まかな処理の流れを説明していきます。

最初にRailsコンテナから受け取るログはFireLensを通して*-firelens-*でタグ付けされるので、分かりやすくする為に一旦stream-processor.conf内でcombine.webにタグを集約させます。

# stream-processor.conf
[STREAM_TASK]
    Name web
    Exec CREATE STREAM web WITH (tag='combine.web') AS SELECT * FROM TAG:'*-firelens-*';

combine.webタグが付与されたログはjsonパースされます。

# parsers.conf
[PARSER]
    Name         json
    Format       json
    Time_Key time
    Time_Format %Y-%m-%dT%H:%M:%S.%L%z
    Time_Keep On
    Time_Offset +0900

後に再度stream-processor.confを通った時にタグを書き換えやすくするために、一度luaスクリプトで仮のタグ情報を付与します。

function categorize_logs(tag, timestamp, record)
  if record["record"] ~= nil then
    record["new_tag"] =  "audit"
  else
    record["new_tag"] =  "rails"
  end

  return 2, timestamp, record
end

stream-processor.conf内でアプリケーションログと監査ログを判別できるようにタグを書き換える処理を加えます。

# stream-processor.conf
...

[STREAM_TASK]
    Name  audit
    Exec  CREATE STREAM audit WITH (tag='logs.audit') AS SELECT * from TAG:'*combine.web*' WHERE new_tag = 'audit';

[STREAM_TASK]
    Name  rails
    Exec  CREATE STREAM rails WITH (tag='logs.rails') AS SELECT * from TAG:'*combine.web*' WHERE new_tag = 'rails';

output.confでは先ほどstream-processor.confで書き換えたタグを元に、従来のコンテナログと監査ログをそれぞれCloudWatch Logs, Firehoseに振り分けます。

# output.conf

# 通常のコンテナログは従来通り CloudWatch Logs に送信する
[OUTPUT]
    Name              cloudwatch_logs
    Match             logs.rails # stream-processor.confで書き換えたタグ
    region            ${REGION}
    log_group_name    ${LOG_GROUP}
    log_stream_prefix rails

# 監査ログは Kinesis Firehose に送信する
[OUTPUT]
    Name              kinesis_firehose
    Match             logs.audit
    region            ${REGION}
    delivery_stream   ${TARGET_FIREHOSE}

最終的なextra.confとログの流れは以下のようになります。

# extra.conf

[SERVICE]
    Parsers_file parsers.conf
    Streams_File stream-processor.conf

[FILTER]
    Name         parser
    Match        combine.web
    Key_Name     log
    Parser       json

[FILTER]
    Name         lua
    Match        combine.web
    script       categorize_logs.lua
    call         categorize_logs

@INCLUDE output.conf

Kinesis Firehose

FireLensからS3にログを送信する間にKinesis Firehoseを挟みます。

一応S3プラグインを使って直接バケットに監査ログを送信することも可能ですが、今回はFargateのような永続ディスクのない環境でFluent Bitを実行しているので、突然のコンテナの停止時に監査ログをロストしてしまう可能性が考えられます。

その為に分散バッファーとしてKinesis Firehoseを経由して継続的にログを送信できるように構成しています。

github.com

設定自体は監査ログを格納するS3バケットをdestinationに指定したシンプルな内容です。

resource "aws_kinesis_firehose_delivery_stream" "fluentbit" {
  ...
  destination = "extended_s3"

  extended_s3_configuration {
    bucket_arn  = aws_s3_bucket.audit_logs.arn
    buffering_size     = 10  # MB
    buffering_interval = 300 # seconds
    compression_format = "GZIP"
    custom_time_zone   = "Asia/Tokyo"
    ...
}

動作確認

該当のバケットを確認すると監査ログが蓄積されていることが分かります。

念の為1週間ほど従来のDBへの保管とS3への保管を並行で稼働させて、レコード数に差がないことまで確認出来たらDBへの書き込みを停止して完了です。

まとめ

FireLensとKinesis Firehoseを使うことで比較的簡単に監査ログの保存先をS3に移行することが出来ました。 S3に保管した監査ログはAthena等と組み合わせてクエリ検索出来るようにしておくと良さそうです。

一方でDBへの書き込みと比較すると監査ログのロストの確率は少しだけ上がるので、移行する際にはログの欠損が起きないか検証を十分にする必要があります。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp