Snowflake 管理 Iceberg テーブルを Amazon Athena の各エンジンから読み取り・書き込みしてみる

Snowflake 管理 Iceberg テーブルを Amazon Athena の各エンジンから読み取り・書き込みしてみる

Snowflake Summit 2026で発表された、外部エンジンからの書き込み機能をAmazon Athenaから検証してみました。Athena Spark・Athena SQL・複数エンジン世代での動作を確認し、実装上の注意点もまとめています。
2026.07.05

こんにちは、データ事業本部のキタガワです。

Snowflake Summit 2026 で、Snowflake 管理の Apache Iceberg テーブルに対する外部エンジンからの書き込みの一般提供(GA)が発表されました。これまで Snowflake 管理 Iceberg テーブルは外部エンジンからは読めましたが、書き込みはサポートされていませんでした。これからは Spark などの外部エンジンから同じテーブルに直接コミットできます。

今回はこの機能を Amazon Athena から、Athena for Apache Spark (以下 Athena Spark) と Athena SQL でそれぞれどのような操作が可能か検証してみました。また Athena for Apache Spark には PySpark engine version 3 と Apache Spark version 3.5 の2つのエンジンがあるので、そのどちらも検証してみました。先に結論をまとめます。

エンジン Iceberg バージョン 読み取り 書き込み
PySpark engine version 3 v2 成功 成功(INSERT/UPDATE/DELETE)
PySpark engine version 3 v3 失敗(クライアントが v3 未対応) ―(読み取り不可のため未検証)
Apache Spark version 3.5 v2 成功 成功(INSERT/UPDATE/DELETE)
Apache Spark version 3.5 v3 成功 部分成功(INSERT/DELETE のみ。UPDATE は row lineage 未対応で失敗)
Athena SQL v2 成功 不可
Athena SQL v3 不可(フォーマット自体が非対応) 不可(フォーマット自体が非対応)

Athena Spark からは Iceberg v2 テーブルへの INSERT / UPDATE / DELETE がすべて成功し、Snowflake と Athena の双方向書き込みが単一のスナップショット履歴に共存することを確認できました。Iceberg v3 テーブルは PySpark engine version 3 が対応しておらず読み取りが不可でしたが、Apache Spark version 3.5 では読み取りが成功し、部分的に書き込みも成功しました。Athena SQL は Iceberg v2 テーブルの読み取りのみサポートされていました。

機能の背景

外部エンジン連携は段階的にリリースされてきました。

日付 内容
2026-02-06 外部エンジンからの読み取り GA
2026-03-16 外部エンジンからの書き込み Preview
2026-05-07 Iceberg v3 サポート GA
2026-05-26 外部エンジンからの書き込み GA
2026-06-02 Snowflake Summit 26 で発表

接続の入口は Snowflake Horizon Catalog が公開する Iceberg REST Catalog API(以下 IRC)です。Apache Polaris がベースになっており、エンドポイントは各アカウントに最初から生えています。

https://<アカウント識別子>.snowflakecomputing.com/polaris/api/catalog

https://docs.snowflake.com/en/user-guide/tables-iceberg-access-using-external-query-engine-snowflake-horizon

https://docs.snowflake.com/en/release-notes/2026/other/2026-03-16-tables-iceberg-query-using-external-query-engine-snowflake-horizon-writes-feature

検証シナリオ

Snowflake 公式のサポートエンジンは Apache Spark、DuckDB、Apache Flink、Trino、Dremio、PyIceberg です。Athena は明記されていませんでしたが、Athena には2つのエンジンがあり、事情が異なります。

  • Athena Spark: Apache Spark + Iceberg ランタイムなので、公式サポートの Apache Spark が該当します。Snowflake の IRC に直接接続することができます。
  • Athena SQL: Trino ベースですがカタログは Glue 経由に限定されており、IRC に直接接続できません。接続するには Glue Data Catalog のカタログフェデレーションを挟む必要があります

検証は3段構成にしました。まず公式サポートパスである 検証1: Athena Spark(PySpark engine version 3)で Snowflake 側設定の正しさと GA 機能の動作を証明し、続いて 検証2: Athena Spark(Apache Spark version 3.5)でエンジン世代による差分を確認、最後に 検証3: Athena SQL + Glue カタログフェデレーションを試します。

S3 へのアクセスには credential vending を使います。エンジンはリクエストヘッダー X-Iceberg-Access-Delegation: vended-credentials を付けるだけで、Snowflake がスコープを絞った一時認証情報を払い出してくれます。今回の検証ではあえて Athena Spark の実行ロールにデータバケットへの S3 権限を付与せず、書き込みが成功すること自体を credential vending が機能している証明としました。

検証用テーブルには written_by 列を用意し、各エンジンが自分の名前を書き込むことでクロスエンジン書き込みを可視化します。

セットアップ

手順の全量とスクリプトはリポジトリに置いてあるので、ここでは要点だけ紹介します。

https://github.com/cm-kitagawa-zempei/snowflake-managed-iceberg-tables-write-from-athena

1. S3 バケットと External Volume

External Volume の作成では、Snowflake 側の IAM ユーザーに自アカウントの IAM ロールを AssumeRole させる信頼関係を結びます。

https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-external-volume-s3

