Amazon S3 Tablesにデータを投入してAthenaとRedshiftからクエリしてみた

この記事は[BeeX Advent Calendar 2024](https://qiita.com/advent-calendar/2024/beex)の5日目の記事です。
遅刻しました。(12/5投稿予定だった)

※本記事には一部AWSのプレビュー機能も含まれており、一般公開された際には記事の内容から変更される可能性がありますので、認識の上ご覧ください。

# はじめに
先日アメリカのラスベガスで開催された AWS re:Invent 2024では多くのAWSアップデートが発表されました。
本記事ではその中でもAmazon S3 Tablesにフォーカスして検証してみましたので、記録として書いていきます。

https://aws.amazon.com/about-aws/whats-new/2024/12/amazon-s3-tables-apache-iceberg-tables-analytics-workloads/

ざっくりどんなことをする必要があるか知りたい方は以下のAWSブログをご確認ください。

https://aws.amazon.com/blogs/aws/new-amazon-s3-tables-storage-optimized-for-analytics-workloads/

# 概要
## S3 Tablesとは
まずS3 Tablesについてですが、概要ページには次のように記載されています。

> Amazon S3 Tablesは、Apache Icebergをサポートする初のクラウドオブジェクトストアであり、表形式データを大規模に保存することを効率化します。 S3 Tablesは、汎用のS3バケットに格納された自己管理Icebergテーブルと比較して、最大3倍のクエリパフォーマンスと最大10倍のトランザクション/秒を実現し、分析ワークロードに特に最適化されています。

https://aws.amazon.com/s3/features/tables/

S3 Tablesとは、Amazon S3とOpen Table Format技術の一つであるApache Icebergを統合したマネージドなApache Icebergテーブルを提供するサービスになります。
以下のページでApache Icebergやそれを利用したトランザクションデータレイクについて解説されているので、興味がある方はそちらをご確認ください。

https://aws.amazon.com/jp/what-is/apache-iceberg/

また、Apache Icebergについて触りながら学びたいという方は [べりんぐ(@_Bassari)](https://x.com/_Bassari)さんが作られたハンズオンがオススメです。
自分もApache Icebergまったくワカラン状態から始めましたが、ハンズオンこなしただけでかなり理解が深まりました。非常に感謝してます。

https://github.com/lawofcycles/apache-iceberg-101-ja

## Open Table Formatとは
Open Table Format技術は他にも種類があり、Apache Icebergに加えて、Uberが開発したApache Hudi、Databricksが開発したDelta Lakeが一般的に知られています。
ちなみにApache IcebergはNetflixが開発したものになります。
詳しくは以下のAWS Black Beltを見ていただくと詳しく説明されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/02ad91db-ebe8-23b7-3ab6-e39f355b5ec3.png)

https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_Datalake-Format-On-AWS_0516_v1.pdf

Open Table Format技術が広まった理由として、以下のようなデータレイクの課題があったことが挙げられます。
私自身も過去にデータ基盤の設計・開発に携わったことがありますが、更新系データの扱いや頻繁にあるデータソースの変更への追従など、かなり苦労した記憶があります。

※上記資料から抜粋
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/fbdd4814-8f2c-2c4a-bbd1-63867642fe09.png)

AWSでは2023年6月からAmazon Athena for Apache Sparkで上記3つのOTFのサポートを開始していました。

https://aws.amazon.com/jp/about-aws/whats-new/2023/06/amazon-athena-apache-spark-hudi-iceberg-delta-lake/

whats newページの以下の記載を見る限りだと最適化やテーブルメンテナンスに課題があったのではないかと思われます。
(私自身は検証しようと思ったままずっと放置してたので自己管理テーブルを構築した経験はありません)

> S3 Tablesは、特に分析ワークロード用に最適化されており、自己管理テーブルと比較して、最大3倍のクエリスループットと最大10倍のトランザクション/秒を実現します。 Apache Iceberg標準をサポートするS3 Tablesにより、表形式データは、一般的なAWSやサードパーティのクエリエンジンで簡単にクエリできます。 さらに、S3 Tablesは、データレイクがスケールし進化しても、クエリの効率とストレージコストを自動的に最適化するために、継続的なテーブルメンテナンスを実行するように設計されている。

https://aws.amazon.com/about-aws/whats-new/2024/12/amazon-s3-tables-apache-iceberg-tables-analytics-workloads/

