LayerX Fintech事業部(三井物産デジタル・アセットマネジメント(MDM)に出向)で、セキュリティ、インフラ、情シス、ヘルプデスク、ガバナンス・コンプライアンスエンジニアリングなどを担当している @ken5scal です。
当社はSIEMソリューション(DataDog SIEM)に加えて、最終的にはデータレイクハウスでのデータ分析にする意図でAmazon Security Lakeを活用しています。 これに関する過去記事は こちらからご参照ください。
Intro
現時点のSecurityLakeにおける課題の一つは、コストの最適化です。以前の記事でふれたように
Apache IcebergテーブルとなっているCloudTrailなどのAWS系ログでは metadata/ 配下だけで数テラバイトに達していました。
Security Lake では、LifeCycle ポリシーで削除や 低頻度アクセスのストレージクラスへの移動することで、この課題をある程度回避していますが、
実データとmetadata 配下を同じライフサイクルで管理するのは、直感的に望ましくないと感じています
では、果たして metadata/ 配下のファイルを消していいのか、それが問題です。
しかし、metadata/ 配下のファイルはフォーマットが一貫しているわけではなく、JSON ファイルと Avro ファイルが混在しています。
さらに、ファイルの命名規則も複数存在するため、それぞれがどのような意味を持ち、どのような動きをしているのかを正確に理解する必要があります。
こうした背景から、本ブログシリーズでは Security Lake の実装を踏まえつつ、Apache Iceberg の仕組みを体系的に学び、今後データ量を最適化する際に生じやすいトラブルを未然に防ぐことを目的としています。 今回は前回の「Catalog 編」に続き、SecurityLake と Apache Icebergの Metadata Layer のひも付き具合を見ていこうと思います。

Icebergメタデータ構造
前回は、AWS Lake Formation のテーブルから取得した 最新の Metadata File に、 有効なスナップショットのリストが含まれることを紹介しました。 以下はそのサンプルです。
{ "snapshots" : [ { "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-xxx値/aws/CLOUD_TRAIL_MGMT/2.0/metadata/snap-xxxxxxx.avro", "schema-id" : 0 } ], }
これはApache Icebergのアーキテクチャにおけるsnapshotに整合しています。
そこで、ここでは manifest-list というフィールドに焦点をあてます。
これは Apache Iceberg のアーキテクチャにおける snapshot と 実データファイルのパスや統計情報を記述したManifest File をつなぐ「目次」のような役割を担っています。
もし manifest-list が Apache Iceberg の Manifest File List に該当するのであれば、正しい道筋となっているはずです。

