やりたいこと
AWS から S3Tables が発表されてしばらく経ちましたが、社内のプロジェクトでデータ基盤を構築することになったのでせっかくなら使ってみるかと思い、簡単に触ってみたので記事にしてみます。
S3 Tables とは
Amazon S3 Tables は、S3 上で分析向けに最適化されたテーブル形式ストレージを提供する新サービス。従来の「ファイル管理中心の S3」とは異なり、データを「テーブル」として管理できるのが最大の特徴。
また、OTF (Open Table Format) である Iceberg や Deltalake などを使えるのもメリット。2025年はじめに東京リージョンでも一般提供が開始されている。
今回やってみること
やることはすごくシンプル。Raw テーブルのデータ (CSV) を Bronze テーブルにロードする。
Raw は Glue DataCatalog Table、Bronze は S3Tables です。
準備
サンプルデータ
サンプルデータとして以下リンク先の Titanic Passengers を使ってみる。
View をクリックして、右上の Export から CSV でダウンロードする。
ファイル名が converted-file.csv になっていて分かりづらいので passengers.csv に変更しておく。
リソース作成
S3 などのリソースは Terraform で作成していく。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>6.24.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
locals {
suffix = "s3tables-sample"
table_name = "titanic_passengers"
}
resource "aws_s3_bucket" "sample_data" {
bucket = "sample-data-${local.suffix}"
}
resource "aws_s3_object" "sample_data" {
bucket = aws_s3_bucket.sample_data.id
key = "data/passengers.csv"
source = "../passengers.csv" # DL した CSV のパス
}
resource "aws_glue_catalog_database" "raw" {
name = "raw"
}
resource "aws_glue_catalog_table" "titanic_passengers_raw" {
database_name = aws_glue_catalog_database.raw.name
name = local.table_name
table_type = "EXTERNAL_TABLE"
parameters = {
"EXTERNAL" = "TRUE"
"skip.header.line.count" = "1"
"use.null.for.invalid.data" = "true"
}
storage_descriptor {
location = "s3://${aws_s3_bucket.sample_data.bucket}/data/"
input_format = "org.apache.hadoop.mapred.TextInputFormat"
output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat"
ser_de_info {
name = "OpenCSVSerDe"
serialization_library = "org.apache.hadoop.hive.serde2.OpenCSVSerde"
parameters = {
"separatorChar" = ","
"quoteChar" = "\""
"escapeChar" = "\\"
}
}
columns {
name = "passengerid"
type = "bigint"
}
columns {
name = "survived"
type = "int"
}
columns {
name = "pclass"
type = "int"
}
columns {
name = "name"
type = "string"
}
columns {
name = "sex"
type = "string"
}
columns {
name = "age"
type = "double"
}
columns {
name = "sibsp"
type = "int"
}
columns {
name = "parch"
type = "int"
}
columns {
name = "ticket"
type = "string"
}
columns {
name = "fare"
type = "double"
}
columns {
name = "cabin"
type = "string"
}
columns {
name = "embarked"
type = "string"
}
}
}
resource "aws_s3tables_table_bucket" "example" {
name = "example-bucket-${local.suffix}"
}
resource "aws_s3tables_namespace" "layers" {
for_each = toset(["bronze"])
namespace = each.value
table_bucket_arn = aws_s3tables_table_bucket.example.arn
}
resource "aws_s3tables_table" "titanic_passengers_bronze" {
name = "titanic_passengers"
namespace = aws_s3tables_namespace.layers["bronze"].namespace
table_bucket_arn = aws_s3tables_table_bucket.example.arn
format = "ICEBERG"
metadata {
iceberg {
schema {
field {
name = "passenger_id"
type = "long"
required = true
}
field {
name = "survived"
type = "int"
required = false
}
field {
name = "pclass"
type = "int"
required = false
}
field {
name = "name"
type = "string"
required = false
}
field {
name = "sex"
type = "string"
required = false
}
field {
name = "age"
type = "double"
required = false
}
field {
name = "sibsp"
type = "int"
required = false
}
field {
name = "parch"
type = "int"
required = false
}
field {
name = "ticket"
type = "string"
required = false
}
field {
name = "fare"
type = "double"
required = false
}
field {
name = "cabin"
type = "string"
required = false
}
field {
name = "embarked"
type = "string"
required = false
}
field {
name = "ingest_date"
type = "string"
required = true
}
}
}
}
}
resource "aws_athena_named_query" "load_to_s3tables" {
name = "Load Titanic Passengers to S3Tables"
workgroup = "primary"
database = aws_glue_catalog_database.raw.name
query = <<EOF
INSERT INTO "bronze"."titanic_passengers"
SELECT
passengerid,
survived,
pclass,
name,
sex,
age,
sibsp,
parch,
ticket,
fare,
cabin,
embarked,
date_format(current_timestamp AT TIME ZONE 'Asia/Tokyo', '%Y-%m-%d %H:%i:%s.%f') AS ingest_date
FROM "AwsDataCatalog"."raw"."titanic_passengers"
EOF
}
本当は LakeFormation の設定も Terraform でやりたかったけど未対応みたい。
https://github.com/hashicorp/terraform-provider-aws/pull/43931
resource "aws_lakeformation_permissions" "athena_raw_database" {
principal = "arn:aws:iam::{account_id}:user/{user_principal_name}"
permissions = [
"ALL",
]
database {
catalog_id = "{account_id}:s3tablescatalog/${aws_s3tables_table_bucket.example.name}"
name = aws_glue_catalog_database.raw.name
}
}
terraform apply でリソースを作成すると、各リソースが作成されているはず。
データ処理
Athena にアクセスして、まずは Raw テーブルのデータを確認してみる。
データソース: AwsDataCatalog
カタログ: なし
データベース: raw
を選択すると Terraform で作成しておいたテーブルが表示されているはず。
メニュー (⋮) からテーブルをプレビューしてみる。CSV データがロードされている状態。
Bronze テーブルにデータをロードしてみる。
上部のタブから「保存したクエリ」を選択すると、Terraform で作成しておいたクエリがあるのでクリック。エディタにクエリが読み込まれる。
データソース: AwsDataCatalog
カタログ: s3tablescatalog/example-bucket-s3tables-sample
データベース: bronze
を選択して「実行」をクリック。
この時点ではエラーになる。理由は対象のテーブルへのアクセス権がないことなので、LakeFormation でアクセス権の設定をしていく。
アクセス設定
本来は Terraform で権限設定もしたかったが、未対応とのことなので手動で行う。
LakeFormation にアクセスし、「Data Permissions」をクリック。
右上の「Grant」をクリックして設定する。
- Principals で自身の IAM ユーザを選択する
- LF-Tags or catalog resources では以下を選択する
- Named Data Catalog resources を選択
- Catalogs: {account_id}/s3tablescatalog/example-bucket-s3tables-sample
- Databases: bronze
- Tables: titanic_passengers
- Table permissions では以下を選択
- Table permissions: Super
元の画面に戻り、bronze へのアクセス設定が1行増えているはず。
再度データ処理
改めて先ほどのクエリを実行してみると、今回は成功するはず。
raw と同様にテーブルをプレビューしてみる。raw のデータが時刻付きでロードされている![]()
![]()
![]()
ということで
非常に簡単ですが S3Tables を使って ETL 的なことをしてみました。
元々はもっと早く記事にする予定だったのですが、業務に追われているうちにアドベントカレンダーの時期になり Iceberg のテーマとして公開しました。
内容的に Iceberg らしさみたいなところはまったくないのですが AWS で S3Tables を使う人の参考になれば嬉しいです!
以上です。


