LayerX Fintech事業部(三井物産デジタル・アセットマネジメント(MDM)に出向)で、セキュリティ、インフラ、情シス、ヘルプデスク、ガバナンス・コンプライアンスエンジニアリングなどを担当している @ken5scal です。
当社はSIEMソリューション(DataDog SIEM)に加えて、最終的にはデータレイクハウスでのデータ分析にする意図でAmazon Security Lakeを活用しています。 これに関する過去記事は こちら からご参照ください。
さて、 Amazon Security Lakeのお金でチョット困った話で紹介したように、Amazon Security Lake(以降 Security Lake)は簡単にデータ基盤をセットアップしてくれるものの、 コスト含む運用最適化において若干の懸念が出始めました。 これはSecuirty Lakeの限界というよりも、当チームにおけるSecurity Lake、特にそれがデータ管理に採用しているテーブルフォーマット「Apache Iceberg」に関する理解が薄いことに起因しています。 S3 Tableがリリースされた今、このまま万全と運用していても場当たり的な対応にしかならなず、よりベターな可能性を逃しそうな予感があります。
そういった懸念から、私は「Apache Iceberg: The Definitive Guide: Data Lakehouse Functionality, Performance, and Scalability on the Data Lake」(以下、O’Reilly本)を読了しました。 その理解を深めるため、改めて実稼働しているソリューションを観察するのが本ブログシリーズ(予定)の趣旨です。
https://amzn.asia/d/8GiLjWLamzn.asia
本エントリでは、まずはApache Icebergのアーキテクチャ...のうちのIceberg Catalog層とMetadata fileをSecurity Lakeから眺めていこうと思います。

Iceberg CatalogとSecurity Lake
Iceberg Caltalogは、テーブル パスを現在の状態を表すメタデータ ファイルのパスにマッピングします。 また、並行した複数のwrite job下であってもデータロスの発生を避けて、メタデータ ポインターを最新にするアトミック操作を実現します。
これを実装するのはSecurity Lakeそのものではありません。したがって、いきなりですが、本ブログ内でSecurity Lakeを見る必要はありません。 Security Lakeを有効にした後、実際のデータ管理はAWS Lake Formationが担うからです。実体は、Data Catalog > Tableにあります。

ここでは、amazon_security_lake_table_ap_northeast_1_cloud_trail_mgmt_2_0 のテーブルを見てみましょう。しっかりTable formatが Apache Iceberg になっていますね。
クリックし、「Advanced table properties」を開くと metadata_location を確認できます。Apache Iceberg形式のテーブルカタログが向けているメタデータポインタですね。

