RustFS で Iceberg Lakehouse を構築する: Apache Polaris + Trino 実装ガイド
Apache Iceberg を中核にした小規模データレイクハウスを、RustFS + Apache Polaris (Iceberg REST カタログ) + Trino で構築する手順と、実装中に Claude Code が踏んだ 13 個の落とし穴をまとめます。
今回の前提は次の通りです。
Apache Polaris 1.3.0-incubating
Trino 474
RustFS 1.0.0-alpha.80
Docker Compose
AWS S3 ではなく 非 AWS な S3 互換ストレージ を使うケース特有の問題が多く登場するため、RustFS や MinIO、Apache Ozone などのオンプレ運用を想定している人向けです。
なぜ RustFS か
RustFS は Rust 実装の S3 互換ストレージで、MinIO からフォークしつつ次のような特徴を持ちます:
Apache 2.0 ライセンス — MinIO のソース可用ライセンス (AGPLv3 + 商用) から自由なライセンスへ
Rust 実装によるメモリ安全性と軽量なランタイム
MinIO と同じ S3 API 互換 (同じ SDK / ツールがそのまま使える)
ARM64 ネイティブ対応 (Apple Silicon / 省電力サーバー向き)
オンプレ・エッジ用途を意識した設計
本記事の設定は MinIO でもほぼそのまま動きますが、環境変数名 (RUSTFS_* vs MINIO_*) だけ置き換えれば OK です。
1. アーキテクチャ
1.1 コンポーネント構成