# 実作業
## Amazon S3 Tablesの構築とデータ投入
概要が理解できたところでS3 Tablesリソースの構築をデータ投入をしていきたいと思います。

AWSドキュメントのS3 Tablesのページを確認すると、S3 Tablesを利用するためのチュートリアルがあったので最初はこちらを利用しようかと思ったのですが、Amazon EMRが書いてあるコマンドだと構築できなかったので別の方法で行いました。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-getting-started.html

※私が普段EMRを使わないのもありますが、コマンドで構築すると起動する前にエラーが出て終了してしまうので諦めました。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/c59ff448-9f9a-d1f9-be49-88936fc89d12.png)

上記チュートリアルに記載されていますが、S3 Tablesリソースにデータを投入するのにspark-shellを利用していたので、ローカルの環境でspark-shellを実行するための環境を構築する方法にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/c15c136c-ca92-595d-91b3-3d8077df0a53.png)

### 環境構築
ローカルの環境はDockerを利用して構築しました。
これから説明する内容はClaudeにサポートしてもらいながら構築したので、もしかしたら少し過不足や見ていて気になるところがあるかもしれませんが、検証用用途として作ったものなので、そのあたりは大目に見てもらえると。

環境構築のために構築したのは以下のファイル群です。

```bash
./
.env
Dockerfile
docker-compose.yml
requirements.txt
```

Docker上で利用するAWS認証情報は.envファイルに記載してます。

```bash
# .env.template
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
```

Dockerfileの内容は以下です。

```Dockerfile
# ベースイメージとしてPython公式イメージを使用
FROM python:3.9-slim

# 必要なパッケージのインストール
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    openjdk-17-jdk \
    curl \
    wget \
    procps \
    awscli \
    && rm -rf /var/lib/apt/lists/*

# 環境変数の設定
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ENV SPARK_VERSION=3.4.4
ENV HADOOP_VERSION=3
ENV SPARK_HOME=/opt/spark
ENV PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin:${JAVA_HOME}/bin:/usr/local/bin
ENV PYSPARK_PYTHON=/usr/local/bin/python
ENV PYSPARK_DRIVER_PYTHON=/usr/local/bin/python

# Sparkのダウンロードとインストール
RUN wget https://dlcdn.apache.org/spark/spark-${SPARK_VERSION}/spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz \
    && tar -xvzf spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz \
    && mv spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION} /opt/spark \
    && rm spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz

# requirements.txtをコピー
COPY requirements.txt /tmp/requirements.txt

# pysparkの追記とパッケージのインストール
RUN echo "pyspark==${SPARK_VERSION}" >> /tmp/requirements.txt && \
    pip install --no-cache-dir -r /tmp/requirements.txt && \
    pip install --upgrade pip && \
    pip cache purge

# PATHを通す
ENV PATH="/usr/local/bin:$PATH"

# ワーキングディレクトリの設定
WORKDIR /app

# Jupyterのポートを公開
EXPOSE 8888 4040

# Jupyter Notebookを起動
CMD ["jupyter", "notebook", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--allow-root"]
```

docker-compose.ymlは最小限の内容だけ記載してます。

```Dockerfile
# docker-compose.yml
version: '3'
services:
  spark:
    build: .
    environment:
      - AWS_REGION=${AWS_REGION}
      - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
    volumes:
      - .:/app
    ports:
      - "8888:8888"
      - "4040:4040"
```

requirements.txtには次の内容を記載してます。

```
jupyter
notebook
pandas
numpy
boto3
```

ファイルの準備ができたら以下のコマンドを実行してコンテナを起動します。

```bash
$ docker-compose up -d
$ docker-compose exec spark bash
```

私の環境だとこんな感じでコンテナ内に入れました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/59b866c6-5c6a-3567-4728-cecec162534a.png)

ここまでできたらS3 Tablesリソースにアクセスできる準備はできたので、次は実際にリソースを構築していきます。

### Amazon S3 Tablesの構築
Amazon S3上での位置づけ的には汎用バケット、ディレクトリバケットに続く新しいバケットタイプとされています。
ドキュメント上だとテーブルバケットと記載されており、S3コンソール上のメニューは別で用意されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/8983e795-9fa7-7756-fd9d-a04f468abe9a.png)

※現在はまだ米国東部（バージニア州北部）、米国東部（オハイオ州）、米国西部（オレゴン州）の3リージョンでしか提供されていません