なお、Comapaction、Retention、Orphan file deletionステータスも見られます。これらはO’Reilly本4章に該当するパフォーマンス最適化のエントリ(予定)で取り上げようと思います。
metadata layer層のmetadata filesとSecurity Lake
ポインタが向いているmetadafaileは、実際のdata fileをオブジェクトとして置かれたS3バケット aws-security-data-lake-ap-northeast-1-ランダム値 の aws/CLOUD_TRAIL_MGMT/2.0/metadata/ にマッピングされています。
実際のメタデータファイルの内容は次のような形です。
なお、説明(簡易)は Apache Iceberg とは何か - 流沙河鎮 を基本的に引用させていただき、必要に応じて https://iceberg.apache.org/ を参照しています。
| ィールド | 使用上のrequirements | データ型 | サンプル値 | 説明(簡易※) |
|---|---|---|---|---|
| format-version | required | int | 2(固定) | フォーマットのバージョン番号 |
| table-uuid | required | string | テーブルごとに固定なUUID | テーブルの作成時に生成される、テーブルを識別するUUID |
| location | required | string | s3://aws-security-data-lake-ap-northeast-1-ランダム文字/aws/CLOUD_TRAIL_MGMT/2.0" |
テーブルのベースパス。これはwriterがdata files, metadatafiles, table meta datadata filesの格納場所を決定するために使用される |
| last-sequence-number | optional | 64-bit signed integer | 111111 | テーブルのスナップショットの順序をトラックするのに用いるシーケンス |
| last-updated-ms | required | integer | 1738086199160 | テーブルが最後に更新されたunixエポックからのミリ秒単位のタイムスタンプ。各table metadata fileは、書き込みの直前にこのフィールドを更新される |
| last-column-id | required | integer | 95(OCSFのtype_idの値) | テーブルのカラムIDの中で最も大きいもの。Schema Evolution時に常に未使用のIDが割り当てられるようにするために使用される。 |
| current-schema-id | optional | integer | 0 | 現在のテーブルスキーマのID |
| schemas | required | list | 実質的にlist長1のOCSFフォーマット | スキーマのリストで、schema-idで管理する。CloudTrailの場合、こちらのスキーマとなっている。 |
| default-spec-id | required | integer | 0 | Writerがデフォルトで使用する現時点のパーティション仕様のID |
| partition-specs | required | list | 略 | パーティション仕様(レコードからパーティション値を切る方法の定義)のリスト。 | required | integer |
| last-partition-id | required | integer | 1003 (ocsfスキーマの time_dt を day に変換したspec id) |
|
| default-sort-order-id | required | integer | 0 | デフォルトの sort-orders のid |
| sort-orders | required | list (sort-orders ) | { "order-id" : 0, "fields" : [ ] } |
ソート順序のリスト |
| properties | optional | map | テーブルプロパティのマップ。これは読み書きに影響する設定を制御するために使用されるもので、任意のメタデータ付与に使用することは想定されていない。本テーブルにおいてはSecurityLake特有の内容になっている | |
| current-snapshot-id | optional | 64-bit signed integer | 1111111111111111111 | 現在のテーブルスナップショットのID。refsのmainブランチの現在のIDと同じでなければならない。 |
| refs | optional | optional | map | { "main" : { "snapshot-id" : 1111111111111111111, "type" : "branch" } } | スナップショット参照のマップ。マップ キーはテーブル内の一意のスナップショット参照名であり、マップ値はスナップショット参照オブジェクトです。たとえ refs マップが null であっても、current-snapshot-id を指すメイン ブランチ参照が常に存在します。 |
| snapshots | optional | optional | list (snapshot ) | 略 | 有効なスナップショットのリスト。有効なスナップショットとは、すべてのdata fileがファイルシステムに存在するスナップショットを指す。Data fileは、それがリストされていた最後のスナップショットがガベージコレクションされるまで、ファイルシステムから削除してはいけない。 |
| statistics | optional | optional | list (statistics) | 空 | { "timestamp-ms" : 1738086105233, "snapshot-id" : 1111111111111111111 } |
| snapshot-log | optional optional | list (snapshot log) | タイムスタンプとスナップショットIDのペアのリストで、テーブルの現在のスナップショットの変更をエンコードする。current-snapshot-idが変更されるたびに、新しいエントリが last-updated-msと新しいcurrent-snapshot-idで追加される必要がある。スナップショットが有効なスナップショットのリストから期限切れになると、期限切れになったスナップショットのエントリは、snapshot-logから安全に削除することができる。 |
以上が、Security Lakeにおける Apache Iceberg Catalogとそのポインタ先のmetadata fileでした。 次回は他のmetadata layerに関連するアーキテクチャ要素を見ていきたいと思います。
partition-specsのサンプル
[ {
"spec-id" : 0,
"fields" : [ {
"name" : "asl_version",
"transform" : "identity",
"source-id" : 29,
"field-id" : 1000
}, {
"name" : "region",
"transform" : "identity",
"source-id" : 28,
"field-id" : 1001
}, {
"name" : "accountid",
"transform" : "identity",
"source-id" : 27,
"field-id" : 1002
}, {
"name" : "time_dt_day",
"transform" : "day",
"source-id" : 3,
"field-id" : 1003
} ]
} ],
"last-partition-id" : 1003,
"default-sort-order-id" : 0,
"sort-orders" : [ {
"order-id" : 0,
"fields" : [ ]
} ]
propertiesのサンプル
"properties" : { "history.expire.max-snapshot-age-ms" : "21600000", "client.factory" : "com.amazonaws.securitylake.metastoremanagement.SecurityLakeAwsClientFactory", "io-impl" : "org.apache.iceberg.aws.s3.S3FileIO", "s3.sse.type" : "s3", "metadata_location" : "metadata", "write.metadata.delete-after-commit.enabled" : "true", "schema.name-mapping.default" : "[ {\n \"field-id\" : 1,\n \"names\" : [ \"metadata\" ],\n \"fields\" : [ {\n \"field-id\" : 31,\n \"names\" : [ \"product\" ],\n \"fields\" : [ {\n \"field-id\" : 36,\n \"names\" : [ \"version\" ]\n }, {\n \"field-id\" : 37,\n \"names\" : [ \"name\" ]\n }, {\n \"field-id\" : 38,\n \"names\" : [ \"vendor_name\" ]\n }, {\n \"field-id\" : 39,\n \"names\" : [ \"feature\" ],\n \"fields\" : [ {\n \"field-id\" : 40,\n \"names\" : [ \"name\" ]\n } ]\n } ]\n }, {\n \"field-id\" : 32,\n \"names\" : [ \"event_code\" ]\n }, {\n \"field-id\" : 33,\n \"names\" : [ \"uid\" ]\n }, {\n \"field-id\" : 34,\n \"names\" : [ \"profiles\" ],\n \"fields\" : [ {\n \"field-id\" : 41,\n \"names\" : [ \"element\" ]\n } ]\n }, {\n \"field-id\" : 35,\n \"names\" : [ \"version\" ]\n } ]\n}, {\n \"field-id\" : 2,\n \"names\" : [ \"time\" ]\n}, {\n \"field-id\" : 3,\n \"names\" : [ \"time_dt\" ]\n}, {\n \"field-id\" : 4,\n \"names\" : [ \"cloud\" ],\n \"fields\" : [ {\n \"field-id\" : 42,\n \"names\" : [ \"region\" ]\n }, {\n \"field-id\" : 43,\n \"names\" : [ \"provider\" ]\n } ]\n}, {\n \"field-id\" : 5,\n \"names\" : [ \"api\" ],\n \"fields\" : [ {\n \"field-id\" : 44,\n \"names\" : [ \"response\" ],\n \"fields\" : [ {\n \"field-id\" : 49,\n \"names\" : [ \"error\" ]\n }, {\n \"field-id\" : 50,\n \"names\" : [ \"message\" ]\n }, {\n \"field-id\" : 51,\n \"names\" : [ \"data\" ]\n } ]\n }, {\n \"field-id\" : 45,\n \"names\" : [ \"operation\" ]\n }, {\n \"field-id\" : 46,\n \"names\" : [ \"version\" ]\n }, {\n \"field-id\" : 47,\n \"names\" : [ \"service\" ],\n \"fields\" : [ {\n \"field-id\" : 52,\n \"names\" : [ \"name\" ]\n } ]\n }, {\n \"field-id\" : 48,\n \"names\" : [ \"request\" ],\n \"fields\" : [ {\n \"field-id\" : 53,\n \"names\" : [ \"data\" ]\n }, {\n \"field-id\" : 54,\n \"names\" : [ \"uid\" ]\n } ]\n } ]\n}, {\n \"field-id\" : 6,\n \"names\" : [ \"dst_endpoint\" ],\n \"fields\" : [ {\n \"field-id\" : 55,\n \"names\" : [ \"svc_name\" ]\n } ]\n}, {\n \"field-id\" : 7,\n \"names\" : [ \"actor\" ],\n \"fields\" : [ {\n \"field-id\" : 56,\n \"names\" : [ \"user\" ],\n \"fields\" : [ {\n \"field-id\" : 60,\n \"names\" : [ \"type\" ]\n }, {\n \"field-id\" : 61,\n \"names\" : [ \"name\" ]\n }, {\n \"field-id\" : 62,\n \"names\" : [ \"uid_alt\" ]\n }, {\n \"field-id\" : 63,\n \"names\" : [ \"uid\" ]\n }, {\n \"field-id\" : 64,\n \"names\" : [ \"account\" ],\n \"fields\" : [ {\n \"field-id\" : 66,\n \"names\" : [ \"uid\" ]\n } ]\n }, {\n \"field-id\" : 65,\n \"names\" : [ \"credential_uid\" ]\n } ]\n }, {\n \"field-id\" : 57,\n \"names\" : [ \"session\" ],\n \"fields\" : [ {\n \"field-id\" : 67,\n \"names\" : [ \"created_time_dt\" ]\n }, {\n \"field-id\" : 68,\n \"names\" : [ \"is_mfa\" ]\n }, {\n \"field-id\" : 69,\n \"names\" : [ \"issuer\" ]\n } ]\n }, {\n \"field-id\" : 58,\n \"names\" : [ \"invoked_by\" ]\n }, {\n \"field-id\" : 59,\n \"names\" : [ \"idp\" ],\n \"fields\" : [ {\n \"field-id\" : 70,\n \"names\" : [ \"name\" ]\n } ]\n } ]\n}, {\n \"field-id\" : 8,\n \"names\" : [ \"http_request\" ],\n \"fields\" : [ {\n \"field-id\" : 71,\n \"names\" : [ \"user_agent\" ]\n } ]\n}, {\n \"field-id\" : 9,\n \"names\" : [ \"src_endpoint\" ],\n \"fields\" : [ {\n \"field-id\" : 72,\n \"names\" : [ \"uid\" ]\n }, {\n \"field-id\" : 73,\n \"names\" : [ \"ip\" ]\n }, {\n \"field-id\" : 74,\n \"names\" : [ \"domain\" ]\n } ]\n}, {\n \"field-id\" : 10,\n \"names\" : [ \"session\" ],\n \"fields\" : [ {\n \"field-id\" : 75,\n \"names\" : [ \"uid\" ]\n }, {\n \"field-id\" : 76,\n \"names\" : [ \"uid_alt\" ]\n }, {\n \"field-id\" : 77,\n \"names\" : [ \"credential_uid\" ]\n }, {\n \"field-id\" : 78,\n \"names\" : [ \"issuer\" ]\n } ]\n}, {\n \"field-id\" : 11,\n \"names\" : [ \"policy\" ],\n \"fields\" : [ {\n \"field-id\" : 79,\n \"names\" : [ \"uid\" ]\n } ]\n}, {\n \"field-id\" : 12,\n \"names\" : [ \"resources\" ],\n \"fields\" : [ {\n \"field-id\" : 80,\n \"names\" : [ \"element\" ],\n \"fields\" : [ {\n \"field-id\" : 81,\n \"names\" : [ \"uid\" ]\n }, {\n \"field-id\" : 82,\n \"names\" : [ \"owner\" ],\n \"fields\" : [ {\n \"field-id\" : 84,\n \"names\" : [ \"account\" ],\n \"fields\" : [ {\n \"field-id\" : 85,\n \"names\" : [ \"uid\" ]\n } ]\n } ]\n }, {\n \"field-id\" : 83,\n \"names\" : [ \"type\" ]\n } ]\n } ]\n}, {\n \"field-id\" : 13,\n \"names\" : [ \"class_name\" ]\n}, {\n \"field-id\" : 14,\n \"names\" : [ \"class_uid\" ]\n}, {\n \"field-id\" : 15,\n \"names\" : [ \"category_name\" ]\n}, {\n \"field-id\" : 16,\n \"names\" : [ \"category_uid\" ]\n}, {\n \"field-id\" : 17,\n \"names\" : [ \"severity_id\" ]\n}, {\n \"field-id\" : 18,\n \"names\" : [ \"severity\" ]\n}, {\n \"field-id\" : 19,\n \"names\" : [ \"user\" ],\n \"fields\" : [ {\n \"field-id\" : 86,\n \"names\" : [ \"uid_alt\" ]\n }, {\n \"field-id\" : 87,\n \"names\" : [ \"uid\" ]\n }, {\n \"field-id\" : 88,\n \"names\" : [ \"name\" ]\n } ]\n}, {\n \"field-id\" : 20,\n \"names\" : [ \"activity_name\" ]\n}, {\n \"field-id\" : 21,\n \"names\" : [ \"activity_id\" ]\n}, {\n \"field-id\" : 22,\n \"names\" : [ \"type_uid\" ]\n}, {\n \"field-id\" : 23,\n \"names\" : [ \"type_name\" ]\n}, {\n \"field-id\" : 24,\n \"names\" : [ \"status\" ]\n}, {\n \"field-id\" : 25,\n \"names\" : [ \"is_mfa\" ]\n}, {\n \"field-id\" : 26,\n \"names\" : [ \"unmapped\" ],\n \"fields\" : [ {\n \"field-id\" : 89,\n \"names\" : [ \"key\" ]\n }, {\n \"field-id\" : 90,\n \"names\" : [ \"value\" ]\n } ]\n}, {\n \"field-id\" : 27,\n \"names\" : [ \"accountid\" ]\n}, {\n \"field-id\" : 28,\n \"names\" : [ \"region\" ]\n}, {\n \"field-id\" : 29,\n \"names\" : [ \"asl_version\" ]\n}, {\n \"field-id\" : 30,\n \"names\" : [ \"observables\" ],\n \"fields\" : [ {\n \"field-id\" : 91,\n \"names\" : [ \"element\" ],\n \"fields\" : [ {\n \"field-id\" : 92,\n \"names\" : [ \"name\" ]\n }, {\n \"field-id\" : 93,\n \"names\" : [ \"value\" ]\n }, {\n \"field-id\" : 94,\n \"names\" : [ \"type\" ]\n }, {\n \"field-id\" : 95,\n \"names\" : [ \"type_id\" ]\n } ]\n } ]\n} ]", "region" : "ap-northeast-1", "table_type" : "ICEBERG" },
snapshotのサンプル
{ "sequence-number" : 111111, "snapshot-id" : 1111111111111111111, "parent-snapshot-id" : 1111111111111111110, "timestamp-ms" : 1738086192629, "summary" : { "operation" : "append", "added-data-files" : "7", "added-records" : "1719", "added-files-size" : "421902", "changed-partition-count" : "7", "total-records" : "780945471", "total-files-size" : "109247397623", "total-data-files" : "1139022", "total-delete-files" : "0", "total-position-deletes" : "0", "total-equality-deletes" : "0" }, "manifest-list" : "s3://aws-security-data-lake-ap-northeast-1-ランダム値/aws/CLOUD_TRAIL_MGMT/2.0/metadata/snap-1111111111111111111-1-00eb0b00-000a-0a00-0000-0000d0b0d000.avro", "schema-id" : 0 }