手順は以下の通りです。

  1. 仮の信頼ポリシーで IAM ロールを作成
  2. CREATE EXTERNAL VOLUMESTORAGE_AWS_EXTERNAL_ID を固定値で指定しておくと後の手順が楽です)
  3. DESC EXTERNAL VOLUMESTORAGE_AWS_IAM_USER_ARN を確認
  4. 信頼ポリシーを本物に更新
CREATE OR REPLACE EXTERNAL VOLUME ICEBERG_EXT_WRITE_VOL
  STORAGE_LOCATIONS = (
    (
      NAME = 'aws-s3-tokyo'
      STORAGE_PROVIDER = 'S3'
      STORAGE_BASE_URL = 's3://cm-example-iceberg-ext-write/iceberg/'
      STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123456789012:role/cm-example-extvol-role'
      STORAGE_AWS_EXTERNAL_ID = 'cm_example_iceberg_ext_write'
    )
  )
  ALLOW_WRITES = TRUE;

DESC EXTERNAL VOLUME ICEBERG_EXT_WRITE_VOL;

疎通確認は SYSTEM$VERIFY_EXTERNAL_VOLUME で行います。

SELECT SYSTEM$VERIFY_EXTERNAL_VOLUME('ICEBERG_EXT_WRITE_VOL');
-- {"success":true,"writeResult":"PASSED","readResult":"PASSED",...}

2. Iceberg テーブル

v2 と v3 の両方を用意しました。外部書き込み GA は v2 / v3 どちらの Snowflake 管理テーブルも対象です。

-- ---- Iceberg v2 テーブル ----
CREATE OR REPLACE ICEBERG TABLE ORDERS_V2 (
  order_id   INT,
  product    STRING,
  quantity   INT,
  unit_price DECIMAL(10,2),
  ordered_at TIMESTAMP_NTZ(6),
  written_by STRING             -- 書き込み元エンジンを記録
)
  CATALOG = 'SNOWFLAKE'
  EXTERNAL_VOLUME = 'ICEBERG_EXT_WRITE_VOL'
  BASE_LOCATION = 'orders_v2';

INSERT INTO ORDERS_V2 VALUES
  (1, 'keyboard', 2,  4500.00, '2026-07-01 09:00:00'::TIMESTAMP_NTZ, 'snowflake'),
  (2, 'mouse',    5,  1980.00, '2026-07-01 10:30:00'::TIMESTAMP_NTZ, 'snowflake'),
  (3, 'monitor',  1, 32000.00, '2026-07-01 14:15:00'::TIMESTAMP_NTZ, 'snowflake');

-- ---- Iceberg v3 テーブル ----
CREATE OR REPLACE ICEBERG TABLE ORDERS_V3 (
  order_id   INT,
  product    STRING,
  quantity   INT,
  unit_price DECIMAL(10,2),
  ordered_at TIMESTAMP_NTZ(6),
  written_by STRING             -- 書き込み元エンジンを記録
)
  CATALOG = 'SNOWFLAKE'
  EXTERNAL_VOLUME = 'ICEBERG_EXT_WRITE_VOL'
  BASE_LOCATION = 'orders_v3'
  ICEBERG_VERSION = 3;

INSERT INTO ORDERS_V3 VALUES
  (1, 'desk',  1, 25000.00, '2026-07-01 09:00:00'::TIMESTAMP_NTZ, 'snowflake'),
  (2, 'chair', 2, 18000.00, '2026-07-01 11:45:00'::TIMESTAMP_NTZ, 'snowflake');

3. サービスユーザーと PAT

外部エンジン用にサービスユーザー と Programmatic Access Token(PAT)を用意します。書き込みには対象テーブルの SELECT / INSERT / UPDATE / DELETE / TRUNCATE すべてと、External Volume の USAGE が必要です。

サービスユーザーの PAT は原則ネットワークポリシー必須ですが、Horizon IRC はユーザーレベルのネットワークポリシーを非サポートです。そのため検証では認証ポリシーで要件を緩和しました(本番では要検討ポイントです)。

CREATE AUTHENTICATION POLICY HORIZON_PAT_AUTH_POLICY
  PAT_POLICY = (NETWORK_POLICY_EVALUATION = ENFORCED_NOT_REQUIRED);

ALTER USER HORIZON_ATHENA_SVC SET AUTHENTICATION POLICY HORIZON_PAT_AUTH_POLICY;

ALTER USER HORIZON_ATHENA_SVC ADD PROGRAMMATIC ACCESS TOKEN ATHENA_VERIFICATION_PAT
  ROLE_RESTRICTION = 'HORIZON_EXT_RW_ROLE'
  DAYS_TO_EXPIRY = 7;

PAT はそのまま、または OAuth トークンエンドポイントでアクセストークンに交換して使えます。

curl -X POST "https://<アカウント識別子>.snowflakecomputing.com/polaris/api/catalog/v1/oauth/tokens" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'scope=session:role:HORIZON_EXT_RW_ROLE' \
  --data-urlencode 'client_secret=<PAT>'

検証1: Athena Spark(PySpark engine version 3)

接続設定

Athena Spark のセッション起動時に、Snowflake の IRC を指す Iceberg REST カタログを Spark プロパティで定義します。最終的に動いた設定がこちらです。

