LayerX エンジニアブログ

LayerX の エンジニアブログです。

Amazon SecurityLakeからみるApache Iceberg - アーキテクチャ章Metadata Layer編

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 のひも付き具合を見ていこうと思います。

データが53.7Gに対し、metadataが8.7Tもある図

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 に該当するのであれば、正しい道筋となっているはずです。

Apache Iceberg: The Definitive Guide の図1.7より

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」という構造が成り立つことになります。

Apache Iceberg: The Definitive Guide の図1.7より

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 コーポレートシステム部では、データエンジニアリングに日々、四苦八苦しています。同じような課題に挑戦したい方や、経験を活かしたいとお考えの方は、ぜひお話を聞かせてください。カジュアル面談の申し込みをお待ちしております!

jobs.layerx.co.jp