S3 TablesリソースとAWSの分析サービスとの統合は現在プレビュー機能として提供されており、Table Bucketsの画面から有効化が可能で、この機能はリージョン毎に設定が可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/1af540c6-082c-46a8-e6b2-dea72753ac61.png)

コンソールからはテーブルバケット名のみ入力すればリソースが構築できます。
今回はわかりやすく「qiitatablebucket」というバケット名にしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/75537ca7-d771-7380-9e24-7e581185741e.png)

ここで記事執筆時点(2024年12月7日)の注意点として、以下ドキュメントのテーブルバケット名の命名規則を見ると「小文字、大文字、数字、ハイフン(-)しか使用できない」とありますが、ハイフン入れると分析サービスからクエリする際にエラーが出たりしたので、極力ハイフンなしの名前にしたほうがよいです。
※クエリ機能はプレビューなのでGAには修正されるんじゃないかなと思います

https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-buckets-naming.html

テーブルバケットを作成され、バケット名のリンクを押下するとテーブルバケットの詳細が確認できますが、見てわかるとおりコンソール上からはテーブルバケットを作る以外何も操作できません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/988dc06c-ba25-fec9-69aa-63bae55b72fa.png)

S3 Tablesの構造としては以下のようになっており、S3 Tablesのテーブルの部分がデータベースなどのテーブルに該当する形になります。(後ほど説明します)

- 「**テーブルバケット＞名前空間＞テーブル**」

### データ投入
テーブルバケットが作成できたら、最初に構築したローカル環境からデータを投入します。
投入するデータは以下のAWSブログのものを使います。

https://aws.amazon.com/blogs/aws/new-amazon-s3-tables-storage-optimized-for-analytics-workloads/

ローカル環境からデータを投入する場合はSpark SQLを利用するので、spark shellを起動する必要があります。
AWSドキュメントの[S3 Tables Tutorial](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-getting-started.html)にEMRで実行するspark shell起動コマンドが記載されていますが、EMRではないからかローカル環境だとこれだけではうまく動きませんでした。

```bash
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=arn:aws:s3tables:us-east-1:111122223333:bucket/amzn-s3-demo-bucket1 \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions            
```

私の環境だと、コンテナに入った後、以下のコマンドで実行することで起動できました。
`[構築したテーブルバケット名]`と`[構築したテーブルバケットARN]`の部分は自環境のものと読み替えてください。

```bash
$ spark-shell \
--packages org.apache.iceberg:iceberg-spark-runtime-3.4_2.12:1.6.1,\
software.amazon.s3tables:s3-tables-catalog-for-iceberg-runtime:0.1.3,\
org.apache.hadoop:hadoop-aws:3.3.4,\
software.amazon.awssdk:s3:2.20.68,\
software.amazon.awssdk:sts:2.20.68,\
software.amazon.awssdk:dynamodb:2.20.68,\
software.amazon.awssdk:glue:2.20.68,\
software.amazon.awssdk:kms:2.20.68,\
software.amazon.awssdk:url-connection-client:2.20.68 \
--conf spark.sql.catalog.[構築したテーブルバケット名]=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.[構築したテーブルバケット名].catalog-impl=software.amazon.s3tables.iceberg.S3TablesCatalog \
--conf spark.sql.catalog.[構築したテーブルバケット名].warehouse=[構築したテーブルバケットARN] \
--conf spark.sql.catalog.[構築したテーブルバケット名].region=us-east-1 \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
```

参考までに私の環境で実行したコマンドとログを貼っておきます。(AWSアカウントIDはダミー入れてます)
<details><summary>実行コマンドとログ</summary>