コンテナ内部では Trino のデフォルト 8080 のまま。
1.2 各コンポーネントの責務
1.3 データフローとポート
CREATE TABLE の例:
Trino が Polaris REST API (:8181/api/catalog/v1/...) にテーブル登録
Polaris が PostgreSQL にメタデータを書き込み
Polaris が RustFS にテーブルメタデータ (metadata.json) を書く
Trino が OAuth トークンを Polaris から取得
Trino が RustFS に直接 Parquet ファイルを読み書き
キーポイント: メタデータの「所在情報」は PostgreSQL、メタデータの「中身」は RustFS (オブジェクトストレージ)、実データも RustFS。PostgreSQL が持つのは RustFS オブジェクトへのポインタに過ぎません。
もう一つの要点: Trino → RustFS の S3 API アクセスは 直接 (Polaris を介さない)。Polaris はメタデータ管理だけを担い、データパスにはいません。これにより大量の parquet 読み書きで Polaris がボトルネックになりません。
2. 設計判断
2.1 なぜ Apache Polaris か
Iceberg カタログには複数の選択肢があります。
単一ホストでのオンプレ小規模が想定なら:
クラスタ不要 → Nessie or Polaris
Iceberg REST Spec 準拠が広範な互換性 → Polaris
Git 的な branching が不要 → Polaris でよい
この記事では Polaris を選択しています。
2.2 Credential Vending は無効化
Polaris には「短命 S3 クレデンシャルをクライアント (Trino) に vending する」機能がありますが、これは AWS STS が前提。MinIO/RustFS は STS 非対応のため無効化し、Trino 側に直接 S3 クレデンシャルを設定します。
# Trino catalog properties
iceberg.rest-catalog.vended-credentials-enabled=false
fs.native-s3.enabled=true
s3.aws-access-key=...
s3.aws-secret-key=...
Polaris カタログ作成時には stsUnavailable: true を指定します。
2.3 JDBC 永続化に伴う bootstrap パターン
Polaris のデフォルト設定例 (context7 や一部 blog に載っている短縮例) では POLARIS_BOOTSTRAP_CREDENTIALS 環境変数だけで server が自動 bootstrap するかのように書かれています。これは in-memory persistence 専用の動作です。
JDBC 永続化 (polaris.persistence.type=relational-jdbc) では必ず apache/polaris-admin-tool を先に走らせて schema DDL を実行する必要があります:
postgres (healthy)
↓ service_healthy
polaris-bootstrap (apache/polaris-admin-tool, one-shot, creates schema)
↓ service_completed_successfully
polaris (apache/polaris, server, schema はもう存在する前提で起動)
2.4 JVM メモリは両層で指定
コンテナ化された JVM は cgroup memory を尊重しないケースが多いため、JVM -Xmx と Docker mem_limit の両方を必ず指定 します。片方だけだと OOM killer が他コンテナを巻き込んで停止させるリスクがあります。
2.5 RustFS の運用ポイント
RustFS は MinIO 互換の API を提供するため、AWS CLI や Iceberg の S3FileIO、Trino の fs.native-s3 などが設定変更なしで動きます。ただし Polaris/Trino からの接続では以下の 3 点が実質的に必須です:
pathStyleAccess=true (virtual-host-style は DNS 設定が必要で詰む)
s3.region=us-east-1 (RustFS は region を見ないが AWS SDK が要求する)
AWS_REGION 環境変数を Polaris コンテナに設定 (application.properties 側の polaris.storage.aws.region は効かないケースあり。env vars 一択)
ストレージ側の Ops:
データ配置: RUSTFS_VOLUMES=/data にすべて書かれる。Docker named volume を bind して永続化
バックアップ: 他の S3 バケットに rclone/s3sync でミラー、または RustFS の EC (erasure coding) 機能を使う
容量監視: Prometheus exporter は RustFS 1.0 alpha 段階では限定的。du -sh /data や df を cron で記録、または S3 API で HEAD bucket → x-amz-bucket-size 等を読む
コンソール: :9001 ポート、RUSTFS_ACCESS_KEY/SECRET_KEY でログイン
2.6 バージョン固定 (:latest 禁止)
Apache Polaris は Incubating で API 互換性が保証されていません。Trino も月次リリース。本番では全イメージをタグ固定しています:
apache/polaris:1.3.0-incubating (※ Incubating なので -incubating suffix が必須)
apache/polaris-admin-tool:1.3.0-incubating (サーバーと同一バージョン)
trinodb/trino:474
postgres:17
rustfs/rustfs:1.0.0-alpha.80 (RustFS はまだ alpha、破壊的変更に備えて固定が特に重要)
3. 実装で踏んだ 13 個の落とし穴
実際に動かすまでに遭遇した問題と解決策。どれも Claude Code が公式ドキュメントと Context7 だけでは気づけなかったものなので、このセクションが本記事の実用的な肝です。
3.1 Docker Hub のバージョンタグは -incubating suffix 必須
Docker Hub 上の apache/polaris のバージョンタグは 1.0.0-incubating / 1.1.0-incubating / ... です。apache/polaris:1.3.0 は 存在しない (引けない)。
✗ apache/polaris:1.3.0
✓ apache/polaris:1.3.0-incubating
3.2 apache/polaris:latest も実体は incubating
latest タグは最新の -incubating を指しています (2026-04 時点で 1.3.0-incubating と同一 digest)。バージョン固定のため -incubating suffix 付きの具体的タグを使います。
3.3 JDBC 永続化では admin-tool bootstrap が必須
前述の通り、POLARIS_BOOTSTRAP_CREDENTIALS 環境変数だけでは JDBC スキーマは作られません。下記のエラーで気づきます:
{"error":{"message":"Failed to retrieve polaris entity due to Failed due to
'ERROR: relation \"polaris_schema.entities\" does not exist ...'"}}
apache/polaris-admin-tool を init コンテナで先行させる必要があります。
3.4 admin-tool の bootstrap コマンドは 非冪等
公式ドキュメントは「idempotent」と明記していますが、実機の 1.3.0-incubing では:
1 回目: Realm 'POLARIS' successfully bootstrapped. Bootstrap completed successfully. (exit 0)
2 回目: IllegalArgumentException: already been bootstrapped (exit 3)
デプロイツール (Ansible や Helm) で 2 回目の docker compose up を回すと、init container が exit 3 で落ちて service_completed_successfully 依存が満たさません。私の環境のように Ansible など IaC していると playbook が失敗することになります。
解決: シェルラッパーで already been bootstrapped を検知して exit 0 に正規化します:
polaris-bootstrap:
image: apache/polaris-admin-tool:1.3.0-incubating
entrypoint: ["/bin/sh", "-c"]
command:
- |
set +e
TMP=$$(mktemp)
trap 'rm -f "$$TMP"' EXIT
java -jar /deployments/polaris-admin-tool.jar \
bootstrap -r "$$POLARIS_REALM" \
-c "$$POLARIS_REALM,$$POLARIS_CLIENT_ID,$$POLARIS_CLIENT_SECRET" > "$$TMP" 2>&1
RC=$$?
cat "$$TMP"
[ "$$RC" -eq 0 ] && exit 0
grep -q "already been bootstrapped" "$$TMP" && exit 0
exit "$$RC"
3.5 SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION=true を設定してはいけない
設定すると Polaris はカタログの storageConfigInfo (endpoint / pathStyleAccess) を完全に無視し、AWS SDK デフォルトで S3 接続を試みて 301 redirect エラー になります。
ERROR: 301 "The bucket you are attempting to access must be addressed using the specified endpoint"
AWS SDK debug log を見ると lakehouse.s3.us-east-1.amazonaws.com/52.216.34.186:443 に接続している (本物の AWS S3 に繋ぎに行っている)。
削除するだけで解決。関連して polaris.storage.aws.access-key/secret-key を application.properties に書くのも競合のもとなので削除。
3.6 Polaris サーバーには AWS_* env var が必要
S3 互換ストレージでも、Polaris 自身の S3 クライアントは AWS SDK を使うため、以下の環境変数が必要:
polaris:
environment:
AWS_REGION: us-east-1
AWS_ACCESS_KEY_ID: <your-s3-key>
AWS_SECRET_ACCESS_KEY: <your-s3-secret>
region はダミーで OK (S3 互換側が無視する)。application.properties で polaris.storage.aws.* を重複設定すると credential provider chain が混乱するため、env vars のみに統一 します。
3.7 カタログ作成時は endpoint + endpointInternal の両方を指定
{
"storageConfigInfo": {
"storageType": "S3",
"endpoint": "http://rustfs:9000",
"endpointInternal": "http://rustfs:9000",
"pathStyleAccess": true,
"stsUnavailable": true,
"region": "us-east-1",
"allowedLocations": ["s3://lakehouse/"]
}
}
endpoint: クライアント (Trino, Spark) に公開する S3 URL。Docker ネットワーク内なら http://rustfs:9000、外部ネットワーク経由なら http://<host>:9000 等
endpointInternal: Polaris サーバー自身が使う S3 URL
単一ホストなら両者は同じで OK
クライアント・サーバーで経路が異なる (内部 / 外部 URL) 場合に分ける
3.8 Trino catalog ファイル名が catalog 識別子になる
/etc/trino/catalog/<name>.properties の <name> が SHOW CATALOGS に出る名前になります。
✗ iceberg.properties → SHOW CATALOGS に "iceberg" が表示
✓ lakehouse.properties → SHOW CATALOGS に "lakehouse" が表示
ファイル名は自分のカタログ名を反映させます。
3.9 Trino コンテナの config ディレクトリは 0755 必須
Trino コンテナは UID 1000 (非 root) で動作。bind-mount した /etc/trino/catalog/ ディレクトリがホスト側で 0750 + root:root だと、コンテナ内のユーザーが traverse できず カタログが読まれず SHOW CATALOGS に system しか表示されません (エラーもログにほぼ出ない)。
chmod 0755 /opt/trino/etc/catalog # ディレクトリは 0755
chmod 0644 /opt/trino/etc/catalog/lakehouse.properties # ファイルは 0644
Docker Desktop (Mac/Windows) は UID remap のためこの問題が再現しません。Linux ホストで検証が必須。
3.10 Trino 474 で query.max-total-memory-per-node は defunct
Defunct property 'query.max-total-memory-per-node' (class NodeMemoryConfig) cannot be configured.
古いチュートリアルをコピペすると踏みます。代替は query.max-total-memory (クラスタ合計) か単に削除。
同時に、Web UI の固定ユーザー設定は:
✗ web-ui.authentication.fixed.username=admin # 存在しないプロパティ
✓ web-ui.user=admin
3.11 Trino の DROP TABLE は purgeRequested=true で飛ぶ
Trino は DROP TABLE で S3 ファイルも消しに行く (?purgeRequested=true)。Polaris 側では 2 つの設定が必要:
(A) サーバー feature flag:
# application.properties
polaris.features."DROP_WITH_PURGE_ENABLED"=true
無いと 403 "Unable to purge entity ... To enable this feature, set DROP_WITH_PURGE_ENABLED"。
(B) catalog_admin ロールに grant:
カタログ作成直後に:
PUT /api/management/v1/catalogs/{name}/catalog-roles/catalog_admin/grants
{"grant": {"type": "catalog", "privilege": "CATALOG_MANAGE_CONTENT"}}
無いと 403 "Principal 'root' ... is not authorized for op DROP_TABLE_WITH_PURGE"。
両方必要です (どちらか片方だと別の 403 で落ちる)。
3.12 Trino を Traefik/nginx 経由にすると 406 で弾かれる
リバースプロキシ経由で Trino にアクセスすると:
HTTP 406 Server configuration does not allow processing of the X-Forwarded-For header
Trino の Jetty は X-Forwarded-* をデフォルトで拒否。受け入れる設定を追加:
# config.properties
http-server.process-forwarded=true
3.13 Polaris 固定ユーザー認証は service_admin + catalog_admin だが権限は限定的
bootstrap で作った root principal は、principal role として service_admin と、catalog_admin への割当を持ちますが、CATALOG_MANAGE_CONTENT などは明示付与が必要です (3.11 参照)。
公式 RustFS guide はこの grant 追加を 別ステップ で行っており、多くの example docker-compose にはこのステップが抜けているため、CREATE/SELECT は通るが DROP で落ちるという症状になります。
4. 実装 (Docker Compose + API 呼び出し)
実機で動作確認済の設定一式。数字や名前は環境に応じて調整してください。
ここから先は
あなたの支えが、私の心の糧になります。 note の収益はガジェットのレビューや、自費出版に使わせていただきます。
