2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MinIOの代わりにRustFSを使ってみる 2 — DockerからGeoParquet・COG・PMTilesのE2Eまで

2
Posted at

MinIOの代わりにRustFSを使ってみる — DockerからGeoParquet・COG・PMTilesのE2Eまで

はじめに

オンプレミスやローカル環境でS3互換のObject Storageを用意するとき、これまではMinIOを使うことが多かったと思います。

自分もGeoParquet、COG、PMTiles、Zarr、Parquetなどを扱うデータ基盤では、S3互換の保存先としてMinIOを使う構成を考えていました。

ところが2026年現在、MinIOの公開GitHubリポジトリはarchiveされ、Community Editionはsource-only distributionとして案内されています。ライセンスはAGPLv3です。

そこで、MinIOの代替候補として試したのが RustFS です。

RustFSはRustで実装されたS3互換の分散Object Storageで、Apache License 2.0で公開されています。2026年9月16日にはRustFS 1.0.0がリリースされました。

この記事では、前回の記事の製品紹介だけではなく、実際にDockerで起動し、

boto3
  ↓
RustFS
  ↓
GeoParquet / COG / PMTiles
  ↓
DuckDB / FastAPI
  ↓
OpenLayers

まで動かします。

途中でいくつか問題にも当たりました。Docker image tag、Rasterioの共有ライブラリ、bind mountの所有権、Docker network、PMTilesの繰り返し表示などです。最終的には、そのあたりも含めて再実行できる形まで整理しました。


1. RustFSとは

RustFSは、Rustで実装されたS3互換Object Storageです。

主な特徴は次のとおりです。

  • S3互換API
  • Rustで実装
  • Apache License 2.0
  • Docker / Kubernetes対応
  • 分散構成
  • Erasure Coding
  • Web Console
  • IAM
  • TLS
  • Replication
  • Versioning
  • Object Lock
  • OpenTelemetryを利用した監視

アプリケーションから見ると、RustFS専用APIを使うというより、AWS SDKなどのS3クライアントから接続する形になります。

RustFSとS3 API

RustFS公式ドキュメントでも、Pythonではboto3を使い、custom endpointとpath-style addressingを指定する例が紹介されています。


2. なぜMinIOの代替として見るのか

自分が重視したのは「Rustで書かれていること」より、次の点です。

S3 API
  ↓
Storageを交換できる

アプリケーション側を、

Application
    │
    │ S3 API
    ↓
Object Storage

という形にしておけば、保存先がRustFSでもMinIOでもAWS S3でも、変更箇所を小さくできます。

今回の実装でも、設定値は次のように一般化しています。

S3_ENDPOINT=http://rustfs:9000
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_REGION=us-east-1
S3_BUCKET=rustfs-demo

Python側にRustFSClientのような専用クラスを作るのではなく、boto3のS3 clientを使います。


3. S3互換でもAmazon S3そのものではない

RustFSはS3互換ですが、Amazon S3の全APIを完全に実装している、という意味ではありません。

RustFS公式のCompatibility Matrixでは、2026年8月9日時点で、

Implemented tests          455
Lifecycle behavior tests     5

が記載されています。

そのため、移行時は「S3互換だから大丈夫」と考えるより、実際に自分が使うAPIを確認した方が安全です。

今回のr001では、次を実際に確認しました。

CreateBucket
PutObject
GetObject
ListObjectsV2
DeleteObject
Presigned URL

4. 開発環境

今回の構成です。

Ubuntu 24.04系
Docker Engine
Docker Compose
Python 3.13
boto3
DuckDB
GeoPandas
Rasterio
rio-cogeo
PMTiles
FastAPI
OpenLayers

RustFSはS3 APIを9000番、Consoleを9001番で使います。

RustFS開発構成

RustFS公式containerは非rootのUID 10001で動作します。RustFS自身へhost directoryをbind mountする場合は所有権に注意が必要です。

一方、今回のGIS変換用toolsコンテナは、host側のwork/へファイルを出すため、現在ユーザーのUID/GIDで動かしています。


5. RustFS imageはdigestで固定した

最初は、RustFS 1.0.0がリリースされたので、