```bash
$ docker-compose exec spark bash
WARN[0000] The "AWS_DEFAULT_REGION" variable is not set. Defaulting to a blank string.
root@02f5797f4c70:/app#
root@02f5797f4c70:/app# spark-shell \
--packages org.apache.iceberg:iceberg-spark-runtime-3.4_2.12:1.6.1,\
software.amazon.s3tables:s3-tables-catalog-for-iceberg-runtime:0.1.3,\
org.apache.hadoop:hadoop-aws:3.3.4,\
software.amazon.awssdk:s3:2.20.68,\
software.amazon.awssdk:sts:2.20.68,\
software.amazon.awssdk:dynamodb:2.20.68,\
software.amazon.awssdk:glue:2.20.68,\
software.amazon.awssdk:kms:2.20.68,\
software.amazon.awssdk:url-connection-client:2.20.68 \
--conf spark.sql.catalog.qiitatablebucket=org.apache.iceberg.spark.SparkCatalog \
--conf spark.sql.catalog.qiitatablebucket.catalog-impl=software.amazon.s3tables.iceberg.S3TablesCatalog \
--conf spark.sql.catalog.qiitatablebucket.warehouse=arn:aws:s3tables:us-east-1:123456789123:bucket/qiitatablebucket \
--conf spark.sql.catalog.qiitatablebucket.region=us-east-1 \
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
:: loading settings :: url = jar:file:/opt/spark/jars/ivy-2.5.1.jar!/org/apache/ivy/core/settings/ivysettings.xml
Ivy Default Cache set to: /root/.ivy2/cache
The jars for the packages stored in: /root/.ivy2/jars
org.apache.iceberg#iceberg-spark-runtime-3.4_2.12 added as a dependency
software.amazon.s3tables#s3-tables-catalog-for-iceberg-runtime added as a dependency
org.apache.hadoop#hadoop-aws added as a dependency
software.amazon.awssdk#s3 added as a dependency
software.amazon.awssdk#sts added as a dependency
software.amazon.awssdk#dynamodb added as a dependency
software.amazon.awssdk#glue added as a dependency
software.amazon.awssdk#kms added as a dependency
software.amazon.awssdk#url-connection-client added as a dependency
:: resolving dependencies :: org.apache.spark#spark-submit-parent-c115f731-564b-4272-b24a-e275d986dd4c;1.0
        confs: [default]
        found org.apache.iceberg#iceberg-spark-runtime-3.4_2.12;1.6.1 in central
        found software.amazon.s3tables#s3-tables-catalog-for-iceberg-runtime;0.1.3 in central
        found org.apache.hadoop#hadoop-aws;3.3.4 in central
        found com.amazonaws#aws-java-sdk-bundle;1.12.262 in central
        found org.wildfly.openssl#wildfly-openssl;1.0.7.Final in central
        found software.amazon.awssdk#s3;2.20.68 in central
        found software.amazon.awssdk#aws-xml-protocol;2.20.68 in central
        found software.amazon.awssdk#aws-query-protocol;2.20.68 in central
        found software.amazon.awssdk#protocol-core;2.20.68 in central
        found software.amazon.awssdk#sdk-core;2.20.68 in central
        found software.amazon.awssdk#annotations;2.20.68 in central
        found software.amazon.awssdk#http-client-spi;2.20.68 in central
        found software.amazon.awssdk#utils;2.20.68 in central
        found org.reactivestreams#reactive-streams;1.0.3 in central
        found org.slf4j#slf4j-api;1.7.30 in central
        found software.amazon.awssdk#metrics-spi;2.20.68 in central
        found software.amazon.awssdk#endpoints-spi;2.20.68 in central
        found software.amazon.awssdk#profiles;2.20.68 in central
        found software.amazon.awssdk#aws-core;2.20.68 in central
        found software.amazon.awssdk#regions;2.20.68 in central
        found software.amazon.awssdk#json-utils;2.20.68 in central
        found software.amazon.awssdk#third-party-jackson-core;2.20.68 in central
        found software.amazon.awssdk#auth;2.20.68 in central
        found software.amazon.eventstream#eventstream;1.0.1 in central
        found software.amazon.awssdk#arns;2.20.68 in central
        found software.amazon.awssdk#crt-core;2.20.68 in central
        found software.amazon.awssdk#apache-client;2.20.68 in central
        found org.apache.httpcomponents#httpclient;4.5.13 in central
        found org.apache.httpcomponents#httpcore;4.4.13 in central
        found commons-logging#commons-logging;1.2 in central
        found commons-codec#commons-codec;1.15 in central
        found software.amazon.awssdk#netty-nio-client;2.20.68 in central
        found io.netty#netty-codec-http;4.1.86.Final in central
        found io.netty#netty-common;4.1.86.Final in central
        found io.netty#netty-buffer;4.1.86.Final in central
        found io.netty#netty-transport;4.1.86.Final in central
        found io.netty#netty-resolver;4.1.86.Final in central
        found io.netty#netty-codec;4.1.86.Final in central
        found io.netty#netty-handler;4.1.86.Final in central
        found io.netty#netty-transport-native-unix-common;4.1.86.Final in central
        found io.netty#netty-codec-http2;4.1.86.Final in central
        found io.netty#netty-transport-classes-epoll;4.1.86.Final in central
        found software.amazon.awssdk#sts;2.20.68 in central
        found software.amazon.awssdk#dynamodb;2.20.68 in central
        found software.amazon.awssdk#aws-json-protocol;2.20.68 in central
        found software.amazon.awssdk#glue;2.20.68 in central
        found software.amazon.awssdk#kms;2.20.68 in central
        found software.amazon.awssdk#url-connection-client;2.20.68 in central
:: resolution report :: resolve 4179ms :: artifacts dl 136ms
        :: modules in use:
        com.amazonaws#aws-java-sdk-bundle;1.12.262 from central in [default]
        commons-codec#commons-codec;1.15 from central in [default]
        commons-logging#commons-logging;1.2 from central in [default]
        io.netty#netty-buffer;4.1.86.Final from central in [default]
        io.netty#netty-codec;4.1.86.Final from central in [default]
        io.netty#netty-codec-http;4.1.86.Final from central in [default]
        io.netty#netty-codec-http2;4.1.86.Final from central in [default]
        io.netty#netty-common;4.1.86.Final from central in [default]
        io.netty#netty-handler;4.1.86.Final from central in [default]
        io.netty#netty-resolver;4.1.86.Final from central in [default]
        io.netty#netty-transport;4.1.86.Final from central in [default]
        io.netty#netty-transport-classes-epoll;4.1.86.Final from central in [default]
        io.netty#netty-transport-native-unix-common;4.1.86.Final from central in [default]
        org.apache.hadoop#hadoop-aws;3.3.4 from central in [default]
        org.apache.httpcomponents#httpclient;4.5.13 from central in [default]
        org.apache.httpcomponents#httpcore;4.4.13 from central in [default]
        org.apache.iceberg#iceberg-spark-runtime-3.4_2.12;1.6.1 from central in [default]
        org.reactivestreams#reactive-streams;1.0.3 from central in [default]
        org.slf4j#slf4j-api;1.7.30 from central in [default]
        org.wildfly.openssl#wildfly-openssl;1.0.7.Final from central in [default]
        software.amazon.awssdk#annotations;2.20.68 from central in [default]
        software.amazon.awssdk#apache-client;2.20.68 from central in [default]
        software.amazon.awssdk#arns;2.20.68 from central in [default]
        software.amazon.awssdk#auth;2.20.68 from central in [default]
        software.amazon.awssdk#aws-core;2.20.68 from central in [default]
        software.amazon.awssdk#aws-json-protocol;2.20.68 from central in [default]
        software.amazon.awssdk#aws-query-protocol;2.20.68 from central in [default]
        software.amazon.awssdk#aws-xml-protocol;2.20.68 from central in [default]
        software.amazon.awssdk#crt-core;2.20.68 from central in [default]
        software.amazon.awssdk#dynamodb;2.20.68 from central in [default]
        software.amazon.awssdk#endpoints-spi;2.20.68 from central in [default]
        software.amazon.awssdk#glue;2.20.68 from central in [default]
        software.amazon.awssdk#http-client-spi;2.20.68 from central in [default]
        software.amazon.awssdk#json-utils;2.20.68 from central in [default]
        software.amazon.awssdk#kms;2.20.68 from central in [default]
        software.amazon.awssdk#metrics-spi;2.20.68 from central in [default]
        software.amazon.awssdk#netty-nio-client;2.20.68 from central in [default]
        software.amazon.awssdk#profiles;2.20.68 from central in [default]
        software.amazon.awssdk#protocol-core;2.20.68 from central in [default]
        software.amazon.awssdk#regions;2.20.68 from central in [default]
        software.amazon.awssdk#s3;2.20.68 from central in [default]
        software.amazon.awssdk#sdk-core;2.20.68 from central in [default]
        software.amazon.awssdk#sts;2.20.68 from central in [default]
        software.amazon.awssdk#third-party-jackson-core;2.20.68 from central in [default]
        software.amazon.awssdk#url-connection-client;2.20.68 from central in [default]
        software.amazon.awssdk#utils;2.20.68 from central in [default]
        software.amazon.eventstream#eventstream;1.0.1 from central in [default]
        software.amazon.s3tables#s3-tables-catalog-for-iceberg-runtime;0.1.3 from central in [default]
        ---------------------------------------------------------------------
        |                  |            modules            ||   artifacts   |
        |       conf       | number| search|dwnlded|evicted|| number|dwnlded|
        ---------------------------------------------------------------------
        |      default     |   48  |   0   |   0   |   0   ||   48  |   0   |
        ---------------------------------------------------------------------
:: retrieving :: org.apache.spark#spark-submit-parent-c115f731-564b-4272-b24a-e275d986dd4c
        confs: [default]
        0 artifacts copied, 48 already retrieved (0kB/82ms)
24/12/07 07:35:08 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
24/12/07 07:35:20 WARN Utils: Service 'SparkUI' could not bind on port 4040. Attempting port 4041.
Spark context Web UI available at http://02f5797f4c70:4041
Spark context available as 'sc' (master = local[*], app id = local-1733556920161).
Spark session available as 'spark'.
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 3.4.4
      /_/

Using Scala version 2.12.17 (OpenJDK 64-Bit Server VM, Java 17.0.13)
Type in expressions to have them evaluated.
Type :help for more information.

scala>
```
</details>

