LayerX エンジニアブログ

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

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

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から眺めていこうと思います。

Apache Iceberg: The Definitive Guide の図1.7より

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_dtday に変換した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
  }