{
  "spark.sql.extensions": "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions",
  "spark.sql.catalog.horizon": "org.apache.iceberg.spark.SparkCatalog",
  "spark.sql.catalog.horizon.type": "rest",
  "spark.sql.catalog.horizon.uri": "https://<アカウント識別子>.snowflakecomputing.com/polaris/api/catalog",
  "spark.sql.catalog.horizon.warehouse": "ICEBERG_EXT_WRITE_DB",
  "spark.sql.catalog.horizon.credential": "<PAT>",
  "spark.sql.catalog.horizon.scope": "session:role:HORIZON_EXT_RW_ROLE",
  "spark.sql.catalog.horizon.header.X-Iceberg-Access-Delegation": "vended-credentials",
  "spark.sql.catalog.horizon.io-impl": "org.apache.iceberg.aws.s3.S3FileIO",
  "spark.sql.iceberg.handle-timestamp-without-timezone": "true",
  "spark.sql.iceberg.vectorization.enabled": "false",
  "spark.sql.shuffle.partitions": "1"
}

warehouse には Snowflake のデータベース名を指定します。以降 Spark からは horizon.<スキーマ>.<テーブル> の形式でアクセスできます。

なお検証はマネジメントコンソールのノートブックではなく、aws athena start-session / start-calculation-execution を使って CLI から再現可能な形で実行しました。

設定の末尾3行は最初から分かっていたものではなく、検証で問題を踏むたびにひとつずつ足していったものです。

動作検証

検証スクリプト(verify_horizon_write.py)では、以下の流れで動作を確認します。

  1. 環境情報の出力(Spark バージョン、Iceberg バージョン)
  2. SHOW NAMESPACES IN horizon で Snowflake 側のスキーマが見えることを確認
  3. v2 テーブルの初期データ(written_by:snowflake)を読み取り
  4. v2 テーブルへの INSERT
  5. v2 テーブルの UPDATE
  6. v2 テーブルからの DELETE
  7. 書き込み後の v2 テーブルを再読み取りし、written_by 別の件数を確認
  8. v2 テーブルのスナップショット履歴(.snapshots メタデータテーブル)を参照し、外部コミットを確認
  9. v3 テーブルの読み取り
  10. v3 テーブルへの INSERT

Iceberg v3 テーブルは 9 の読み取り時点で Cannot read unsupported version 3 というエラーが発生しました。念のため INSERT の検証も行いましたが結果は同じだったため UPDATE / DELETE の検証は行っていません。

実行ログ抜粋
============================================================
### 0. 環境情報
============================================================
Spark version: 3.2.1-amzn-0
Iceberg version: 1.4.0-SNAPSHOT

============================================================
### 1. ネームスペース一覧(Snowflake スキーマが見えること)
============================================================
+---------+
|namespace|
+---------+
|ANALYTICS|
+---------+

============================================================
### 2. v2 テーブルの読み取り(初期データ = written_by:snowflake)
============================================================
+--------+--------+--------+----------+-------------------+----------+
|ORDER_ID|PRODUCT |QUANTITY|UNIT_PRICE|ORDERED_AT         |WRITTEN_BY|
+--------+--------+--------+----------+-------------------+----------+
|1       |keyboard|2       |4500.00   |2026-07-01 09:00:00|snowflake |
|2       |mouse   |5       |1980.00   |2026-07-01 10:30:00|snowflake |
|3       |monitor |1       |32000.00  |2026-07-01 14:15:00|snowflake |
+--------+--------+--------+----------+-------------------+----------+


============================================================
### 3. v2 テーブルへの INSERT(外部エンジン書き込みの本命検証)
============================================================
INSERT 成功

============================================================
### 4. v2 テーブルの UPDATE
============================================================
UPDATE 成功

============================================================
### 5. v2 テーブルの DELETE(1 行のみ削除し、201 は Snowflake 側確認用に残す)
============================================================
DELETE 成功

============================================================
### 6. v2 テーブルの読み取り(書き込み後)
============================================================
+--------+--------+--------+----------+-------------------+------------+
|ORDER_ID|PRODUCT |QUANTITY|UNIT_PRICE|ORDERED_AT         |WRITTEN_BY  |
+--------+--------+--------+----------+-------------------+------------+
|1       |keyboard|2       |4500.00   |2026-07-01 09:00:00|snowflake   |
|2       |mouse   |5       |1980.00   |2026-07-01 10:30:00|snowflake   |
|3       |monitor |1       |32000.00  |2026-07-01 14:15:00|snowflake   |
|201     |ssd-1tb |2       |12800.00  |2026-07-02 10:00:00|athena-spark|
+--------+--------+--------+----------+-------------------+------------+

+------------+---------+
|written_by  |row_count|
+------------+---------+
|athena-spark|1        |
|snowflake   |3        |
+------------+---------+


============================================================
### 7. v2 テーブルのスナップショット履歴(外部コミットの確認)
============================================================
+-----------------------+-------------------+---------+
|committed_at           |snapshot_id        |operation|
+-----------------------+-------------------+---------+
|2026-07-04 12:47:05.39 |7325365669594828787|append   |
|2026-07-04 12:49:41.525|3821238997686183694|append   |
|2026-07-04 12:49:46.974|1167080387288909088|overwrite|
|2026-07-04 12:49:50.239|8203787599929521889|delete   |
+-----------------------+-------------------+---------+


============================================================
### 8. v3 テーブルの読み取り(Athena Spark 側の v3 対応検証)
============================================================
v3 読み取り失敗: An error occurred while calling o44.sql.
: org.apache.iceberg.exceptions.RESTException: Received a success response code of 200, but failed to parse response body into LoadTableResponse
        ...(スタックトレース 97 行省略)...