以下が表示されたら起動成功です。

```bash
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 3.4.4
      /_/

Using Scala version 2.12.17 (OpenJDK 64-Bit Server VM, Java 17.0.13)
Type in expressions to have them evaluated.
Type :help for more information.

scala>
```

先ほど説明したようにテーブルバケット配下には先に名前空間を作成する必要があるので、次のSQLで対象のテーブルに名前空間を作成します。
[命名規則](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-buckets-naming.html#naming-rules-table)に従う必要がありますが、小文字、大文字、数字、アンダースコアを使っていれば問題ないと思います。

```bash
> spark.sql("""CREATE NAMESPACE IF NOT EXISTS [構築したテーブルバケット名].[名前空間名]""")
```

名前空間ができたらCREATE TABLEでテーブルを作ります。
このあたりは通常のSQLと変わりません。

```bash
> spark.sql("""CREATE TABLE IF NOT EXISTS [構築したテーブルバケット名].[名前空間名].[テーブル名]
 ([カラム名] [データ型]
  """)
```

テーブルができたらINSERT文でデータを投入してみます。

```bash
> spark.sql("""INSERT INTO [構築したテーブルバケット名].[名前空間名].[テーブル名]
  VALUES
  ([データ値])
  """)
```

データ投入ができたら、まずはSpark SQLを使用してSELECT文で投入データが取得できるか確認します。

```bash
> spark.sql(""" SELECT * FROM [構築したテーブルバケット名].[名前空間名].[テーブル名] """).show()
```

ここでも私の環境で実行したコマンドとログを参考までに貼っておきます。
<details><summary>実行コマンドとログ</summary>

```bash
scala> spark.sql("""CREATE NAMESPACE IF NOT EXISTS qiitatablebucket.test_ns""")
res0: org.apache.spark.sql.DataFrame = []

scala> spark.sql("""CREATE TABLE IF NOT EXISTS qiitatablebucket.test_ns.sample_table
     |  (id INT,
     |   name STRING,
     |   value INT)
     |   USING iceberg
     |   """)
res1: org.apache.spark.sql.DataFrame = []

scala> spark.sql("""INSERT INTO qiitatablebucket.test_ns.sample_table
     |   VALUES
     |   (1, 'Jeff', 100),
     |   (2, 'Carmen', 200),
     |   (3, 'Stephen', 300),
     |   (4, 'Andy', 400),
     |   (5, 'Tina', 500),
     |   (6, 'Bianca', 600),
     |   (7, 'Grace', 700)
     |   """)
res2: org.apache.spark.sql.DataFrame = []

scala> spark.sql(""" SELECT * FROM qiitatablebucket.test_ns.sample_table """).show()
+---+-------+-----+
| id|   name|value|
+---+-------+-----+
|  1|   Jeff|  100|
|  2| Carmen|  200|
|  3|Stephen|  300|
|  4|   Andy|  400|
|  5|   Tina|  500|
|  6| Bianca|  600|
|  7|  Grace|  700|
+---+-------+-----+
```
</details>

これでデータの投入は完了です。
マネジメントコンソールを確認すると作成した名前空間とテーブルが表示されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/d82059a3-7801-60a3-90e1-2320e9a1210b.png)