image: rustfs/rustfs:v1.0.0

としていました。

しかし、このDocker tagは存在せず、manifest unknownになりました。

そこで一度latestをpullし、実際に取得できたRepoDigestを確認してから、検証環境ではdigest固定にしました。

実機で確認したimageは次です。

rustfs/rustfs@sha256:8cc9801755448b71a786705ce76692c77e14936cccd87cf2fc31842e58f4d1ff

latestをそのまま使い続けるより、動作確認できたdigestへ固定した方が、後から同じ環境を再現しやすくなります。


6. r001 — まずS3 APIを確認する

最初の段階ではGIS処理を入れず、RustFSそのものを確認しました。

Python
  ↓
boto3
  ↓
S3 API :9000
  ↓
RustFS

boto3 clientは次の考え方です。

from botocore.config import Config

s3 = boto3.client(
    "s3",
    endpoint_url="http://localhost:9000",
    aws_access_key_id=access_key,
    aws_secret_access_key=secret_key,
    region_name="us-east-1",
    config=Config(
        signature_version="s3v4",
        s3={"addressing_style": "path"},
    ),
)

r001では、Bucket作成からObjectの削除まで一連で実行するsmoke testを用意しました。

./scripts/run_smoke.sh

これが通ったので、次に実際のGISデータへ進みました。


7. r002 — GeoParquet・COG・PMTilesをRustFSへ置く

r002では次の流れを作りました。

GIS E2E全体

GeoJSON / GeoTIFF
        ↓
      変換
   ┌────┼────┐
   ↓    ↓    ↓
GeoParquet  COG  PMTiles
   │       │       │
   └───────┼───────┘
           ↓
         RustFS
           ↓
   ┌───────┼────────┐
   ↓       ↓        ↓
 DuckDB  FastAPI  OpenLayers

サンプルデータはプログラムで生成します。

別途ShapefileやGeoTIFFを用意しなくても、ZIPを展開してそのままE2Eを試せるようにしました。
参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。


8. GeoParquet

ベクタ側は、サンプル領域を4分割したPolygonをGeoJSONとして作ります。

属性値は、確認しやすいように、

12
28
45
67

としています。

その後、GeoPandasでGeoParquetへ変換します。

変換後にはもう一度読み戻し、

  • CRSがあるか
  • Feature数が一致するか

を確認します。

RustFSへuploadした後も、別ディレクトリへdownloadしてSHA-256を照合します。


9. COG

ラスタ側は、PMTilesと同じ範囲になる256×256のGeoTIFFを作ります。

値は単純な平面ではなく、丘陵と勾配を組み合わせたサンプルにしています。

通常GeoTIFF
    ↓
rio-cogeo
    ↓
COG
    ↓
cog_validate(strict=True)

ここで一度、Docker内でRasterioをimportしたところ、

ImportError: libexpat.so.1: cannot open shared object file

になりました。

Python packageだけでは足りず、python:3.13-slim-bookworm側にlibexpat1が必要でした。

その後は、Docker image build時にも主要GIS moduleを実importして、native library不足をE2Eより前に検出するようにしています。


10. PMTiles

今回はE2E確認が目的なので、一般的な多段ズームのタイルピラミッドではなく、z=10の1タイルだけをPMTiles v3へ格納しています。

COG
 ↓
PNG 256×256
 ↓
PMTiles v3

PMTiles v3は先頭に127 byteの固定headerを持っています。

そのため、表示範囲を確認するだけなら、Object全体を取得する必要はありません。

Range: bytes=0-126
        ↓
PMTiles header
        ↓
bounds / minZoom / maxZoom / center

FastAPIの/api/map-metadataでは、この127 byteだけをRustFSからRange GETしています。


11. DuckDBからRustFSを直接読む

GeoParquetは、一度localへdownloadしてDuckDBへ渡すのではなく、DuckDBのhttpfsからRustFSを直接読みます。

RustFS
  ↓ S3 API
DuckDB
  ↓
read_parquet('s3://...')

custom S3 endpointなので、DuckDB側では、

ENDPOINT
URL_STYLE 'path'
USE_SSL false

を設定します。

実機での結果は、

