見出し画像

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 のホスト公開ポートは cAdvisor とコンフリクトしているので 8090 にしています。
コンテナ内部では Trino のデフォルト 8080 のまま。

1.2 各コンポーネントの責務

1.3 データフローとポート

CREATE TABLE の例:

  1. Trino が Polaris REST API (:8181/api/catalog/v1/...) にテーブル登録

  2. Polaris が PostgreSQL にメタデータを書き込み

  3. Polaris が RustFS にテーブルメタデータ (metadata.json) を書く

  4. Trino が OAuth トークンを Polaris から取得

  5. 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 点が実質的に必須です:

  1. pathStyleAccess=true (virtual-host-style は DNS 設定が必要で詰む)

  2. s3.region=us-east-1 (RustFS は region を見ないが AWS SDK が要求する)

  3. 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 呼び出し)

実機で動作確認済の設定一式。数字や名前は環境に応じて調整してください。

ここから先は

13,052字
この記事のみ ¥ 500
Amazon Payで支払うと最大2%還元のチャンス! 9/30まで

IBM、Salesforceでの経験を生かして、AI時代で生き抜くためのITアーキテクト像をほのぼの…

スタンダード

¥500 / 月
1ヶ月無料

あなたの支えが、私の心の糧になります。 note の収益はガジェットのレビューや、自費出版に使わせていただきます。