## AthenaとRedshiftからクエリしてみる
せっかくデータ投入まで実施したので、プレビューリリースされているAWS分析サービスとの統合とクエリを試してみます。

S3 Tablesコンソールを表示したときに出てきていた以下の部分から統合を有効化します。
注意点として、これを統合するとLakeFormationを利用したアクセス管理されることになります。
現在LakeFormationを利用しておらず、他の方法でアクセス管理を行っているという方は影響が出る可能性もあるので、それを認識の上有効化してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/8677365e-8b94-3681-c9fa-8ff61f9b2ea4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/c1a57494-b69f-c517-b06b-5ca2eba0835d.png)

有効化すると次のような表示に変わります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/b84d790f-2850-0b58-e89d-4fef41b19a2b.png)

有効化されるとs3tablescatalog経由でAWS Glue Data Catalogからテーブルバケット、名前空間、テーブルにアクセスできるようになります。

S3 TablesとAWS Glue Catalogの対応表は以下になります。
少しわかりづらいですが、S3 TablesでいうところのテーブルバケットがGlue Data Catalogのカタログ、名前空間がGlueのデータベース、テーブルがGlueのテーブルとなっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/354435cc-58d2-49e2-1447-8be63eb078b7.png)
https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-aws.html#how-table-integration-works

