S3 Tablesによる新しいデータ管理！クエリ高速化と運用コスト削減を試してみた

# はじめに

先日、2025/01/20 に[Amazon S3 Tablesが東京リージョンで利用可能](https://aws.amazon.com/jp/about-aws/whats-new/2025/01/amazon-s3-tables-additional-aws-regions/)になりました。

そこで今回は、Amazon S3 Tablesについて以下を中心に解説していきます。

- Amazon S3 Tablesとは何か
- 既存構成との違い
- 実際の使い方

実際に検証した内容も含めて紹介するので、導入を検討されている方の参考になれば幸いです。

# Amazon S3 Tablesについて

## 概要

Amazon S3 Tablesは、Apache Icebergサポートが組み込まれた初のクラウドオブジェクトストアです。
以下のサービスとシームレスに連携可能です。
- Amazon Athena
- Amazon Redshift
- Amazon EMR
- Apache Spark

また、継続的なテーブルメンテナンス機能を備えており、クエリ性能の最適化やストレージコストの削減を自動的に実施してくれます。

## 主なメリット

では、既存のS3 + Glue + クエリエンジン(Athena, Redshift, EMR, Spark)の組み合わせとどう違うのでしょうか？

大きく異なるポイントは2つあると考えております。

1つ目は **クエリパフォーマンスの最適化** 

Icebergテーブルをセルフマネージドで運用する場合と比較して、
クエリ性能が最大3倍高速化、1秒あたりのトランザクション数が10倍増加します。


2つ目は **管理コストの低減** 

自動的な以下のメンテナンス機能により、管理の手間を大幅に削減：
- コンパクション（ファイル統合）
- スナップショット管理
- 未使用ファイルの削除


参照：https://aws.amazon.com/jp/s3/features/tables/

おおまかにですが、以上の2点が既存の手法と比べて優れている点なのではないかと思います。

## 個人的に嬉しいポイント

①コンパクションの部分と②データの削除メンテナンス機能がとても画期的だと感じております。

コンパクションは、一定サイズ(デフォルト512MB)のファイルになるように、複数の小さなファイルを1つのファイルにまとめてくれる機能です。
[Amazon Athena のパフォーマンスチューニング Tips トップ10](https://aws.amazon.com/jp/blogs/news/top-10-performance-tuning-tips-for-amazon-athena/) にもあるようにAthenaでクエリするS3上のファイルはほどよく大きい方がクエリのパフォーマンスが高いです。

![スクリーンショット 2025-01-27 0.35.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/d9e0330a-7cf9-1ec2-c746-9200b0d31b18.png)

私も現場で度々、大量のサイズの小さいログデータファイルが配置されたS3に対してクエリを実行するという機会があったのですが、わかりやすくパフォーマンスが低下します。これを自動でメンテナンスしてくれるというのは大変ありがたいです。

②データの削除
既存の構成(S3, Glue, Athena)では特定のデータを削除することができません。
それだと何が不都合だったかというと、GDPR対応などで特定のユーザーのデータだけを削除するということができませんでした。そのため、AthenaではなくDelta Lakeなどを導入してどうにかして特定のユーザーのデータを削除するということをしておりました。Apache IcebergテーブルデータだとAthena上でDELETE文を実行するだけでそれが可能になるため運用がかなり楽になります。
参考：https://docs.aws.amazon.com/ja_jp/athena/latest/ug/delete-statement.html


# 使ってみた

百聞は一見にしかず ということで実際に使って検証してみましょう。

## テーブルバケットの作成

まずはテーブルバケットを作成していきます。

AWSコンソールからS3のサービスを選択し、ナビゲーションペインの部分からテーブルバケットを選択
そして "テーブルバケットを作成"
![スクリーンショット 2025-01-26 19.33.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/81903fb9-8fe4-5698-3757-0adc680c4d27.png)

今回は `inu-is-dog-s3-table-test` というバケットを作成します。
![スクリーンショット 2025-01-26 19.34.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/a14707cd-041e-26d7-23ce-1f6eb3a9b919.png)

![スクリーンショット 2025-01-26 19.34.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/0dba6268-8e4b-b34f-5af7-d71c6ef25b8c.png)

無事、作成できました。（アカウントIDは伏せさせていただいております。）


## データの投入

AWSが公開している[チュートリアル](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-getting-started.html#s2-tables-tutorial-EMR-cluster)に従って、
Amazon EMRを使ってデータを投入していきます。

まずEMRのIAMロールを作成します。
CloudShellを開いて以下のコマンドを実行

![スクリーンショット 2025-01-26 20.06.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/a03423b3-e7c7-c8e8-3dac-b1ce686b789b.png)

```shell
~ $ aws emr create-default-roles
```

これでIAMロール `EMR_EC2_DefaultRole` が作成されました。
次にこのロールに `AmazonS3TablesFullAccess` ポリシーをアタッチしたいので以下のコマンドを実行します。
```shell
aws iam attach-role-policy --role-name EMR_EC2_DefaultRole --policy-arn arn:aws:iam::aws:policy/AmazonS3TablesFullAccess
```
これでアタッチが完了しました。
一応、IAMの画面でも確認しておきましょう。

![スクリーンショット 2025-01-26 20.08.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/b1cef3b7-358e-f675-a3d3-ef562c4621ad.png)

きちんと追加されていますね。大丈夫そうです。

では、EMRクラスターを起動してみましょう。
下ごしらえとして、CloudShell上で `configurations.json`ファイルを生成します。
```shell
echo "[{\"Classification\":\"iceberg-defaults\",\"Properties\":{\"iceberg.enabled\":\"true\"}}]" > configurations.json
```


そしてキーペアをCloudShellにアップロードした後、以下のコマンドをCloudShell上で実行します。
```
aws emr create-cluster --release-label emr-7.5.0 \
--applications Name=Spark \
--configurations file://configurations.json \
--region ap-northeast-1 \
--name My_Spark_Iceberg_Cluster \
--log-uri s3://inu-is-dog-log/emr/ \
--instance-type m5.xlarge \
--instance-count 2 \
--service-role EMR_DefaultRole \
--ec2-attributes \
InstanceProfile=EMR_EC2_DefaultRole,SubnetId=subnet-1234567890abcdef0,KeyName=junjun-key
```

サブネットの`subnet-1234567890abcdef0` と EMRインスタンスへのキーペア `junjun-key`はご自身の環境にあわせてカスタマイズしてください。

私の設定ですと、`s3://inu-is-dog-log/emr/` 配下にログファイルを生成するので `EMR_DefaultRole` にこのS3へのアクセス許可をしております。これを忘れるとログファイルを保存できなくなってしまうのでお忘れなきよう。

このコマンドを実行してエラーが出なければ、EMRクラスターが生成されているはずです。
![スクリーンショット 2025-01-26 20.38.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/58c4f73d-36c7-7c50-839b-40c41e2648d8.png)

AWSコンソールでも確認してみましょう。

EMRクラスターが無事生成されていますね。

![スクリーンショット 2025-01-26 20.58.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/261292da-5f29-f7fb-86e7-4bef14914222.png)


EMR Clusterの準備ができたら、CloudShellからSSH接続してみましょう
SSHコマンドはこちらから参照できます。
![スクリーンショット 2025-01-26 21.00.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/5f444dc5-d086-61b5-585c-077fc4598513.png)


このご機嫌な画面がでてきたらSSH接続成功です。
![スクリーンショット 2025-01-26 21.15.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/f5bfc82c-2d6c-9ac1-7161-333f71169ce7.png)

それではSpark Shellを立ち上げましょう
※ <S3 Tables BucketのARN> は置換してください。
```shell
spark-shell \
--packages software.amazon.s3tables:s3-tables-catalog-for-iceberg-runtime:0.1.3 \
--conf spark.sql.catalog.s3tablesbucket=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.s3tablesbucket.catalog-impl=software.amazon.s3tables.iceberg.S3TablesCatalog \
--conf spark.sql.catalog.s3tablesbucket.warehouse=<S3 Tables BucketのARN> \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
```

お馴染み(?)のSparkの画面が出てきましたね。
![スクリーンショット 2025-01-26 21.22.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/36d44d2e-304a-e0ae-b4d4-b53dbdd320e4.png)


Spark SQLコマンドでnamespaceを作成します。今回は`inuspace`というnamespaceを作ってみます。
```shell
scala> spark.sql("CREATE NAMESPACE IF NOT EXISTS s3tablesbucket.inuspace") 
res0: org.apache.spark.sql.DataFrame = []  
```

namespaceができたようです。確認しましょう。
```
scala> spark.sql("SHOW NAMESPACES IN s3tablesbucket").show()
~中略~
+---------+
|namespace|
+---------+
| inuspace|
+---------+
```

無事、作成されたようです。
次に、Icebergテーブルを作成します。テーブル名は`dog_table`です。
```shell
scala> spark.sql( 
""" CREATE TABLE IF NOT EXISTS s3tablesbucket.inuspace.dog_table ( 
    id INT, 
    name STRING, 
    value INT 
) 
USING iceberg """
)
```

作成した`dog_table`にデータを投入します。
```shell
scala> spark.sql(
"""
    INSERT INTO s3tablesbucket.inuspace.dog_table
    VALUES 
        (1, 'ABC', 100), 
        (2, 'XYZ', 200)
""")
```

show()関数でデータを確認しましょう。

```shell
scala> spark.sql(""" SELECT * FROM s3tablesbucket.inuspace.dog_table """).show()
+---+----+-----+                                                                
| id|name|value|
+---+----+-----+
|  1| ABC|  100|
|  2| XYZ|  200|
+---+----+-----+
```

これでS3 Tables Bucketへのデータの投入は以上となります。
EMRクラスターは用済みなので終了しておきましょう。（残っていると費用がかかってしまいますので）
```shell
aws emr terminate-clusters --cluster-ids <cluster-id>
```

## S3 Tables Bucketの確認

では、namespace、tableを作成したテーブルバケットをAWSコンソール上で確認してみましょう。
![スクリーンショット 2025-01-26 21.37.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/f1d0b54b-3f9d-25d8-2626-ae8a9cbd1078.png)

## AWS Lake Formation 権限付与

データができたということでこのデータにアクセスしてクエリできるように権限設定していきたいと思います。
AWS Lake Formation をAWSコンソールで開き、`s3tablescatalog` にGrantしていきます。

![スクリーンショット 2025-01-27 0.14.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/f293cf41-52c9-69a4-3534-f60791449133.png)

今回はIAMユーザー junjun に以下のように権限付与していきます。

![スクリーンショット 2025-01-27 0.16.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/1293b8b6-c949-7c87-6044-1bcfd9c300f4.png)

![スクリーンショット 2025-01-27 0.16.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/b997a3d8-a901-a76c-04c5-13bb079060f6.png)

これで権限付与は完了です。

## Athena連携

AWSのコンソールでAthenaを開きます。

データソースを`AwsDataCatalog`、
カタログを`s3tablescatalog/inu-is-dog-s3-tables-test`、
データベースを`inuspace`に設定し、以下のクエリを実行
```sql
SELECT * FROM "inuspace"."dog_table" LIMIT 10;
```
![スクリーンショット 2025-01-27 0.25.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/a5b437d9-76c5-a7c7-3da0-32b2a5753bdb.png)

無事、データを取得することができました。
データの削除も試してみましょう。
DELETE文を実行した後、
```sql
DELETE FROM "inuspace"."dog_table" WHERE id = 1;
```
再度、データを取得。データがちゃんと消えていますね。素晴らしい。
![スクリーンショット 2025-01-27 0.48.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451269/4c8370a2-9c08-f628-dcc2-517f630e47d2.png)

これで検証は以上になります。

# 最後に

この記事がこれからAmazon S3 Tablesを使ってみたいという方のご助力になれば幸いです。

所感としては、データ（テーブル）のメンテナンスコストがかなり低減された一方で、テーブルの作成はEMRやData Firehose経由でなければならない点のハードルの高さが気になりました。このあたりが既存の構成（S3, Glue, Athena）と同じようにサクッと導入できるようになればより嬉しいですね。

AWS Athenaについて詳しく知りたい方は[こちら](https://qiita.com/yoshiyama_hana/items/9b2f26b214de92a0f36c)をどうぞ