manifet-list S3オブジェクトの取得
では、manifest-list の S3 オブジェクトを実際にダウンロード・調査してみましょう。
実際の manifest-list オブジェクトは Avro 形式 で保存されています。
Avro は、スキーマ情報を含むバイナリ形式でデータをシリアライズし、データ交換を行う仕組みです。
Avroファイルのデコード
Avro ファイルはバイナリ形式のため、VSCode や Cursor などのテキストエディタでは直接閲覧できません。 そこで Python のコード を使ってデコードし、JSON に変換してみます。 ※Cursorによるもの
import avro.datafile import avro.io import json import pprint def convert_bytes(obj): """ Recursively convert bytes in a data structure to UTF-8 strings. """ if isinstance(obj, bytes): return obj.decode("utf-8") elif isinstance(obj, dict): return {convert_bytes(key): convert_bytes(value) for key, value in obj.items()} elif isinstance(obj, list): return [convert_bytes(element) for element in obj] else: return obj def read_avro_metadata(avro_path): """ Reads Avro metadata (key-value pairs) and data records from an Iceberg manifest file, then prints them as JSON. """ with open(avro_path, 'rb') as f: reader = avro.datafile.DataFileReader(f, avro.io.DatumReader()) # Extract Avro file metadata (dictionary of key-value pairs) metadata_dict = dict(reader.meta) # Optionally, read all records into a list records_list = [] for record in reader: records_list.append(record) for data in reader: pprint.pprint(data) # schema schema = reader.meta['avro.schema'] schema_json = json.loads(schema) pprint.pprint(schema_json) reader.close() # Combine everything into one JSON object output = { "metadata": metadata_dict, "records": records_list } # Print as JSON # Convert all bytes values to string so JSON can handle them. converted_output = convert_bytes(output) # Print as JSON with proper formatting print(json.dumps(converted_output, indent=2)) if __name__ == "__main__": import sys if len(sys.argv) != 2: print("Usage: python read_avro_meta.py <manifest_file_path>") sys.exit(1) manifest_file_path = sys.argv[1] read_avro_metadata(manifest_file_path)
manifest-list の観察
デコード結果がこちらです。
Avro の object file container 形式に則り、metadata フィールドに Manifest File の構造スキーマ が定義され、
それに応じたレコードが records リストに格納されていました。
また、この構造はApache Icebergの「Manifest Lists」の定義する構造にマッチしています。
{
"metadata": {
"avro.schema": "{\"type\":\"record\",\"name\":\"manifest_file\",\"fields\":[{\"name\":\"manifest_path\",\"type\":\"string\",\"doc\":\"Location URI with FS scheme\",\"field-id\":500},{\"name\":\"manifest_length\",\"type\":\"long\",\"doc\":\"Total file size in bytes\",\"field-id\":501},{\"name\":\"partition_spec_id\",\"type\":\"int\",\"doc\":\"Spec ID used to write\",\"field-id\":502},{\"name\":\"content\",\"type\":\"int\",\"doc\":\"Contents of the manifest: 0=data, 1=deletes\",\"field-id\":517},{\"name\":\"sequence_number\",\"type\":\"long\",\"doc\":\"Sequence number when the manifest was added\",\"field-id\":515},{\"name\":\"min_sequence_number\",\"type\":\"long\",\"doc\":\"Lowest sequence number in the manifest\",\"field-id\":516},{\"name\":\"added_snapshot_id\",\"type\":\"long\",\"doc\":\"Snapshot ID that added the manifest\",\"field-id\":503},{\"name\":\"added_data_files_count\",\"type\":\"int\",\"doc\":\"Added entry count\",\"field-id\":504},{\"name\":\"existing_data_files_count\",\"type\":\"int\",\"doc\":\"Existing entry count\",\"field-id\":505},{\"name\":\"deleted_data_files_count\",\"type\":\"int\",\"doc\":\"Deleted entry count\",\"field-id\":506},{\"name\":\"added_rows_count\",\"type\":\"long\",\"doc\":\"Added rows count\",\"field-id\":512},{\"name\":\"existing_rows_count\",\"type\":\"long\",\"doc\":\"Existing rows count\",\"field-id\":513},{\"name\":\"deleted_rows_count\",\"type\":\"long\",\"doc\":\"Deleted rows count\",\"field-id\":514},{\"name\":\"partitions\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"r508\",\"fields\":[{\"name\":\"contains_null\",\"type\":\"boolean\",\"doc\":\"True if any file has a null partition value\",\"field-id\":509},{\"name\":\"contains_nan\",\"type\":[\"null\",\"boolean\"],\"doc\":\"True if any file has a nan partition value\",\"default\":null,\"field-id\":518},{\"name\":\"lower_bound\",\"type\":[\"null\",\"bytes\"],\"doc\":\"Partition lower bound for all files\",\"default\":null,\"field-id\":510},{\"name\":\"upper_bound\",\"type\":[\"null\",\"bytes\"],\"doc\":\"Partition upper bound for all files\",\"default\":null,\"field-id\":511}]},\"element-id\":508}],\"doc\":\"Summary for each partition\",\"default\":null,\"field-id\":507}]}",
"avro.codec": "deflate",
"snapshot-id": "1000046944358792529",
"format-version": "2",
"sequence-number": "24466",
"iceberg.schema": "{\"type\":\"struct\",\"schema-id\":0,\"fields\":[{\"id\":500,\"name\":\"manifest_path\",\"required\":true,\"type\":\"string\",\"doc\":\"Location URI with FS scheme\"},{\"id\":501,\"name\":\"manifest_length\",\"required\":true,\"type\":\"long\",\"doc\":\"Total file size in bytes\"},{\"id\":502,\"name\":\"partition_spec_id\",\"required\":true,\"type\":\"int\",\"doc\":\"Spec ID used to write\"},{\"id\":517,\"name\":\"content\",\"required\":true,\"type\":\"int\",\"doc\":\"Contents of the manifest: 0=data, 1=deletes\"},{\"id\":515,\"name\":\"sequence_number\",\"required\":true,\"type\":\"long\",\"doc\":\"Sequence number when the manifest was added\"},{\"id\":516,\"name\":\"min_sequence_number\",\"required\":true,\"type\":\"long\",\"doc\":\"Lowest sequence number in the manifest\"},{\"id\":503,\"name\":\"added_snapshot_id\",\"required\":true,\"type\":\"long\",\"doc\":\"Snapshot ID that added the manifest\"},{\"id\":504,\"name\":\"added_data_files_count\",\"required\":true,\"type\":\"int\",\"doc\":\"Added entry count\"},{\"id\":505,\"name\":\"existing_data_files_count\",\"required\":true,\"type\":\"int\",\"doc\":\"Existing entry count\"},{\"id\":506,\"name\":\"deleted_data_files_count\",\"required\":true,\"type\":\"int\",\"doc\":\"Deleted entry count\"},{\"id\":512,\"name\":\"added_rows_count\",\"required\":true,\"type\":\"long\",\"doc\":\"Added rows count\"},{\"id\":513,\"name\":\"existing_rows_count\",\"required\":true,\"type\":\"long\",\"doc\":\"Existing rows count\"},{\"id\":514,\"name\":\"deleted_rows_count\",\"required\":true,\"type\":\"long\",\"doc\":\"Deleted rows count\"},{\"id\":507,\"name\":\"partitions\",\"required\":false,\"type\":{\"type\":\"list\",\"element-id\":508,\"element\":{\"type\":\"struct\",\"fields\":[{\"id\":509,\"name\":\"contains_null\",\"required\":true,\"type\":\"boolean\",\"doc\":\"True if any file has a null partition value\"},{\"id\":518,\"name\":\"contains_nan\",\"required\":false,\"type\":\"boolean\",\"doc\":\"True if any file has a nan partition value\"},{\"id\":510,\"name\":\"lower_bound\",\"required\":false,\"type\":\"binary\",\"doc\":\"Partition lower bound for all files\"},{\"id\":511,\"name\":\"upper_bound\",\"required\":false,\"type\":\"binary\",\"doc\":\"Partition upper bound for all files\"}]},\"element-required\":true},\"doc\":\"Summary for each partition\"}]}",
"parent-snapshot-id": "3894833620301386549"
},
"records": [
{
"manifest_path": "s3://aws-security-data-lake-ap-northeast-1-xxx/aws/CLOUD_TRAIL_MGMT/2.0/metadata/xxx-m0.avro",
"manifest_length": 13618,
"partition_spec_id": 0,
"content": 0,
"sequence_number": 24466,
"min_sequence_number": 24466,
"added_snapshot_id": 1000046944358792529,
"added_data_files_count": 1,
"existing_data_files_count": 0,
"deleted_data_files_count": 0,
"added_rows_count": 129,
"existing_rows_count": 0,
"deleted_rows_count": 0,
"partitions": [
{
"contains_null": false,
"contains_nan": false,
"lower_bound": "2_0",
"upper_bound": "2_0"
},
{
"contains_null": false,
"contains_nan": false,
"lower_bound": "ap-northeast-1",
"upper_bound": "ap-northeast-1"
},
{
"contains_null": false,
"contains_nan": false,
"lower_bound": "xxx",
"upper_bound": "xxx"
},
{
"contains_null": false,
"contains_nan": false,
"lower_bound": "mM\u0000\u0000",
"upper_bound": "mM\u0000\u0000"
}
]
},
もし records のなかにある manifest_path が Iceberg テーブルの参照する 実データファイル のパスや統計情報を含む Avro 形式のメタデータファイル(= Manifest File) を指しているなら、Icebergの仕様通り、「Manifest List → Manifest File → Data File」という構造が成り立つことになります。

manifest_path の取得と観察
S3オブジェクトであるavroを取得し、先のpythonコードでJSONにデコードしましょう
{
"metadata": {
"schema": "{\"type\":\"struct\",\"schema-id\":0,\"fields\":[{\"id\":1,\"name\":\"metadata\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":31,\"name\":\"product\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":36,\"name\":\"version\",\"required\":true,\"type\":\"string\"},{\"id\":37,\"name\":\"name\",\"required\":true,\"type\":\"string\"},{\"id\":38,\"name\":\"vendor_name\",\"required\":true,\"type\":\"string\"},{\"id\":39,\"name\":\"feature\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":40,\"name\":\"name\",\"required\":true,\"type\":\"string\"}]}}]}},{\"id\":32,\"name\":\"event_code\",\"required\":false,\"type\":\"string\"},{\"id\":33,\"name\":\"uid\",\"required\":false,\"type\":\"string\"},{\"id\":34,\"name\":\"profiles\",\"required\":false,\"type\":{\"type\":\"list\",\"element-id\":41,\"element\":\"string\",\"element-required\":true}},{\"id\":35,\"name\":\"version\",\"required\":true,\"type\":\"string\"}]}},{\"id\":2,\"name\":\"time\",\"required\":true,\"type\":\"long\"},{\"id\":3,\"name\":\"time_dt\",\"required\":false,\"type\":\"timestamp\"},{\"id\":4,\"name\":\"cloud\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":42,\"name\":\"region\",\"required\":false,\"type\":\"string\"},{\"id\":43,\"name\":\"provider\",\"required\":true,\"type\":\"string\"}]}},{\"id\":5,\"name\":\"api\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":44,\"name\":\"response\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":49,\"name\":\"error\",\"required\":false,\"type\":\"string\"},{\"id\":50,\"name\":\"message\",\"required\":false,\"type\":\"string\"},{\"id\":51,\"name\":\"data\",\"required\":false,\"type\":\"string\"}]}},{\"id\":45,\"name\":\"operation\",\"required\":false,\"type\":\"string\"},{\"id\":46,\"name\":\"version\",\"required\":false,\"type\":\"string\"},{\"id\":47,\"name\":\"service\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":52,\"name\":\"name\",\"required\":false,\"type\":\"string\"}]}},{\"id\":48,\"name\":\"request\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":53,\"name\":\"data\",\"required\":false,\"type\":\"string\"},{\"id\":54,\"name\":\"uid\",\"required\":false,\"type\":\"string\"}]}}]}},{\"id\":6,\"name\":\"dst_endpoint\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":55,\"name\":\"svc_name\",\"required\":false,\"type\":\"string\"}]}},{\"id\":7,\"name\":\"actor\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":56,\"name\":\"user\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":60,\"name\":\"type\",\"required\":false,\"type\":\"string\"},{\"id\":61,\"name\":\"name\",\"required\":false,\"type\":\"string\"},{\"id\":62,\"name\":\"uid_alt\",\"required\":false,\"type\":\"string\"},{\"id\":63,\"name\":\"uid\",\"required\":false,\"type\":\"string\"},{\"id\":64,\"name\":\"account\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":66,\"name\":\"uid\",\"required\":false,\"type\":\"string\"}]}},{\"id\":65,\"name\":\"credential_uid\",\"required\":false,\"type\":\"string\"}]}},{\"id\":57,\"name\":\"session\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":67,\"name\":\"created_time_dt\",\"required\":false,\"type\":\"timestamp\"},{\"id\":68,\"name\":\"is_mfa\",\"required\":false,\"type\":\"boolean\"},{\"id\":69,\"name\":\"issuer\",\"required\":false,\"type\":\"string\"}]}},{\"id\":58,\"name\":\"invoked_by\",\"required\":false,\"type\":\"string\"},{\"id\":59,\"name\":\"idp\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":70,\"name\":\"name\",\"required\":false,\"type\":\"string\"}]}}]}},{\"id\":8,\"name\":\"http_request\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":71,\"name\":\"user_agent\",\"required\":false,\"type\":\"string\"}]}},{\"id\":9,\"name\":\"src_endpoint\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":72,\"name\":\"uid\",\"required\":false,\"type\":\"string\"},{\"id\":73,\"name\":\"ip\",\"required\":false,\"type\":\"string\"},{\"id\":74,\"name\":\"domain\",\"required\":false,\"type\":\"string\"}]}},{\"id\":10,\"name\":\"session\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":75,\"name\":\"uid\",\"required\":false,\"type\":\"string\"},{\"id\":76,\"name\":\"uid_alt\",\"required\":false,\"type\":\"string\"},{\"id\":77,\"name\":\"credential_uid\",\"required\":false,\"type\":\"string\"},{\"id\":78,\"name\":\"issuer\",\"required\":false,\"type\":\"string\"}]}},{\"id\":11,\"name\":\"policy\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":79,\"name\":\"uid\",\"required\":false,\"type\":\"string\"}]}},{\"id\":12,\"name\":\"resources\",\"required\":false,\"type\":{\"type\":\"list\",\"element-id\":80,\"element\":{\"type\":\"struct\",\"fields\":[{\"id\":81,\"name\":\"uid\",\"required\":false,\"type\":\"string\"},{\"id\":82,\"name\":\"owner\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":84,\"name\":\"account\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":85,\"name\":\"uid\",\"required\":false,\"type\":\"string\"}]}}]}},{\"id\":83,\"name\":\"type\",\"required\":false,\"type\":\"string\"}]},\"element-required\":true}},{\"id\":13,\"name\":\"class_name\",\"required\":true,\"type\":\"string\"},{\"id\":14,\"name\":\"class_uid\",\"required\":true,\"type\":\"int\"},{\"id\":15,\"name\":\"category_name\",\"required\":true,\"type\":\"string\"},{\"id\":16,\"name\":\"category_uid\",\"required\":true,\"type\":\"int\"},{\"id\":17,\"name\":\"severity_id\",\"required\":true,\"type\":\"int\"},{\"id\":18,\"name\":\"severity\",\"required\":true,\"type\":\"string\"},{\"id\":19,\"name\":\"user\",\"required\":false,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":86,\"name\":\"uid_alt\",\"required\":false,\"type\":\"string\"},{\"id\":87,\"name\":\"uid\",\"required\":false,\"type\":\"string\"},{\"id\":88,\"name\":\"name\",\"required\":false,\"type\":\"string\"}]}},{\"id\":20,\"name\":\"activity_name\",\"required\":true,\"type\":\"string\"},{\"id\":21,\"name\":\"activity_id\",\"required\":true,\"type\":\"int\"},{\"id\":22,\"name\":\"type_uid\",\"required\":true,\"type\":\"long\"},{\"id\":23,\"name\":\"type_name\",\"required\":true,\"type\":\"string\"},{\"id\":24,\"name\":\"status\",\"required\":false,\"type\":\"string\"},{\"id\":25,\"name\":\"is_mfa\",\"required\":false,\"type\":\"boolean\"},{\"id\":26,\"name\":\"unmapped\",\"required\":false,\"type\":{\"type\":\"map\",\"key-id\":89,\"key\":\"string\",\"value-id\":90,\"value\":\"string\",\"value-required\":true}},{\"id\":27,\"name\":\"accountid\",\"required\":false,\"type\":\"string\"},{\"id\":28,\"name\":\"region\",\"required\":false,\"type\":\"string\"},{\"id\":29,\"name\":\"asl_version\",\"required\":false,\"type\":\"string\"},{\"id\":30,\"name\":\"observables\",\"required\":false,\"type\":{\"type\":\"list\",\"element-id\":91,\"element\":{\"type\":\"struct\",\"fields\":[{\"id\":92,\"name\":\"name\",\"required\":false,\"type\":\"string\"},{\"id\":93,\"name\":\"value\",\"required\":false,\"type\":\"string\"},{\"id\":94,\"name\":\"type\",\"required\":false,\"type\":\"string\"},{\"id\":95,\"name\":\"type_id\",\"required\":false,\"type\":\"int\"}]},\"element-required\":true}}]}",
"avro.schema": "{\"type\":\"record\",\"name\":\"manifest_entry\",\"fields\":[{\"name\":\"status\",\"type\":\"int\",\"field-id\":0},{\"name\":\"snapshot_id\",\"type\":[\"null\",\"long\"],\"default\":null,\"field-id\":1},{\"name\":\"sequence_number\",\"type\":[\"null\",\"long\"],\"default\":null,\"field-id\":3},{\"name\":\"file_sequence_number\",\"type\":[\"null\",\"long\"],\"default\":null,\"field-id\":4},{\"name\":\"data_file\",\"type\":{\"type\":\"record\",\"name\":\"r2\",\"fields\":[{\"name\":\"content\",\"type\":\"int\",\"doc\":\"Contents of the file: 0=data, 1=position deletes, 2=equality deletes\",\"field-id\":134},{\"name\":\"file_path\",\"type\":\"string\",\"doc\":\"Location URI with FS scheme\",\"field-id\":100},{\"name\":\"file_format\",\"type\":\"string\",\"doc\":\"File format name: avro, orc, or parquet\",\"field-id\":101},{\"name\":\"partition\",\"type\":{\"type\":\"record\",\"name\":\"r102\",\"fields\":[{\"name\":\"asl_version\",\"type\":[\"null\",\"string\"],\"default\":null,\"field-id\":1000},{\"name\":\"region\",\"type\":[\"null\",\"string\"],\"default\":null,\"field-id\":1001},{\"name\":\"accountid\",\"type\":[\"null\",\"string\"],\"default\":null,\"field-id\":1002},{\"name\":\"time_dt_day\",\"type\":[\"null\",{\"type\":\"int\",\"logicalType\":\"date\"}],\"default\":null,\"field-id\":1003}]},\"doc\":\"Partition data tuple, schema based on the partition spec\",\"field-id\":102},{\"name\":\"record_count\",\"type\":\"long\",\"doc\":\"Number of records in the file\",\"field-id\":103},{\"name\":\"file_size_in_bytes\",\"type\":\"long\",\"doc\":\"Total file size in bytes\",\"field-id\":104},{\"name\":\"column_sizes\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"k117_v118\",\"fields\":[{\"name\":\"key\",\"type\":\"int\",\"field-id\":117},{\"name\":\"value\",\"type\":\"long\",\"field-id\":118}]},\"logicalType\":\"map\"}],\"doc\":\"Map of column id to total size on disk\",\"default\":null,\"field-id\":108},{\"name\":\"value_counts\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"k119_v120\",\"fields\":[{\"name\":\"key\",\"type\":\"int\",\"field-id\":119},{\"name\":\"value\",\"type\":\"long\",\"field-id\":120}]},\"logicalType\":\"map\"}],\"doc\":\"Map of column id to total count, including null and NaN\",\"default\":null,\"field-id\":109},{\"name\":\"null_value_counts\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"k121_v122\",\"fields\":[{\"name\":\"key\",\"type\":\"int\",\"field-id\":121},{\"name\":\"value\",\"type\":\"long\",\"field-id\":122}]},\"logicalType\":\"map\"}],\"doc\":\"Map of column id to null value count\",\"default\":null,\"field-id\":110},{\"name\":\"nan_value_counts\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"k138_v139\",\"fields\":[{\"name\":\"key\",\"type\":\"int\",\"field-id\":138},{\"name\":\"value\",\"type\":\"long\",\"field-id\":139}]},\"logicalType\":\"map\"}],\"doc\":\"Map of column id to number of NaN values in the column\",\"default\":null,\"field-id\":137},{\"name\":\"lower_bounds\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"k126_v127\",\"fields\":[{\"name\":\"key\",\"type\":\"int\",\"field-id\":126},{\"name\":\"value\",\"type\":\"bytes\",\"field-id\":127}]},\"logicalType\":\"map\"}],\"doc\":\"Map of column id to lower bound\",\"default\":null,\"field-id\":125},{\"name\":\"upper_bounds\",\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"k129_v130\",\"fields\":[{\"name\":\"key\",\"type\":\"int\",\"field-id\":129},{\"name\":\"value\",\"type\":\"bytes\",\"field-id\":130}]},\"logicalType\":\"map\"}],\"doc\":\"Map of column id to upper bound\",\"default\":null,\"field-id\":128},{\"name\":\"key_metadata\",\"type\":[\"null\",\"bytes\"],\"doc\":\"Encryption key metadata blob\",\"default\":null,\"field-id\":131},{\"name\":\"split_offsets\",\"type\":[\"null\",{\"type\":\"array\",\"items\":\"long\",\"element-id\":133}],\"doc\":\"Splittable offsets\",\"default\":null,\"field-id\":132},{\"name\":\"equality_ids\",\"type\":[\"null\",{\"type\":\"array\",\"items\":\"int\",\"element-id\":136}],\"doc\":\"Equality comparison field IDs\",\"default\":null,\"field-id\":135},{\"name\":\"sort_order_id\",\"type\":[\"null\",\"int\"],\"doc\":\"Sort order ID\",\"default\":null,\"field-id\":140}]},\"field-id\":2}]}",
"avro.codec": "deflate",
"format-version": "2",
"partition-spec-id": "0",
"iceberg.schema": "{\"type\":\"struct\",\"schema-id\":0,\"fields\":[{\"id\":0,\"name\":\"status\",\"required\":true,\"type\":\"int\"},{\"id\":1,\"name\":\"snapshot_id\",\"required\":false,\"type\":\"long\"},{\"id\":3,\"name\":\"sequence_number\",\"required\":false,\"type\":\"long\"},{\"id\":4,\"name\":\"file_sequence_number\",\"required\":false,\"type\":\"long\"},{\"id\":2,\"name\":\"data_file\",\"required\":true,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":134,\"name\":\"content\",\"required\":true,\"type\":\"int\",\"doc\":\"Contents of the file: 0=data, 1=position deletes, 2=equality deletes\"},{\"id\":100,\"name\":\"file_path\",\"required\":true,\"type\":\"string\",\"doc\":\"Location URI with FS scheme\"},{\"id\":101,\"name\":\"file_format\",\"required\":true,\"type\":\"string\",\"doc\":\"File format name: avro, orc, or parquet\"},{\"id\":102,\"name\":\"partition\",\"required\":true,\"type\":{\"type\":\"struct\",\"fields\":[{\"id\":1000,\"name\":\"asl_version\",\"required\":false,\"type\":\"string\"},{\"id\":1001,\"name\":\"region\",\"required\":false,\"type\":\"string\"},{\"id\":1002,\"name\":\"accountid\",\"required\":false,\"type\":\"string\"},{\"id\":1003,\"name\":\"time_dt_day\",\"required\":false,\"type\":\"date\"}]},\"doc\":\"Partition data tuple, schema based on the partition spec\"},{\"id\":103,\"name\":\"record_count\",\"required\":true,\"type\":\"long\",\"doc\":\"Number of records in the file\"},{\"id\":104,\"name\":\"file_size_in_bytes\",\"required\":true,\"type\":\"long\",\"doc\":\"Total file size in bytes\"},{\"id\":108,\"name\":\"column_sizes\",\"required\":false,\"type\":{\"type\":\"map\",\"key-id\":117,\"key\":\"int\",\"value-id\":118,\"value\":\"long\",\"value-required\":true},\"doc\":\"Map of column id to total size on disk\"},{\"id\":109,\"name\":\"value_counts\",\"required\":false,\"type\":{\"type\":\"map\",\"key-id\":119,\"key\":\"int\",\"value-id\":120,\"value\":\"long\",\"value-required\":true},\"doc\":\"Map of column id to total count, including null and NaN\"},{\"id\":110,\"name\":\"null_value_counts\",\"required\":false,\"type\":{\"type\":\"map\",\"key-id\":121,\"key\":\"int\",\"value-id\":122,\"value\":\"long\",\"value-required\":true},\"doc\":\"Map of column id to null value count\"},{\"id\":137,\"name\":\"nan_value_counts\",\"required\":false,\"type\":{\"type\":\"map\",\"key-id\":138,\"key\":\"int\",\"value-id\":139,\"value\":\"long\",\"value-required\":true},\"doc\":\"Map of column id to number of NaN values in the column\"},{\"id\":125,\"name\":\"lower_bounds\",\"required\":false,\"type\":{\"type\":\"map\",\"key-id\":126,\"key\":\"int\",\"value-id\":127,\"value\":\"binary\",\"value-required\":true},\"doc\":\"Map of column id to lower bound\"},{\"id\":128,\"name\":\"upper_bounds\",\"required\":false,\"type\":{\"type\":\"map\",\"key-id\":129,\"key\":\"int\",\"value-id\":130,\"value\":\"binary\",\"value-required\":true},\"doc\":\"Map of column id to upper bound\"},{\"id\":131,\"name\":\"key_metadata\",\"required\":false,\"type\":\"binary\",\"doc\":\"Encryption key metadata blob\"},{\"id\":132,\"name\":\"split_offsets\",\"required\":false,\"type\":{\"type\":\"list\",\"element-id\":133,\"element\":\"long\",\"element-required\":true},\"doc\":\"Splittable offsets\"},{\"id\":135,\"name\":\"equality_ids\",\"required\":false,\"type\":{\"type\":\"list\",\"element-id\":136,\"element\":\"int\",\"element-required\":true},\"doc\":\"Equality comparison field IDs\"},{\"id\":140,\"name\":\"sort_order_id\",\"required\":false,\"type\":\"int\",\"doc\":\"Sort order ID\"}]}}]}",
"partition-spec": "[{\"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}]",
"content": "data"
},
"records": [
{
"status": 1,
"snapshot_id": 1000046944358792529,
"sequence_number": null,
"file_sequence_number": null,
"data_file": {
"content": 0,
"file_path": "s3://aws-security-data-lake-ap-northeast-1-xxx/aws/CLOUD_TRAIL_MGMT/2.0/region=ap-northeast-1/accountId=083801292469/eventDay=20240408/xxx.gz.parquet",
"file_format": "PARQUET",
"partition": {
"asl_version": "2_0",
"region": "ap-northeast-1",
"accountid": "xxx",
"time_dt_day": 19821
},
"record_count": 129,
"file_size_in_bytes": 53190,
"column_sizes": null,
"value_counts": null,
"null_value_counts": null,
"nan_value_counts": null,
"lower_bounds": null,
"upper_bounds": null,
"key_metadata": null,
"split_offsets": null,
"equality_ids": null,
"sort_order_id": 0
}
}
]
}
果たして、上記のスキーマはApache IcebergのManifestファイルのスキーマと合致しました。
今回わかったこと & 次回予告
これによりSecurity Lakeの metadata/配下には、Apache IcebergのMetadata Layer層にある各種ファイルが特定のフォーマットで保存されていることがわかりました。
| ファイル種別 | 役割 | ファイル名 | |
|---|---|---|---|
| Metadata File | スナップショットの一覧などを含む | ランダムな値.metadata.json |
|
| Manifest List | スナップショット単位で複数の Manifest File を追跡 | snap-ランダムな値.avro |
|
| Manifest File | 実データファイルの詳細情報・統計情報を列挙 | ランダムな値-m0.avro |
次回以降は、Athenaなどのクエリに応じて、これらのファイルがどのように読み込まれ、また管理・削除・圧縮 に伴いどのような影響がでるかを、Icebergの仕様を読み解きながら調査していく予定です。ぜひお楽しみに!
Obligatory Advertizement
このように、LayerX Fitench事業部の出向先であるMDM コーポレートシステム部では、データエンジニアリングに日々、四苦八苦しています。同じような課題に挑戦したい方や、経験を活かしたいとお考えの方は、ぜひお話を聞かせてください。カジュアル面談の申し込みをお待ちしております!