クエリするための準備作業の[前提条件](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-aws.html#table-integration-prerequisites)がいくつかあり、作業者にLakeFormationやGlueの権限を付与する必要があります。
今回は検証なのでAdministratorAccessを付与したユーザーで作業してます。

### Amazon Athenaからのクエリ
Athenaの方が設定が楽なので、まずはAthenaからクエリしてみます。
以下にAthena利用時のドキュメントが記載されていますが、記載が不足している部分があります。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-athena.html

まずLakeFormationコンソールから`Data Catalog＞Catalogs＞s3tablescatalog`を表示して、先ほど作成したqiitatablebucketバケットが自動で登録されているか確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/7416d4ec-9e35-ac4b-38ad-c9ee1ca45e06.png)

その後テーブルバケットを選択してアクションから`Grant`を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/17b84086-1b7e-0f26-8c51-21920d3fbb48.png)

Athenaからクエリする作業者のプリンシパルを指定し、先ほど作成した名前空間(＝データベース)を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/d091d4ff-0230-3a64-f3fd-3a81cde03ba0.png)

データベース権限としてSuper権限にチェックを付けて`Grant`を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/2e5f149f-e056-ccab-1155-3d0a6e2231d5.png)

同じ要領でテーブルレベルの権限の付与も以下の通り行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/5876fd74-a2b1-e4d1-fb66-6e65a8ee9d0e.png)

以下のようにデータベースレベルとテーブルレベルの権限が付与されると思います。
※表示されていなければ一覧を更新すると出てくると思います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/f5ca34ff-576a-06f6-50ee-ef7957774347.png)

権限を付与した作業者でAthena Query editorから以下のSQLを実行すると、S3 Tablesに対してクエリができます。

```sql
SELECT * FROM "s3tablescatalog/[構築したテーブルバケット名]"."[構築した名前空間]"."[作成したテーブル名]"
```

私の環境だと以下のSQLになります。

