実践セキュリティ監視基盤構築(13): ログ保全の実装要点
この記事はアドベントカレンダー実践セキュリティ監視基盤構築の13日目です。
今回は、ログ収集によって取得したログをCloud Storageにどのように保存するかについて説明します。ログの保存にはいくつかのポイントがあります。ログの保存先のバケット名やオブジェクト名の設計、ログの保存形式、ログの保存期間などを考慮する必要があります。
今回のアーキテクチャでは、Cloud Storageは「ログの収集」と「ログの変換・取込」をつなぐストレージとして利用することが主な目的です。すぐにログをデータウェアハウスであるBigQueryに投入するため、Cloud Storageは一時的なストレージとして扱うこともできます。しかし、Cloud Storageにログデータを残しておくことによるメリットもあるため、それについても説明します。
Cloud Storageの設計
Pull型でもPush型でも、取得したログは構造データとして保存することを前提とします[1]。ここでの構造データとは、フィールドと値のペアで表現されるデータのことです。この際のデータ形式はJSONやCSVなどが一般的です。
取得したログはなるべく未加工のままCloud Storageに保存します。これは、ログのパースや解析を次の「ログの変換・取込」に回すためです。ただし、データのサイズがそのままストレージのコストに影響するため、ログの保存形式や圧縮形式を工夫することでコストを抑えることができます。一般的にはgzipなどの圧縮形式を使って保存することが多いです。
Cloud StorageにPub/Subへのイベント通知設定をしておくことで、後続の処理に引き渡すことができます。この際、通知にはログの保存先のバケット名やオブジェクト名が含まれるため、後続の処理でその情報を使ってログを取得することができます。そのため、ログの保存先のバケット名やオブジェクトの名前(パス)は無秩序であってもパイプラインの処理としては問題ありません。しかし、管理上の観点からは適切なデータ配置設計をしておくことをお勧めします。
ここから、ログ保存先のバケットやオブジェクト名を考える上でのポイントをいくつか紹介します。
バケットの設計
ログの保存先のバケットの設計は大きく分けて、(1)セキュリティ監視基盤関連のバケットを1つに集約する方法、(2)取得元のサービスごとにバケットを分割する方法の2つが考えられます。ほとんどのケースでは、(2)の取得元のサービスごとにバケットを分割する方法が適していると考えられます。
(1) セキュリティ監視基盤関連のバケットを1つに集約する方法
セキュリティ監視基盤関連のバケットを1つに集約することで、ログのデータを一元管理することができます。これによって関連リソースを探す際に便利です。また、コスト管理の観点で、セキュリティ監視基盤関連のリソースがまとまっていると、状況を把握しやすくなります。
一方で、バケット内のパス構造が複雑化する可能性があります。これによってアクセス制御の設定やオブジェクトのライフサイクルマネジメントの設定が複雑になる可能性があります。また、バケットごとにサポートされるデータ書込・取得のスループットが制限されるため、この制限を超えないように意識する必要があります。(ただし、これは申請によって制限を緩和できます)
(2) 取得元のサービスごとにバケットを分割する方法
取得元のサービスごとにバケットを分割することで、ログの保存先を取得元ごとに分けることができます。バケットごとにアクセス権限を設定することで、ログのアクセス制御やライフサイクルマネジメントをわかりやすく管理することができます。特にアクセス制御に関しては、ログの種類によって公開できる範囲が異なるため、明確にバケットで分離しておくことでシンプルに権限を設定できます。
一方でバケットの数が増えるため、バケットの管理に注意を払う必要があります。例えば、バケット共通で適用するべきポリシーがある場合、バケットごとに設定するため設定漏れなどに注意が必要です。これはバケットを作成する際にIaC (Infrastructure as Code) を使って共通のポリシーなどを適用することで解決できます。
オブジェクト名の設計
オブジェクト名の設計もバケットの設計と同様に、完全な正解はありません。しかし、いくつか考慮すべきポイントを挙げておきます。基本的にPull型によるログの取得を前提としていますが、いくつかのポイントはPush型でも同様の考え方が適用できます。
-
バケットにログ以外のデータが入ることを考慮する: バケットにログ以外のデータ(例えばログに関するメタデータ)をまとめて保存したいというケースがあります。そのため、まずログの保存パスであることを示す
/logs
などを先頭につけておくのが無難です。 - バージョニングできるようにしておく: 例えばログのスキーマなどが変わったり、後続の処理の都合でログの保存形式を変えないといけない場合があります。その際、後続の処理がパス名に依存すると変更が面倒になるため、バージョニングごとにディレクトリを分けると変更がしやすくなります。
- ログの種類、スキーマごとにパスを分ける: ログの種類やスキーマの違いは後続のパース、変換、投入の処理に影響を与えます。同じシステムから取得するログでも異なるスキーマのログが得られる場合もあるため、取得の時点でログの種類やスキーマがわかるようにパスを分けておくと、後続の処理がスムーズになります。
- ログの取得日時を含める: ログの取得日時がわかるようにパスに含めることで、障害が発生した場合などに特定の日時のログを探しやすくなります。理想としては「ログの取得日時」ではなく「ログ内のタイムスタンプ」の方がより便利ですが、ログ内のタイムスタンプを反映するにはログのパースが必要になるため、ログの取得日時をパスに含めるのが現実的です。
- 重複を許容し、パス名が衝突しないようにする: ログの取得については重複が発生するよりも、ログの取得漏れが発生することのほうが問題となります。そのため、ログの取得日時を含めつつ、もし同じ時刻に複数のログが取得された場合でもパス名が衝突しないようにすることが重要です。
- ファイル名の部分だけである程度の一意性を持たせる: ログから生成されたオブジェクトは基本的にパイプラインでしか使われませんが、稀にトラブルシュートや検証の目的でダウンロードする場合があります。その際、ファイル名だけで一意性を持たせておくことで、ログの取得日時やログの種類を見なくても特定のログを見つけやすくなります。
- 一度の処理で複数のオブジェクト保存の可能性を考える: 例えばPull型のAPIによる取得でページネーションが発生した場合は1回の処理で複数のオブジェクトが生成されます。もちろん結合してから保存することもできますが、あまりに大きいデータだと結合時にメモリがあふれるなどのリスクがあるため、なるべくそのまま保存するのが望ましいです。
- ファイル名には拡張子を含める: ログの形式に関するヒントになるので、拡張子は適切に含めておくと便利です。
上記のようなポイントを踏まえて、ログの保存先のバケット名やオブジェクト名を設計していきます。一例として以下のようなパス名となります。
/logs/{version}/{schema}/{year}/{month}/{day}/{hour}/{datetime}_{slug}_{seq}.{ext}
{}
で囲まれた部分は変数として扱い、実際の値に置き換えます。
-
{version}
: ログのバージョン。例えばv1
など。 -
{schema}
: ログのスキーマ名。例えばaccess_log
やerror_log
など。 -
{year}
: ログの取得日時の年。 -
{month}
: ログの取得日時の月。 -
{day}
: ログの取得日時の日。 -
{hour}
: ログの取得日時の時。 -
{datetime}
: ログの取得日時。例えば20241123T163936
のような形式。 -
{slug}
: ログ取得の一意性を表すような文字列。ランダムな文字列やハッシュ値など。 -
{seq}
: ログの取得日時が同じ場合に複数のログが取得された場合の連番。例えば0000
など。 -
{ext}
: ログの拡張子。例えばjson.gz
など。
階層の時刻についてはあまりにも細かくするとパスが深くなりすぎるため、適宜調整してください。時 (hour) までで十分な場合もあります。また、{slug}
や {seq}
は必ずしも必要ではありませんが、ログの取得日時が同じ場合に複数のログが取得された場合に重複を避けるために利用します。
実際には以下のようなパス名となります。
/logs/v1/some-schema/2024/11/23/16/20241123T163936_eh8hIQ1P_0000.json.gz
データレイクとしてデータを残すメリットと Object Lifecycle Management
先述した通り、Cloud Storageは一時的なストレージとして利用することもできます。しかし、Cloud Storageにログデータを残しておくことによるメリットもあります。これらのメリットとストレージにデータを残すコストを考慮した上でどの程度元データを残すのかを決め、Object Lifecycle Managementを設定することが重要です。
Object Lifecycle Management は、オブジェクトのライフサイクルを管理するための機能です。例えば、ある期間が経過したオブジェクトを削除する、ある期間が経過したオブジェクトを他のストレージクラスに移動する、などの設定が可能です。これによって、データの保全期間を設定することができます。
(1) DWHのリビルドができる
DWHにログデータを投入した際の元データをそのまま残しておくことで、DWHのリビルドが容易になります。DWHのスキーマを変更しなければならない場合やDWHのデータが壊れた場合、またはログの変換や取込処理のバグが後から発覚した場合にも、元データをそのまま残しておくことで新しいテーブルを作り直すことができます。
再構築に関してはCloud Storageからのデータ再取得や変換・取込処理が再度発生するのでコストは掛かりますが、正しく修正された状態のテーブルを利用できるようになります。これによって不具合や不整合が蓄積された状態での分析を行うことを防ぐことができ、分析をしたりルールをメンテナンスするメンバーの認知負荷を下げられます。
DWHリビルドの実行を考える場合は、Lifecycle Managementにおいてストレージクラスの変更は慎重に検討したほうが良いでしょう。リビルドの際は原則全てのオブジェクトにアクセスする必要があります。ストレージクラスの変更は保存の料金は安くなるものの、アクセスをする場合は料金が割増になったり、遅延が発生することがあります。
そのため、一定期間が経過したオブジェクトはリビルドに利用しないという方針で運用するというアプローチもあります。これであればストレージクラスを変更したり、オブジェクトを削除することでコストを圧縮できます。
(2) オリジナルデータの確認ができる
DWHに投入されたデータを使って分析をするのが基本形となりますが、場合によってはオリジナルデータを確認することが必要になることがあります。例えば、DWHに投入されたデータに不整合が疑われたり、DWHに投入する過程で落としたデータが必要になるようなケースです。
このケースの場合は、古くなったオブジェクトのストレージクラス変更が有効です。オリジナルデータを確認することが必要な場合は、多少アクセスのコストが上がっても、そのオブジェクトを取得して確認することができます。
まとめ
ログの保存先のバケット名やオブジェクト名の設計について説明しました。ログの保存先のバケット名やオブジェクト名は、ログの取得元やログの種類、ログの取得日時などを考慮して設計する必要があります。また、コスト圧縮のため、Cloud Storageにログデータを残しておくことによるメリットと、Object Lifecycle Managementによるデータの保全についても検討するのが良いかと思います。
次回は、ログの取得と保全の実装例を紹介します。
-
OSやミドルウェアからメッセージ形式で出力されるログもありますが、何らかの方法で構造データに変換することを前提とします。例えば、ログメッセージごとに正規表現を使って必要な値を抜き出し、構造データに変換する方法があります。 ↩︎
Discussion