Rows      4
Min value 12
Max value 67

となり、元の4Polygonと一致しました。


12. BrowserへAccess Keyを渡さない

OpenLayersからRustFSへ直接アクセスさせると、credentialの扱いが面倒になります。

そこで今回は、

Browser
   ↓ HTTP Range
FastAPI
   ↓ S3 Range GET
RustFS

という構成にしています。

FastAPIがRange headerを受け取り、boto3のget_object(Range=...)へ渡します。

これならJavaScriptへAccess Key / Secret Keyを埋め込む必要がありません。


13. 最初のOpenLayers画面には問題があった

E2EはPASSしていましたが、最初のWeb画面には問題がありました。

1タイルしか入っていないPMTilesのRasterが、右側一面へ繰り返して描画されていました。

原因は2つありました。

13.1 古いapp.jsがBrowser cacheに残っていた

HTMLだけ新しくなっていて、JavaScriptが古いままという状態が発生しました。

そこで、

<script src="/app.js?v=0.2.12"></script>

のようにbuild versionをURLへ付けました。

Nginx側も、検証用Web assetについては、

Cache-Control: no-store, no-cache, must-revalidate, max-age=0

を返します。

画面にも、

Web build: v0.2.12

を出すようにしました。

これで「本当に新しいJavaScriptが動いているか」を画面で確認できます。

13.2 PMTilesRasterSourceのTileGridがWeb Mercator全域だった

ol-pmtilesのPMTilesRasterSourceへTileGridを渡さないと、Web Mercator全域のXYZ gridが使われます。

今回のarchiveは1タイルだけなので、そのままだと周辺にも同じ表示が続いているように見えます。

ただし、ここで、

ol.tilegrid.createXYZ({ extent: pmtilesExtent })

とするだけではだめでした。

小さなextentをcreateXYZ()へ渡すと、そのextentを基準にz=0のresolutionが計算されるため、標準XYZのz/x/yと合わなくなります。

最終的には、標準Web Mercator gridからoriginとresolutionsを引き継ぎ、extentだけをPMTilesの実範囲へ限定しました。

const defaultGrid = ol.tilegrid.createXYZ({
  maxZoom: archiveMaxZoom,
  tileSize: 256,
});

const pmtilesTileGrid = new ol.tilegrid.TileGrid({
  extent: pmtilesExtent,
  origin: defaultGrid.getOrigin(0),
  resolutions: defaultGrid.getResolutions(),
  tileSize: defaultGrid.getTileSize(0),
  minZoom: archiveMinZoom,
});

さらにLayer側にも、

raster.setExtent(pmtilesExtent);
raster.setVisible(true);

を設定しています。

sourceとlayerの両方で範囲を制限する形です。


14. 表示範囲もデータから決める

地図の初期位置をJavaScriptへ固定値で書くのではなく、PMTilesとGeoParquetの実データから決めます。

PMTiles header bounds ───┐
                         ├─ union
GeoParquet total_bounds ─┘
                         ↓
                  OpenLayers View.fit()

実機で返ったmetadataは次のとおりでした。

{
  "crs": "EPSG:4326",
  "pmtiles": {
    "version": 3,
    "bounds": [139.5703125, 35.4606699, 139.921875, 35.7465122],
    "min_zoom": 10,
    "max_zoom": 10
  },
  "geoparquet": {
    "bounds": [139.5703125, 35.4606699514953, 139.921875, 35.7465122599185],
    "feature_count": 4
  }
}

これをEPSG:3857へ変換し、View.fit()しています。


15. 最終的なOpenLayers画面

最終画面です。

RustFS r002 最終画面

左側で次を確認できます。

接続状態      PASS / rustfs-demo
DuckDB Rows   4
Min value     12
Max value     67
Web build     v0.2.12
PMTiles       z10–10
PM bounds     表示あり
Geo bounds    表示あり
Features      4

PMTilesの疑似カラーRasterは、白いGeoParquetの4分割枠と同じ1タイル範囲だけに表示されています。

以前のように右側一面へ繰り返す表示はなくなりました。


16. Dockerで実際に実行する

展開後、まず環境ファイルを作ります。