Caused by: org.apache.iceberg.shaded.com.fasterxml.jackson.databind.JsonMappingException: Cannot read unsupported version 3 (through reference chain: org.apache.iceberg.rest.responses.LoadTableResponse["metadata"])
        ...(スタックトレース 11 行省略)...
Caused by: java.lang.IllegalArgumentException: Cannot read unsupported version 3
        ...(スタックトレース 8 行省略)...


============================================================
### 9. v3 テーブルへの INSERT(v3 外部書き込みの可否検証)
============================================================
v3 INSERT 失敗: An error occurred while calling o44.sql.
: org.apache.iceberg.exceptions.RESTException: Received a success response code of 200, but failed to parse response body into LoadTableResponse
        ...(スタックトレース 79 行省略)...
Caused by: org.apache.iceberg.shaded.com.fasterxml.jackson.databind.JsonMappingException: Cannot read unsupported version 3 (through reference chain: org.apache.iceberg.rest.responses.LoadTableResponse["metadata"])
        ...(スタックトレース 11 行省略)...
Caused by: java.lang.IllegalArgumentException: Cannot read unsupported version 3
        ...(スタックトレース 8 行省略)...

セッション情報を出力したところ、PySpark engine version 3 のランタイムは Spark 3.2.1 + Iceberg 1.4.0(2026-07 時点)でした。このランタイムの古さに起因して、3つの問題を順に踏みました。以下に問題と解決した方法を残しておきます。

NTZ タイムスタンプが読めない

最初の SELECT で失敗しました。

pyspark.sql.utils.IllegalArgumentException: Cannot handle timestamp without timezone fields in Spark.
Spark does not natively support this type but if you would like to handle all timestamps
as timestamp with timezone set 'spark.sql.iceberg.handle-timestamp-without-timezone' to true.

Spark 3.2 には TIMESTAMP_NTZ 型が存在しない(3.4 で導入)ため、Iceberg の timestamp(タイムゾーンなし)列を扱えません。エラーメッセージの指示どおり spark.sql.iceberg.handle-timestamp-without-timezone = true を設定して解決です。格納値は変わらず、Spark 上での解釈だけが変わります。

Spark 3.4 で TIMESTAMP_NTZ 型が導入されて以降の Iceberg 統合では、この設定自体が不要になっています。

https://github.com/apache/iceberg/blob/apache-iceberg-1.4.0/spark/v3.2/spark/src/main/java/org/apache/iceberg/spark/SparkSQLProperties.java

https://spark.apache.org/releases/spark-release-3-4-0.html

Snowflake が書いた Parquet が読めない

データファイルの読み取りで失敗しました。

java.lang.UnsupportedOperationException: Cannot support vectorized reads for column [ORDER_ID]
optional int32 ORDER_ID (INTEGER(32,true)) = 1 with encoding DELTA_BINARY_PACKED.
Disable vectorized reads to read this table/file

Snowflake が書き出す Parquet ファイルは DELTA_BINARY_PACKED エンコーディング(Parquet V2 系)を含みますが、Iceberg 1.4 のベクトル化リーダー(Arrow)が未対応です。spark.sql.iceberg.vectorization.enabled = false で行ベース読み取りにフォールバックさせて解決しました。性能は落ちますが検証には影響ありません。この設定は Iceberg 公式ドキュメントの Spark Configuration(Spark SQL Options)に記載があります。

https://iceberg.apache.org/docs/latest/spark-configuration/#spark-sql-options

https://qiita.com/manabian/items/fc89b81b327545da9f69

UPDATE が s3:DeleteObject の 403 で失敗する

INSERT は成功したのに、UPDATE で失敗しました。

software.amazon.awssdk.services.s3.model.S3Exception:
User: arn:aws:sts::123456789012:assumed-role/cm-example-extvol-role/snowflake
is not authorized to perform: s3:DeleteObject on resource: ".../orders_v2.XXXX/data/00002-....parquet"
because no session policy allows the s3:DeleteObject action

エラー主体が assumed-role/.../snowflake になっていることから credential vending 自体は機能していて、Snowflake が External Volume 用ロールを引き受けた一時認証情報が使われています。問題は末尾の no session policy allows で、IAM ロール側には DeleteObject を付与済みなのに、Snowflake がベンディング時に適用するセッションポリシーが Put/Get/List のみで Delete を含んでいないのです。

一方 Iceberg の Spark ライターは、0行のまま閉じた空データファイルの掃除やタスク中断時のクリーンアップで DeleteObject を発行することがあります。コミット済みデータファイルの物理削除は Snowflake 側メンテナンスの役割なので意図的なスコープ制限だと思われますが、ライターの内部動作と衝突するケースがある、というわけです。この挙動はドキュメントに見当たりませんでした。

対策は2つ考えられます。

  1. 空ファイルを発生させない: copy-on-write の書き出しタスクは書き換え対象のデータファイル単位で分配されるため、対象ファイル数が書き出しタスク数より少ないと0行のタスク、つまり空ファイルが生じます。行数は関係なく、Snowflake が1ファイルに書き出した20,000行のテーブルでも同じ 403 になることを実測で確認しました。spark.sql.shuffle.partitions = 1 で書き込みタスクを1つに集約すれば、DeleteObject の発行契機そのものを回避できます。今回はこれで解決しました
  2. バケットポリシーで直接許可する: リソースベースポリシーでセッションプリンシパル(arn:aws:sts::...:assumed-role/<ロール名>/snowflake)に s3:DeleteObject を直接付与する方法です。セッションプリンシパルへの直接付与はセッションポリシーの制限を受けないという IAM の仕様を利用します。ただし Snowflake が意図的に絞っているスコープを緩める操作なので、利用は慎重に判断してください