```sql
SELECT * FROM "s3tablescatalog/qiitatablebucket"."test_ns"."sample_table"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/6f694fa8-61f7-ebad-681a-f5df33853c77.png)

これでAthenaからのクエリは完了です。

### Amazon Redshiftからのクエリ
次にRedshiftからのクエリを行います。

今回は既に構築済みのRedshiftクラスター(ra3.large)を利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/85190785-5af8-5566-9b5e-2715f4c94105.png)

Redshiftからのクエリも一応ドキュメントに記載されていますが、Athena同様に記載通り設定しても上手くいかない部分があるので、私が検証した内容を書いていきます。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-redshift.html

Redshiftからクエリするために必要な作業は次の2つです。
※上記ドキュメントには他の作業も記載されていますが、既に実施済みの作業は省略しています。

1. 名前空間へのLakeFormationリソースリンクを作成する
2. クエリ実行者のプリンシパルに対してLakeFormationリソースリンクへのアクセス権限を付与する

#### LakeFormationリソースリンクの作成
S3 Tablesに対して分析サービスからアクセスを行う場合、Redshift、Data Firehose、EMRについてはリソースリンクを作成する必要があります。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-aws.html#database-link-tables

リソースリンクとは、LakeFormationで異なるAWSアカウントや別リージョンのデータベースやテーブルに対してアクセスするためのエイリアスのようなものを設定できる機能になります。
この機能を利用してs3tablescatalogとの紐付けを行います。

https://docs.aws.amazon.com/lake-formation/latest/dg/resource-links-about.html

リソースリンクはコンソールからだとLakeFormationの`Databases＞Create database`から作成できます。
設定項目は次のようなものを入力します。

- Resource link name：`[S3 Tabales名前空間と紐付けるリンク名]`
- Shared catalog：`[AWSアカウントID]:s3tablescatalog/[構築したテーブルマケット名]`
- Shared database：`[作成した名前空間]`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/6618db8b-20c5-00ac-291e-3fdb3b41801a.png)

次のコマンドでも作成できます。

```bash
$ aws glue create-database --region us-east-1 --catalog-id "123456789123" --database-input \
'{
  "Name": "test_ns_link",
  "TargetDatabase": {
    "CatalogId": "123456789123:s3tablescatalog/qiitatablebucket",
    "DatabaseName": "test_ns"
  },
  "CreateTableDefaultPermissions": []
}'
```

#### リソースリンクを対象としたアクセス権限の付与
後はAthena同様にアクセス権限をデータベースとテーブルの2つ分をクエリする作業者ユーザーに付与します。

まずはデータベース権限です。以下の通り設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/8b1ba3c5-dfad-93a1-6874-a661a542f375.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/793fac78-23cd-fc9b-b4c0-4b48e0abaff8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/e3d13f62-5e7b-2ec3-2966-ddef1a78ede4.png)

次にテーブル権限です。
先ほどは`Grant`を選択しましたが、次は`Grant on target`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/52988526-6ef6-20f8-793c-7ff9754b1414.png)

Athenaの時と同じように設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/b8df0312-1002-a6dc-e9c5-87e2de66782d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/43c92d06-298c-7848-992a-f0374b28a72f.png)

#### Redshift側の権限設定・クエリ確認
RedshiftからAWS Glue Data Catalogに対してクエリする場合、外部データベースのawsdatacatalogに対してIAMユーザーがクエリできるように設定が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/b59c2a4e-7c15-b9ab-cc00-11f8c2299d7c.png)

RedshiftのDBユーザー(≠IAMユーザーではない)で以下のコマンドを実行して、Redshift内部の権限設定を行います。

```sql
GRANT USAGE ON DATABASE awsdatacatalog to "IAM:[IAMユーザー名]"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/a78c45df-55a0-4fb0-4f4d-3109403c3890.png)

権限設定後、`Edit connection`からクエリエディタの認証方法をIAMに変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/0a778a2f-2c4e-f9f0-1837-62a8cffaa5cf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/452eef3e-4947-d028-1093-46645618dd85.png)

実行先などを確認した上で以下のSQLを実行するとS3 Tablesにクエリされ、正常にデータが表示されます。

```sql
SELECT * FROM awsdatacatalog.[リソースリンク名].[作成したテーブル名];
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/0024b8e5-3d08-5670-2a13-f6ad631af369.png)

これでRedshiftからのクエリは完了です。

# おわりに
元々S3上でIcebergを利用する構成には興味がありましたが、その学習時間が取れず中々手が付けられてませんでした。
今回マネージドなIcebergテーブル機能としてS3 Talesが出てくれたおかげで、学習に掛けられる時間が少なくて済みそうだったので検証を行ってみました。

re:Inventで発表直後のサービスや機能はドキュメントやコンソール上の操作が上手くいかないことが多いので、今回も中々試行錯誤で時間がかかりました。ただ、そのおかげでそれなりに理解ができたと思います。

S3 Tablesへのクエリ方法はAthenaとRedshift以外にもう3つあるのですが、その中でもData FirehoseはデータベースからのCDCレプリケーションにも利用できるとのことなので、次回はS3 TablesとData Firehoseの連携について検証してみようと思います。

- [Amazon Data Firehose を使用して、データベースから Apache Iceberg テーブルに変更をレプリケート (プレビュー)](https://aws.amazon.com/jp/blogs/news/replicate-changes-from-databases-to-apache-iceberg-tables-using-amazon-data-firehose/)

残りのEMRやQuickSightについては個人的に検証優先度低めなので、他の方にお任せしたいと思います。

この記事がどなたかの参考になれば幸いです。