Migrate Between Object Storage Providers
Overview
Ilum supports several S3-compatible object storage backends as the data plane: the bundled RustFS (default since 6.7.2-RC2), the bundled MinIO (opt-in), and any external S3-compatible service such as AWS S3, Wasabi, or Backblaze B2. Switching between them is performed entirely through Helm values. The procedure below is the canonical migration playbook. The most-traveled path, MinIO to RustFS, is documented in detail; the Other migration recipes section at the end covers the reverse path and migrations involving external providers.
A pre-upgrade hook protects existing installs from accidental data loss, so the safe path is to make a deliberate choice rather than to upgrade with no overrides.
- Object Storage Overview for the alias model and resolution rules.
- Choose a Provider for the comparison matrix.
- Troubleshoot Object Storage for recovery recipes if a cutover goes wrong.
How migration works
Migration in Ilum is parameterized: the same playbook covers any source-and-target pair. Three properties of the chart make this work:
- Provider-agnostic alias. A stable Service named
ilum-objectstorageis provisioned as a label-selector alias that routes to whichever provider is currently active. Bundled consumers target this alias rather than provider-specific Service names. Switching providers does not require consumer reconfiguration. - Shared credentials Secret. A single
Secret,ilum-objectstorage-credentials, carries the S3 root credentials used by every consumer. The Secret exposes six aliased keys (access-key,secret-key,root-user,root-password,RUSTFS_ACCESS_KEY,RUSTFS_SECRET_KEY) so each consumer can read the same value under its native env-var name. For rotation, refer to Rotate Object Storage Credentials. - Pre-upgrade safety hook. A
pre-upgradeHelm hook detects an existing MinIOPersistentVolumeClaimand refuses to proceed when disabling MinIO would orphan the PVC without an acknowledged cutover. The hook is gated behindpreUpgradeChecks.enabled(defaulttrue).
For the release-by-release record of when each property landed, refer to the Upgrade Notes. For the upstream chart status and known caveats of each provider, refer to the per-provider reference pages linked from the Object Storage Overview.
The cutover model
The migration playbook is parameterized by three Helm values. Operators running 6.7.2-RC2 or later should prefer the new names; the legacy flag from earlier releases continues to work as a back-compat alias.
| Helm value | Type | Purpose |
|---|---|---|
objectStorage.activeProvider | string | Explicit override. Set to a provider name (rustfs, minio, ...) to pin the alias to that provider. Defaults to auto, which defers to the resolution rules. |
objectStorage.previousProvider | string | Names the data-bearing side during a cutover when two providers are enabled and activeProvider=auto. Defaults to minio. |
objectStorage.cutoverAcknowledged | bool | Flips the alias from previousProvider to the other enabled provider once the operator has finished migrating data. Defaults to false. |
rustfs.migrationAcknowledged | bool | Legacy name from 6.7.2-RC2 onward; honored as an alias for objectStorage.cutoverAcknowledged. Still accepted; deprecated in favour of the new name in a future major release. |
Both objectStorage.cutoverAcknowledged=true and the legacy
rustfs.migrationAcknowledged=true produce the same result. The
examples below use the legacy flag where it matches the existing
MinIO to RustFS migration path. Subsequent migrations between
other providers should prefer the generalized objectStorage.cutoverAcknowledged
name.
Choose a path
- Path A — Keep MinIO. Set two Helm flags. No data movement. No reconfiguration of consumers.
- Path B — Migrate to RustFS. Run both providers side by side, mirror data, then disable MinIO. Either automated or manual.
- Path C — Net-new install. Nothing to do. RustFS is the default; the alias Service is created automatically.
Path A: Keep MinIO
Run a single helm upgrade with two explicit flags. The pre-upgrade hook accepts the upgrade because MinIO remains enabled.
helm upgrade ilum ilum/helm_aio \
--set minio.enabled=true \
--set rustfs.enabled=false
The ilum-objectstorage Service alias is rendered with a selector matching MinIO pods, so consumer traffic continues to reach MinIO without any further reconfiguration.
Path B: Migrate to RustFS
The migration runs both providers concurrently, mirrors bucket contents, and then disables MinIO. Either an automated migration Job or the manual mc mirror procedure can be used.
- Automated migration Job (recommended)
- Manual mc mirror
The bundled migration Job is gated on migration.minioToRustfs.enabled. It refuses to render unless both minio.enabled and rustfs.enabled are true.
Step 1. Run both providers and acknowledge the cutover.
helm upgrade ilum ilum/helm_aio \
--set minio.enabled=true \
--set rustfs.enabled=true \
--set rustfs.migrationAcknowledged=true
Step 2. Dry-run the migration. The Job invokes mc mirror --fake and reports what would be copied without writing.
helm upgrade ilum ilum/helm_aio \
--set minio.enabled=true \
--set rustfs.enabled=true \
--set rustfs.migrationAcknowledged=true \
--set migration.minioToRustfs.enabled=true \
--set migration.minioToRustfs.dryRun=true
Step 3. Inspect the Job logs.
kubectl -n ilum logs -l app.kubernetes.io/component=migration --tail=-1
Step 4. Run the migration for real.
helm upgrade ilum ilum/helm_aio \
--set minio.enabled=true \
--set rustfs.enabled=true \
--set rustfs.migrationAcknowledged=true \
--set migration.minioToRustfs.enabled=true
kubectl -n ilum wait job -l app.kubernetes.io/component=migration \
--for=condition=complete --timeout=600s
Step 5. Verify (see the next section), then disable MinIO.
helm upgrade ilum ilum/helm_aio \
--set rustfs.migrationAcknowledged=true
When the bundled Job is not desirable, the same outcome can be reached with the mc CLI from any pod that can reach both Services.
Step 1. Run both providers without the migration Job.
helm upgrade ilum ilum/helm_aio \
--set minio.enabled=true \
--set rustfs.enabled=true \
--set rustfs.migrationAcknowledged=true
Step 2. Open a shell with the mc CLI and configure aliases. The credentials are read from the shared ilum-objectstorage-credentials Secret rather than hard-coded.
ACCESS_KEY=$(kubectl -n ilum get secret ilum-objectstorage-credentials -o jsonpath='{.data.access-key}' | base64 -d)
SECRET_KEY=$(kubectl -n ilum get secret ilum-objectstorage-credentials -o jsonpath='{.data.secret-key}' | base64 -d)
kubectl -n ilum run mc --rm -it --restart=Never \
--image=minio/mc:RELEASE.2025-04-16T18-13-26Z \
--env="MC_ACCESS=$ACCESS_KEY" --env="MC_SECRET=$SECRET_KEY" -- sh
mc alias set src http://ilum-minio:9000 "$MC_ACCESS" "$MC_SECRET"
mc alias set dst http://ilum-rustfs-svc:9000 "$MC_ACCESS" "$MC_SECRET"
Step 3. Mirror each default bucket. Repeat the command for every bucket in objectStorage.defaultBuckets.
for bucket in ilum-files ilum-data ilum-tables ilum-mlflow ilum-kestra ilum-ducklake ilum-langfuse; do
mc mb --ignore-existing dst/$bucket
mc mirror --preserve src/$bucket dst/$bucket
done
Step 4. Verify with mc diff (see the next section).
Step 5. Disable MinIO.
helm upgrade ilum ilum/helm_aio \
--set minio.enabled=false \
--set rustfs.migrationAcknowledged=true
Verify the cutover
| Check | Command | Expected |
|---|---|---|
| Object Storage view loads in UI | navigate to /object-storage | iframe renders the RustFS console |
| Bucket parity | mc diff src/<bucket> dst/<bucket> | empty output for every default bucket |
| Spark write | submit a job that writes to s3a://ilum-files/probe/ | object visible in the RustFS pod |
| Default buckets present | mc ls dst/ | seven default buckets |
| MinIO PVC retained | kubectl get pvc -n ilum | grep ilum-minio | PVC still exists until manually deleted |
Migration-window operational FAQ
Can consumers continue writing while mc mirror is running?
Yes. Before objectStorage.cutoverAcknowledged is set to true, the
ilum-objectstorage Service alias still targets the source provider,
so every consumer continues to write there. Objects written to the
source after the first mc mirror pass started may be missed by that
pass. The recommended pattern is therefore to run the mirror twice: a
first pass during normal operation, then a brief read-only window
(quiesce schedulers and Spark jobs, scale long-running consumers to
zero) during which a second mc mirror run finalizes the copy.
What happens if the migration Job fails mid-run?
The bundled migration Job is configured with backoffLimit: 0. A
failure exits the Job and leaves whichever buckets had completed in
their post-mirror state on the destination. Re-running the Job is
safe: mc mirror --preserve is idempotent and resumes from the
existing destination state. Delete the failed Job before the next
helm upgrade if the Job's name would otherwise conflict:
kubectl -n ilum delete job -l app.kubernetes.io/component=migration
How do I verify the mirror was complete?
mc diff src/<bucket> dst/<bucket> returns no output when source and
destination are byte-identical. Run it for every bucket in
objectStorage.defaultBuckets:
for bucket in ilum-files ilum-data ilum-tables ilum-mlflow ilum-kestra ilum-ducklake ilum-langfuse; do
echo "=== $bucket ==="
mc diff src/$bucket dst/$bucket
done
Empty output for every bucket means the cutover is safe. Any objects
listed by mc diff should be reconciled (typically by running
mc mirror again) before acknowledging the cutover.
Rollback
Helm preserves PVCs across helm upgrade and helm rollback by default, so the original MinIO data is recoverable as long as its PVC has not been manually deleted. Re-enable MinIO and disable RustFS:
helm upgrade ilum ilum/helm_aio \
--set minio.enabled=true \
--set rustfs.enabled=false \
--set rustfs.migrationAcknowledged=true
Consumer traffic returns to MinIO automatically because the ilum-objectstorage Service alias re-targets MinIO pods.
Other migration recipes
The MinIO to RustFS path above is the canonical example. The same playbook applies to any source-and-target pair. The recipes below cover the three other paths that operators commonly take.
RustFS to MinIO (downgrade or rollback)
The rollback procedure from the Rollback section above is
the supported way to revert from RustFS back to MinIO, provided
the original MinIO PersistentVolumeClaim still exists.
When the original MinIO PVC has been deleted, the procedure becomes symmetric to the MinIO to RustFS playbook:
- Enable both providers:
--set minio.enabled=true --set rustfs.enabled=true --set objectStorage.previousProvider=rustfs. - Run
mc mirrorfromrustfs/<bucket>tominio/<bucket>for every default bucket. - Set
objectStorage.cutoverAcknowledged=trueto flip the alias to MinIO. - Once MinIO is verified, disable RustFS with
--set rustfs.enabled=false.
Migrate to an external S3 provider (AWS S3, Wasabi, Backblaze B2)
Migrating to a managed external backend has no in-cluster Helm install for the target. Only the alias target and the data copy change.
-
Provision the external bucket(s) and an IAM credential pair with read and write access. Make sure the bucket names match
objectStorage.defaultBuckets. -
Update
ilum-objectstorage-credentialsto carry the external credential pair, or create a newSecretand pointobjectStorage.credentials.existingSecretat it. -
Run
mc mirrorfrom the in-cluster source to the external destination using the external provider's S3 endpoint as the destination alias. -
Disable the in-cluster provider and point
objectStorage.endpointat the external service:helm upgrade ilum ilum/helm_aio \
--set minio.enabled=false \
--set rustfs.enabled=false \
--set objectStorage.endpoint=https://s3.us-east-1.amazonaws.com
For provider-specific endpoint examples, refer to Provider Reference: External S3.
Migrate to a non-bundled provider added via the registry
When the target is a provider that Ilum does not yet ship as a
sub-chart (for example, Garage or SeaweedFS), the migration
follows the same shape as the MinIO to RustFS path but the
bundled migration Job does not apply: it is hard-coded for
MinIO to RustFS. The procedure is:
- Provision the target. Stand up the new provider in the release
namespace, with pods carrying labels
app.kubernetes.io/name: <provider>andapp.kubernetes.io/instance: <release>. Follow Add a New Provider for the registry entry and the per-provider Service shape. - Mirror the buckets manually. Use the
mc mirrorrecipe from the Manual mc mirror tab, substituting the destination alias for the new provider's in-cluster Service name. - Flip the alias. Set
objectStorage.activeProvider=<provider>to pin the alias to the new backend explicitly. SkipobjectStorage.cutoverAcknowledged(it applies only to theauto-mode resolution between two enabled providers). - Verify with
mc difffor every default bucket, then disable the source provider once the verification is clean.
Migrate from external S3 back to a bundled provider
To move from a managed external backend to a bundled provider (for example, repatriating storage to keep everything in-cluster), reverse the steps above:
- Enable the target bundled provider (
--set rustfs.enabled=trueor--set minio.enabled=true). Theilum-objectstorageService alias renders automatically. - Run
mc mirrorfrom the external source to the bundled destination (http://ilum-rustfs-svc:9000orhttp://ilum-minio:9000). - Update
objectStorage.endpointto point at the bundled provider's in-cluster Service, or remove the override so the chart resolves the alias automatically.
If the cutover goes wrong
If the Ilum UI returns 502 Bad Gateway after a cutover, the
ilum-objectstorage Service alias most likely lost its endpoints. The
fastest recovery is a helm rollback to the previous release revision.
For the full diagnosis and recovery procedure, refer to
Troubleshoot Object Storage: 502 from the Object Storage view.
Known limitations
- RustFS distributed mode is "under testing" upstream. The bundled defaults configure standalone mode with a single PVC.
- Auto-wiring of Hydra OIDC into RustFS is not yet shipped. Operators that depend on OIDC against the object-storage console should override
rustfs.extraEnvdirectly until the integration stabilizes. - MinIO root credentials are recorded by the MinIO server on first install. Rotating the shared
Secretafter MinIO has been bootstrapped does not change the live MinIO root user. RustFS reads its root credentials from the env on every Pod start, so RustFS supports live rotation.
References
For the upgrade summary and the Helm chart changelog refer to the Upgrade Notes, which mirrors the in-tree helm/CHANGELOG.md.
For the upstream RustFS chart refer to the Artifact Hub listing.
For the mc client reference refer to the MinIO Client documentation.