こんにちは。サーバーサイドエンジニアの @atolix_です。
今回はメドピアで運用しているアプリケーションのkakariの監査ログをDB管理からS3管理に移行したので、その方法と手順について紹介したいと思います。
背景
従来kakariではAuditedを用いて、監査ログを専用のauditsテーブルに保管する処理を行っていました。
# 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を使用することでコンテナのログルーティング設定が簡単に実装出来ます。
コンテナ定義内で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を経由して継続的にログを送信できるように構成しています。
設定自体は監査ログを格納する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