対策1の前提となる「書き出しタスクは書き換え対象ファイル単位で分配される」という挙動は、Iceberg 1.4.0 のソースで確認できます。copy-on-write の UPDATE / DELETE の分散モードはデフォルトで hash(SparkWriteConf)、hash 分散のクラスタリングキーは行の出自データファイルを表す _file メタデータ列(SparkDistributionAndOrderingUtil)です。

https://github.com/apache/iceberg/blob/apache-iceberg-1.4.0/spark/v3.2/spark/src/main/java/org/apache/iceberg/spark/SparkWriteConf.java

https://github.com/apache/iceberg/blob/apache-iceberg-1.4.0/spark/v3.2/spark/src/main/java/org/apache/iceberg/spark/SparkDistributionAndOrderingUtil.java

v2 テーブル

修正後の実行で、読み取り → INSERT → UPDATE → DELETE がすべて成功しました。スナップショット履歴がこちらです。

+-----------------------+-------------------+---------+
|committed_at           |snapshot_id        |operation|
+-----------------------+-------------------+---------+
|2026-07-04 12:47:05.39 |7325365669594828787|append   |  <- Snowflake: 初期データ INSERT
|2026-07-04 12:49:41.525|3821238997686183694|append   |  <- Athena Spark: INSERT
|2026-07-04 12:49:46.974|1167080387288909088|overwrite|  <- Athena Spark: UPDATE
|2026-07-04 12:49:50.239|8203787599929521889|delete   |  <- Athena Spark: DELETE
+-----------------------+-------------------+---------+

Snowflake が作った初期データ(先頭の append)に続いて、Athena Spark のコミットが同じ Iceberg 履歴に積まれているのが分かります。
Snowflake 側からも外部エンジンの書き込みが即座に見えます。

SELECT written_by, COUNT(*) AS row_count FROM ORDERS_V2 GROUP BY written_by;
-- WRITTEN_BY    ROW_COUNT
-- athena-spark  1
-- snowflake     3

v3 テーブル

v3 テーブルは読み取りの時点で失敗しました。

Caused by: java.lang.IllegalArgumentException: Cannot read unsupported version 3
	at org.apache.iceberg.TableMetadataParser.fromJson(...)

Snowflake(サーバー側)は HTTP 200 でメタデータを返しており、Athena Spark の Iceberg 1.4.0 クライアントがパース段階で拒否しています。つまり Snowflake 側の制限ではなく、エンジン側クライアントの v3 未対応(v3 対応は Iceberg 1.8 以降)が原因です。

検証2: Athena Spark(Apache Spark version 3.5)

Apache Spark version 3.5 エンジンは EMR 7.12 ベースの Spark 3.5.6 + Iceberg 1.10.0 と、検証1のランタイム(Spark 3.2.1 + Iceberg 1.4.0)より大幅に新しい構成なので、検証1で問題となった箇所が解消されるかも確認しました。

まず前提として、Apache Spark version 3.5 は検証1と実行方法が異なります。Athena コンソールのノートブックや検証1で使った start-calculation-execution は使えず、コード実行の入口は Spark Connect になります。既存ワークグループのエンジン変更もできないので、専用のワークグループを新規作成しました。

https://docs.aws.amazon.com/athena/latest/ug/notebooks-spark-release-versions.html

Spark Connect で接続

セッションを起動し、GetSessionEndpoint API でエンドポイント URL と認証トークンを取得して、手元の pyspark[connect] クライアントから接続します。

athena = boto3.client("athena", region_name="ap-northeast-1")
session_id = athena.start_session(
    WorkGroup="cm-example-spark35-wg",
    EngineConfiguration={"MaxConcurrentDpus": 20},
)["SessionId"]
# (セッションが IDLE になるまで待機)

resp = athena.get_session_endpoint(SessionId=session_id)
url = (resp["EndpointUrl"].replace("https", "sc", 1)
       + f":443/;use_ssl=true;x-aws-proxy-auth={resp['AuthToken']}")

spark = SparkSession.builder.remote(url).getOrCreate()

StartSessionSparkProperties が使えないため、Horizon カタログの定義は接続後にクライアント側から流し込みます。

spark.conf.set("spark.sql.catalog.horizon", "org.apache.iceberg.spark.SparkCatalog")
spark.conf.set("spark.sql.catalog.horizon.type", "rest")
# ...以降、検証1と同じカタログ設定を spark.conf.set で設定

動作検証