chmod +x ./scripts/*.sh
./scripts/generate_env.sh

ここでhostのUID/GIDも.envへ保存します。

次に品質確認です。

./scripts/run_tests_docker.sh

確認しているのは、

Python target alignment
Python compileall
Ruff format
Ruff lint
mypy strict
pytest

です。

その後E2Eを実行します。

./scripts/run_e2e.sh

最終的に、

Map metadata: PASS
OpenLayers fix012 assets/cache: PASS
PMTiles Range: PASS
R002 E2E: PASS

まで出れば、バックエンド側は完了です。

ブラウザは、

RustFS Console : http://localhost:9001
FastAPI docs   : http://localhost:8000/docs
OpenLayers     : http://localhost:8080

です。


17. bind mountの所有権も直した

途中で、E2Eを2回実行すると、

Permission denied

になりました。

原因は、コンテナがwork/へroot所有のファイルを作っていたためです。

最終版では、

HOST_UID
HOST_GID

をgenerate_env.shで取得し、GIS処理用コンテナをhostと同じUID/GIDで動かします。

過去のroot所有ファイルが残っている場合は、reset_workdir.shがDocker側rootで掃除し、その後host UID/GIDへ所有権を戻します。

E2E中にも、

Work ownership: PASS (1000:1000)

を確認します。


18. Docker networkも増え続けないようにした

fixを繰り返していると、Docker Composeのdefault networkが残り、

all predefined address pools have been fully subnetted

になりました。

品質ゲートはネットワーク通信を必要としないので、最終版では、

docker build
  ↓
docker run --network none

で実行します。

E2E側ではRustFS r002系列の未使用networkだけを掃除し、他プロジェクトのnetworkには触れません。


19. 実機で確認できた結果

最終的なE2Eでは次まで確認できました。

RustFS image pull             PASS
Docker network capacity       PASS
Work directory reset          PASS owner=1000:1000
RustFS readiness              PASS
R002 GEO E2E                  PASS
feature_count                 4
COG valid                     True
PMTiles version               3
DuckDB rows                   4
DuckDB min                    12.0
DuckDB max                    67.0
Work ownership                PASS
Map metadata                  PASS
OpenLayers assets/cache       PASS
PMTiles HTTP Range            PASS
R002 E2E                      PASS

品質ゲートでは、実機のfix012で、

Ruff lint    PASS
mypy strict  PASS
pytest       PASS 49 passed

まで確認しています。

ruff format --checkだけ、テストコードの引用符整形が1件残っていましたが、最終配布版ではRuffが表示した差分どおりに修正しています。機能側の変更はありません。


20. 実装の構成

最終版は次の構成に整理しました。

rustfs_minio_replacement_r002_final/
├── compose.yaml
├── Dockerfile
├── Dockerfile.test
├── pyproject.toml
├── README.md
├── VALIDATION.md
├── src/
│   └── rustfs_demo/
├── scripts/
├── tests/
├── web/
├── images/
└── docs/
    ├── QIITA_FINAL.md
    ├── CODE_GUIDE.md
    ├── TESTING.md
    ├── MIGRATION.md
    └── history/

21. RustFSをMinIOの代わりに使えるか

今回の範囲では、

boto3
GeoParquet
COG
PMTiles
DuckDB
FastAPI
OpenLayers

まで、RustFSをS3互換Object Storageとして使えました。

一方、これだけで本番のMinIOをすぐ置き換える、とは考えていません。

RustFS 1.0.0は出たばかりですし、本番ではさらに、

  • 大量小Object
  • 数GB以上のObject
  • Multipart Upload
  • 長時間連続運転
  • Disk障害
  • Node障害
  • Network障害
  • Healing
  • Backup / Restore
  • Upgrade

を確認する必要があります。

今回一番大事だったのは、RustFSをアプリケーションへ直接埋め込むのではなく、S3 APIを境界にしたことです。

この形なら、次の比較もやりやすくなります。

MinIO
  VS
RustFS

次は、同じデータと同じDocker環境で、PUT / GET性能、CPU、Memory、Disk I/O、障害復旧まで比較する予定です。


参考資料

RustFS

MinIO

PMTiles / OpenLayers


2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?