検証スクリプト(verify_horizon_write_35.py)は検証1と同じ流れですが、検証1で必要だった3つのワークアラウンド(NTZ タイムスタンプ・ベクトル化読み取り・shuffle partitions)をあえて外して実行し、Iceberg 1.10.0 でどこまで解消されるかを確認する構成にしています。また v3 テーブルは検証1では読み取り時点で失敗して試せなかった UPDATE / DELETE まで検証しています。

  1. 環境情報の出力(Spark バージョン。Spark Connect クライアントからは JVM 側の Iceberg バージョンを取得できないため、エンジンのベースである EMR 7.12 同梱のドキュメント値 1.10.0 として記録)
  2. SHOW NAMESPACES IN horizon で Snowflake 側のスキーマが見えることを確認
  3. v2 テーブルの読み取り
  4. v2 テーブルへの INSERT
  5. v2 テーブルの UPDATE
  6. v2 テーブルからの DELETE
  7. 書き込み後の v2 テーブルを再読み取りし、written_by 別の件数を確認
  8. v2 テーブルのスナップショット履歴を参照し、外部コミットを確認
  9. v3 テーブルの読み取り
  10. v3 テーブルへの INSERT
  11. v3 テーブルの UPDATE
  12. v3 テーブルの DELETE
実行ログ
============================================================
### 0. 環境情報
============================================================
Spark version: 3.5.6-amzn-1
Iceberg version: 1.10.0(EMR 7.12 同梱・ドキュメント値)

============================================================
### 1. ネームスペース一覧(Snowflake スキーマが見えること)
============================================================
+---------+
|namespace|
+---------+
|ANALYTICS|
+---------+

============================================================
### 2. v2 テーブルの読み取り(NTZ ワークアラウンドなし)
============================================================
+----------+-------------+-------+
|col_name  |data_type    |comment|
+----------+-------------+-------+
|ORDER_ID  |int          |NULL   |
|PRODUCT   |string       |NULL   |
|QUANTITY  |int          |NULL   |
|UNIT_PRICE|decimal(10,2)|NULL   |
|ORDERED_AT|timestamp_ntz|NULL   |
|WRITTEN_BY|string       |NULL   |
+----------+-------------+-------+

+--------+--------+--------+----------+-------------------+------------+
|ORDER_ID|PRODUCT |QUANTITY|UNIT_PRICE|ORDERED_AT         |WRITTEN_BY  |
+--------+--------+--------+----------+-------------------+------------+
|1       |keyboard|2       |4500.00   |2026-07-01 09:00:00|snowflake   |
|2       |mouse   |5       |1980.00   |2026-07-01 10:30:00|snowflake   |
|3       |monitor |1       |32000.00  |2026-07-01 14:15:00|snowflake   |
|201     |ssd-1tb |2       |12800.00  |2026-07-02 10:00:00|athena-spark|
+--------+--------+--------+----------+-------------------+------------+


============================================================
### 3. v2 テーブルへの INSERT
============================================================
INSERT 成功

============================================================
### 4. v2 テーブルの UPDATE(shuffle.partitions=1 なし)
============================================================
UPDATE 成功

============================================================
### 5. v2 テーブルの DELETE
============================================================
DELETE 成功

============================================================
### 6. v2 テーブルの読み取り(書き込み後)
============================================================
+--------+--------+--------+----------+-------------------+--------------------+
|ORDER_ID|PRODUCT |QUANTITY|UNIT_PRICE|ORDERED_AT         |WRITTEN_BY          |
+--------+--------+--------+----------+-------------------+--------------------+
|1       |keyboard|2       |4500.00   |2026-07-01 09:00:00|snowflake           |
|2       |mouse   |5       |1980.00   |2026-07-01 10:30:00|snowflake           |
|3       |monitor |1       |32000.00  |2026-07-01 14:15:00|snowflake           |
|201     |ssd-1tb |2       |12800.00  |2026-07-02 10:00:00|athena-spark        |
|301     |usb-hub |2       |3400.00   |2026-07-02 11:00:00|athena-spark-connect|
+--------+--------+--------+----------+-------------------+--------------------+

+--------------------+---------+
|written_by          |row_count|
+--------------------+---------+
|athena-spark        |1        |
|athena-spark-connect|1        |
|snowflake           |3        |
+--------------------+---------+


============================================================
### 7. v2 テーブルのスナップショット履歴
============================================================
+-----------------------+-------------------+---------+
|committed_at           |snapshot_id        |operation|
+-----------------------+-------------------+---------+
|2026-07-04 12:47:05.39 |7325365669594828787|append   |
|2026-07-04 12:49:41.525|3821238997686183694|append   |
|2026-07-04 12:49:46.974|1167080387288909088|overwrite|
|2026-07-04 12:49:50.239|8203787599929521889|delete   |
|2026-07-04 12:52:21.215|6867292432401266181|append   |
|2026-07-04 12:52:25.633|2458057220796135436|overwrite|
|2026-07-04 12:52:28.405|8848674517559331220|delete   |
+-----------------------+-------------------+---------+


============================================================
### 8. v3 テーブルの読み取り(Iceberg 1.10 での v3 対応検証)
============================================================
+--------+-------+--------+----------+-------------------+----------+
|ORDER_ID|PRODUCT|QUANTITY|UNIT_PRICE|ORDERED_AT         |WRITTEN_BY|
+--------+-------+--------+----------+-------------------+----------+
|1       |desk   |1       |25000.00  |2026-07-01 09:00:00|snowflake |
|2       |chair  |2       |18000.00  |2026-07-01 11:45:00|snowflake |
+--------+-------+--------+----------+-------------------+----------+

v3 読み取り成功

============================================================
### 9. v3 テーブルへの INSERT
============================================================
v3 INSERT 成功
+--------+--------+--------+----------+-------------------+--------------------+
|ORDER_ID|PRODUCT |QUANTITY|UNIT_PRICE|ORDERED_AT         |WRITTEN_BY          |
+--------+--------+--------+----------+-------------------+--------------------+
|1       |desk    |1       |25000.00  |2026-07-01 09:00:00|snowflake           |
|2       |chair   |2       |18000.00  |2026-07-01 11:45:00|snowflake           |
|111     |desk-mat|1       |2200.00   |2026-07-02 11:10:00|athena-spark-connect|
+--------+--------+--------+----------+-------------------+--------------------+


============================================================
### 10. v3 テーブルの UPDATE(行レベル更新)
============================================================
FAILED: (org.apache.spark.SparkException) Job aborted due to stage failure: Task 0 in stage 21.0 failed 4 times, most recent failure: Lost task 0.3 in stage 21.0 (TID 20) ([2406:da14:910:b315:4dba:bb4:cd82:78bd] executor 1): java.lang.IllegalArgumentException: Structs do not match: StructType(StructField(ORDER_ID,IntegerType,true),StructField(PRODUCT,StringType,true),StructField(QUANTITY,IntegerType,true),StructField(UNIT_PRICE,DecimalType(10,2),true),StructField(ORDERED_AT,TimestampNTZType,true),StructField(WRITTEN_BY,StringType,true)) and message table {
  optional int32 ORDER_ID = 1;
  optional binary PRODUCT (STRING) = 2;
  optional int32 QUANTITY = 3;
  optional int64 UNIT_PRICE (DECIMAL(10,2)) = 4;
  optional int64 ORDERED_AT (TIMESTAMP(MICROS,false)) = 5;
  optional binary WRITTEN_BY (STRING) = 6;
  optional int64 _row_id = 2147483540;
  optional int64 _last_updated_sequence_number = 2147483539;
}

        ...(スタックトレース 13 行省略)...

============================================================
### 11. v3 テーブルの DELETE(行レベル削除 / deletion vectors)
============================================================
v3 DELETE 成功
+--------+-------+--------+----------+-------------------+----------+
|ORDER_ID|PRODUCT|QUANTITY|UNIT_PRICE|ORDERED_AT         |WRITTEN_BY|
+--------+-------+--------+----------+-------------------+----------+
|1       |desk   |1       |25000.00  |2026-07-01 09:00:00|snowflake |
|2       |chair  |2       |18000.00  |2026-07-01 11:45:00|snowflake |
+--------+-------+--------+----------+-------------------+----------+

v2 テーブル

v2 テーブルへの読み取りと INSERT / UPDATE / DELETE はすべて成功しました。PySpark engine version 3 で必要だったワークアラウンドの要否は以下のとおりです。

検証1(Spark 3.2.1 + Iceberg 1.4.0) 検証2(Spark 3.5.6 + Iceberg 1.10.0)
handle-timestamp-without-timezone 必要 不要(TIMESTAMP_NTZ ネイティブ対応)
vectorization.enabled = false 必要 引き続き必要
shuffle.partitions = 1(DeleteObject 403 回避) 必要 不要(403 は再現せず)

ベクトル化読み取りの問題だけが残りました。ただしエラーの中身が変わっています。

java.lang.UnsupportedOperationException: Cannot support vectorized reads for column [PRODUCT]
optional binary PRODUCT (STRING) = 2 with encoding DELTA_LENGTH_BYTE_ARRAY.
Disable vectorized reads to read this table/file

検証1では整数列の DELTA_BINARY_PACKED で失敗していましたが、こちらは Iceberg 1.10 で対応済みです。今度は Snowflake が文字列型の列に使う DELTA_LENGTH_BYTE_ARRAY で引っかかります。この対応は Iceberg 1.11 かららしいので、もう1世代先のランタイム更新があれば解消される見込みです。

v3 テーブル

v3 テーブルは検証1では読み取りができませんでしたが、Iceberg 1.10.0 は v3 読み取りが成功し、INSERT した行も見えています。

+--------+--------+--------+----------+-------------------+--------------------+
|ORDER_ID|PRODUCT |QUANTITY|UNIT_PRICE|ORDERED_AT         |WRITTEN_BY          |
+--------+--------+--------+----------+-------------------+--------------------+
|1       |desk    |1       |25000.00  |2026-07-01 09:00:00|snowflake           |
|2       |chair   |2       |18000.00  |2026-07-01 11:45:00|snowflake           |
|111     |desk-mat|1       |2200.00   |2026-07-02 11:10:00|athena-spark-connect|
+--------+--------+--------+----------+-------------------+--------------------+

DELETE も成功しました。一方 UPDATE だけは失敗します。

java.lang.IllegalArgumentException: Structs do not match:
StructType(StructField(ORDER_ID,IntegerType,true), ..., StructField(WRITTEN_BY,StringType,true))
and message table {
  optional int32 ORDER_ID = 1;
  ...
  optional binary WRITTEN_BY (STRING) = 6;
  optional int64 _row_id = 2147483540;
  optional int64 _last_updated_sequence_number = 2147483539;
}

書き込みスキーマの末尾に _row_id_last_updated_sequence_number が現れています。これは Iceberg v3 で必須になった row lineage(行系譜)のメタデータ列で、Spark 3.5 向けの Iceberg 統合はこれを含む行の書き直しをまだ扱えません。DELETE が成功するのは既存行への削除印(deletion vectors)を書くだけで行の書き直しが発生しないから、UPDATE が失敗するのは変更後の行を lineage 列付きで書き直す必要があるから、という差です。row lineage の対応は Spark 4.x 系の Iceberg 統合からなので、v3 のフル読み書きはもう少し先になります。

検証3: Athena SQL + Glue カタログフェデレーション

続いて Athena SQL からの読み書きです。Athena SQL は IRC に直接接続できないため、Glue Data Catalog のカタログフェデレーションを挟みます。

https://docs.aws.amazon.com/lake-formation/latest/dg/catalog-federation-snowflake.html

セットアップの要点

構築は次の流れです。

  1. PAT をアクセストークンに交換し、{"BEARER_TOKEN": "<トークン>"} の形で Secrets Manager に保存
  2. Glue / Lake Formation 用の IAM ロールを作成(シークレット読み取り + テーブルデータの S3 アクセス)
  3. Glue 接続(SNOWFLAKEICEBERGRESTCATALOG・CUSTOM 認証)を作成
  4. 接続を Lake Formation にリソース登録(--with-federation --with-privileged-access
  5. フェデレーテッドカタログを作成(Identifier には Snowflake のデータベース名を指定)
  6. Lake Formation でクエリ実行プリンシパルにテーブル権限を付与

Secrets Manager に保存した Bearer トークンは約1時間で失効するため、恒久運用にはトークン自動更新の仕組みが必要です。

結果

読み取りは成功です。検証1・検証2で Athena Spark が書き込んだ行もフェデレーション越しに見えます。

SELECT written_by, COUNT(*) AS row_count
FROM "snowflake_horizon_cat"."analytics"."orders_v2"
GROUP BY written_by;
-- athena-spark          1
-- athena-spark-connect  1
-- snowflake             3

一方 INSERT は権限エラーで失敗しました。

com.amazonaws.services.lakeformation.model.AccessDeniedException:
Principal does not have any privilege on specified resource

Lake Formation のエラーで失敗しているように見えますが、テーブル権限(SELECT / INSERT / DELETE / ALTER / DESCRIBE)に加えデータベースレベルの権限をすべて付与しても結果は変わりませんでした。

AWS・Snowflake 双方の公式ドキュメントを確認しましたが、「Glue カタログフェデレーション経由の書き込みは仕様上サポートされない」と明言した記述は見当たりませんでした。両社のドキュメント・ブログはいずれも一貫して「クエリ(読み取り)」としか言及しておらず、書き込みへの言及自体が存在しません。

https://docs.aws.amazon.com/lake-formation/latest/dg/catalog-federation-snowflake.html

https://aws.amazon.com/blogs/big-data/access-snowflake-horizon-catalog-data-using-catalog-federation-in-the-aws-glue-data-catalog/

https://docs.snowflake.com/en/user-guide/tables-iceberg-access-using-external-query-engine-snowflake-horizon

v3 テーブル: こちらもエンジン側で不可

v3 テーブルは読み取り・書き込みとも同じエラーで失敗しました。

GENERIC_INTERNAL_ERROR: Iceberg format version 3 is not supported

Athena SQL は明確なメッセージで v3 未対応を返します。2026-07 時点で Athena から Iceberg v3 テーブルに触れるのは、検証2で確認した Apache Spark version 3.5 エンジンだけという状況です。

まとめ

ここまでの検証をまとめます。

  • Athena Spark で、Snowflake 管理 Iceberg テーブル(v2)への INSERT / UPDATE / DELETE がすべて成功し、外部エンジン書き込み GA を確認できました。これは PySpark engine version 3・Apache Spark version 3.5 の両エンジンで成立します
  • 両エンジンのコミットは単一のスナップショット履歴に共存し、互いの書き込みが即座に見えます
  • credential vending により、エンジン側の実行ロールに Iceberg データ領域への S3 権限を付与せずに読み書きできました
  • PySpark engine version 3(Spark 3.2.1 + Iceberg 1.4.0)起因の問題が3つ: NTZ タイムスタンプ、DELTA_BINARY_PACKED、vended credentials に s3:DeleteObject が含まれない問題。いずれもセッション設定で回避可能で、Apache Spark version 3.5(Spark 3.5.6 + Iceberg 1.10.0)ではベクトル化読み取り(対象が DELTA_LENGTH_BYTE_ARRAY に変化)だけが残ります
  • Iceberg v3 テーブルは PySpark engine version 3・Athena SQL では読み書きとも不可ですが、Apache Spark version 3.5 では読み取り・INSERT・DELETE の動作が確認できました。UPDATE は row lineage 対応が入る Spark 4.x 系で解消される見込みです
  • Athena SQL + Glue カタログフェデレーションは読み取り専用です。書き込みは Lake Formation のベンディング拒否により権限エラー風のメッセージで失敗します。公式ドキュメントに明言はありませんが、実機検証の結果からは仕様上の制約と考えられます

Snowflake 管理 Iceberg テーブルへ AWS 側から書き込みたい場合、現時点では Spark 系エンジンから Horizon IRC に直接接続する構成となります。Athena なら、コンソールノートブックで手軽に使える PySpark engine version 3 と、v3 テーブルや新しいランタイムが必要なときの Apache Spark version 3.5 + Spark Connect という使い分けになります。フェデレーションは読み取り統合の仕組みと割り切り、書き込みは IRC への直接接続と使い分けるのが良さそうです。


Snowflakeの導入支援はクラスメソッドに!

クラスメソッドでは Snowflake の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

Snowflakeの詳細を見る

この記事をシェアする

関連記事