Overview
This article walks the installer through migrating LILT object storage from an in-cluster MinIO deployment to an Amazon S3 bucket. It applies when moving an existing self-managed installation onto a fully managed cluster, such as the environment described in Install System (AWS EKS), where S3 replaces MinIO as the object store. Object data is read out of MinIO through its S3-compatible API into a plain directory tree on a staging host, then uploaded to the destination bucket. The procedure is read-only against the source cluster: nothing in the export stage modifies the running MinIO deployment or its volume, so the original environment remains available as a fallback until the migration is accepted. Database migration is a separate procedure and is not covered here.Tools you will need
- AWS user with permissions to read and write the destination S3 bucket, and to use its KMS key if the bucket is encrypted with SSE-KMS.
-
Required utilities for the staging host:
- AWS CLI
-
kubectl, with access to the source cluster
-
A staging host with:
- Free disk space greater than the total size of the object data
-
A case-sensitive filesystem (Linux
ext4orxfs)
Structure of this Article
The first section explains why the MinIO storage volume cannot be copied directly, and how to confirm which on-disk format your deployment uses. The remaining sections cover preparing the destination bucket, exporting the objects, uploading them without overwriting existing data, verifying the result, and the configuration changes required before the application can read from the new bucket.Installer privileges
All commands are run from a staging host by a user with the permissions stated in the “Tools” section above. Commands that target the source cluster assume the LILT namespace islilt; adjust if your installation uses a different namespace.
MinIO On-Disk Format
Why the storage volume cannot be copied directly
Current MinIO releases do not store objects as plain files, even in single-drive deployments. Objects are held in an erasure-coded layout:-
Each object key is a directory containing a metadata file named
xl.meta -
Object data is written into a version-UUID subdirectory as
part.1,part.2, and so on -
Objects smaller than 128 KiB are inlined into
xl.metaand have no separate data file -
Drive-level state is kept in a hidden
.minio.sysdirectory
/ become nested directories under either format, so the directory tree on the volume resembles the output of aws s3 ls. Recognising the prefix structure is not evidence that a direct copy will work.
Confirm the on-disk format
Read the drive format from the MinIO pod. The data volume is mounted at/export:
format field determines which method applies:
-
xl-singleorxl— erasure-coded. Use the procedure in this article. -
fs— a legacy plain-file layout, used only by installations created by much older MinIO releases. A direct copy of the bucket directory is valid in that case; see “Legacy filesystem layout” at the end of this article.
Prepare the Destination
Set the user environment variables
Confirm bucket access and configuration
Record the encryption and versioning settings; they determine which permissions are required and whether an accidental overwrite can be recovered:Required permissions
The uploading principal needs the following. Ifget-bucket-encryption reports aws:kms, the KMS permissions are also required — without them every upload fails with AccessDenied, which is easily mistaken for an S3 policy problem:
Export Objects from MinIO
Stop application writes
An object store copy is only consistent if nothing is writing to it. Scale down the application workloads while leaving the data services running, since MinIO is the source of the export. Record the current replica counts first, so the environment can be restored if the migration is abandoned:Retrieve the MinIO credentials
Read them from the cluster rather than assuming the installation defaults are still in place:Open a connection to the MinIO API
Forward the MinIO service port to the staging host:200 confirms the connection.
Port forwarding tunnels through the Kubernetes API server and can drop during a long transfer. If the connection proves unstable, capture the service definition, expose it as a NodePort for the duration of the migration, connect directly to a node, and restore the original definition afterwards:
Configure the AWS CLI for MinIO
MinIO does not provide per-bucket DNS, so path-style addressing is required. This profile is used only to read the source; the upload uses your normal AWS credentials:TLS-enabled MinIO endpoints
A default installation serves the MinIO API over plain HTTP inside the cluster, which is what the endpoint above assumes. If custom certificates have been applied to MinIO outside of the service mesh, the endpoint must usehttps instead, and certificate verification usually has to be relaxed for the export:
127.0.0.1. --no-verify-ssl is acceptable here since the connection is a local tunnel to a port on the same host.
The alternative is to trust the certificate authority properly and connect by a name the certificate covers, which avoids disabling verification:
curl -k https://127.0.0.1:9000/minio/health/live when the endpoint is served over TLS.
These settings affect only the export from MinIO. Connections to the destination S3 bucket use public AWS certificates, which verify against the system trust store, so no equivalent option is needed and AWS_CA_BUNDLE must not remain set for those commands.
Identify the source bucket. A default installation uses a single bucket for application object storage:
Record the source inventory
These files are the reference for every verification step, including after the upload. Retain them:Check the object key names
Some valid S3 keys cannot be represented as filesystem paths. Check before starting a long transfer rather than after. No output from all four commands is the expected result:/ cannot round-trip through a filesystem at all.
Export to the staging directory
Verify the export format
Confirm the export contains objects rather than MinIO’s internal layout. All four commands must return0. A non-zero result means the storage volume was copied instead of the API contents, and the export cannot be used:
Verify the export is complete
The object count and byte total must match the inventory, and bothcomm commands must produce no output:
--apparent-size with du. Without it, du reports allocated blocks and overstates the total against the byte count S3 reports.
Verify content integrity on a sample
Matching names and sizes do not prove the contents are correct. For objects uploaded in a single part, the MinIO ETag is the MD5 of the content and can be compared directly. Objects uploaded in multiple parts have an ETag ending in-<n> and are skipped by this check:
MISMATCH result means the export must be repeated for those keys before continuing.
Close the connection
Upload Objects to S3
Object keys are preserved exactly. Only the bucket name changes:$STAGE_DIR/<key> maps to s3://$DEST_BUCKET/<key>. Do not introduce an additional prefix at the destination — the application stores these keys in its database, and adding a prefix invalidates every stored reference.
Inventory the destination
The destination bucket may already contain objects that must not be overwritten. Record its contents in detail first. This listing is also the baseline that proves, after the upload, that nothing pre-existing was modified:collisions.txt are skipped by the upload, so the object already in the destination is kept and the exported copy is not migrated. Review the list before continuing: for any colliding key whose contents differ, the source version is silently left behind, and the application will serve the destination version because it references keys rather than contents.
How aws s3 sync decides what to transfer
aws s3 sync does not blindly re-upload everything. The reference documentation states the rule:
A local file will require uploading if the size of the local file is different than the size of the S3 object, the last modified time of the local file is newer than the last modified time of the S3 object, or the local file does not exist under the specified bucket and prefix.Two consequences matter for this migration:
-
An object already present at the destination, with the same size and an equal or later modification time, is not transferred.
syncwill not re-upload identical objects, so re-running it after an interruption resumes rather than repeating work. - An object already present at the destination is replaced if its size differs from the exported copy, or if the exported copy carries a newer modification time. Presence at the destination is therefore not by itself a guarantee that the existing object is preserved.
Upload only the keys absent from the destination
bash for export -f. Remove -P 8 to run the uploads serially. Re-running is safe: keys already uploaded are skipped by the check.
Do not set --sse or --sse-kms-key-id; the bucket’s default encryption is applied server-side, and specifying a different key only introduces a failure.
Using aws s3 sync instead
If the destination bucket is empty — confirmed by the inventory in the previous step reporting no objects —aws s3 sync is simpler and parallelises automatically:
--delete, which removes destination objects that are absent from the source.
Recent AWS CLI versions also provide a --no-overwrite option, described in the reference as preventing overwriting at the destination so that only files not already present are transferred. It is not available in every version, so confirm it before depending on it:
0 means the installed version does not support it, and a sync issued with the flag would fail or ignore it. Use the key-list method above in that case.
Stricter alternative for a bucket in active use
If other writers may be adding objects to the bucket while the upload runs, a conditional write removes the remaining race. The service rejects the request withPreconditionFailed when the key already exists, so an overwrite is impossible at the API level rather than by prior inspection:
Verify the Migration
Confirm no existing objects were modified
Take the detailed listing again and compare it against the baseline recorded before the upload. No output is the expected result, and confirms that no pre-existing object changed:Confirm every source object arrived
This comparison must produce no output:Confirm the expected prefixes are present
Post-Migration Configuration
Migrating the data does not by itself connect the application to the new bucket. Confirm the following before returning the environment to service.- Bucket name. The bucket name configured for the application must match the destination bucket. Check every location where the deployment sets it, including any per-service override, since a single global value can be shadowed by a stale one elsewhere.
- Object storage endpoint. Any MinIO endpoint, region placeholder, or TLS verification override used for in-cluster storage must be replaced with the AWS S3 endpoint and the real region.
- Credentials. If pods obtain AWS credentials from an IAM role for service accounts, any static access key left in the configuration must be cleared. A static value takes precedence over the role in the AWS SDK credential chain, so leftover MinIO development keys cause every read to fail against S3.
- Certificate bundle. Any custom CA bundle configured for a private object store must be unset. AWS certificates verify against the system trust store.
- Upload notifications. In-cluster MinIO publishes object-created events over AMQP. With S3, the equivalent notification is delivered through SQS, so the queue the application subscribes to must exist in the same account and be named correctly in the configuration. Without it, uploads never complete in the interface.
-
Notification event types. The bucket notification must include
s3:ObjectCreated:CompleteMultipartUploadin addition tos3:ObjectCreated:Put. Browser uploads above the multipart threshold emit the former, so a configuration covering onlyPutresults in small files working and large files appearing to hang:
- CORS. Browsers upload directly to the bucket, so the CORS configuration must allow the application’s front-end origin:
Cleanup
Restore the application to its previous replica counts using the file recorded before the export, then remove the staged copy and temporary credentials:Legacy Filesystem Layout
This section applies only if the format check reportedfs, which occurs on installations created by much older MinIO releases. In that case the bucket directory contains regular files named after the object keys, and it can be synchronised directly.
Inspect the dry run before running the transfer. Every key listed must be a real object key, with no xl.meta, no part.1, and no UUID path segments. If any of those appear, the format check was misread and the procedure in this article applies instead:
Debugging
AccessDenied on upload with correct S3 permissions: the bucket uses SSE-KMS and the uploading principal is missing kms:GenerateDataKey or kms:Encrypt on the bucket’s key. The error names S3 rather than KMS, which makes this easy to misdiagnose.
The export contains xl.meta or part.1 files: the storage volume was copied instead of reading through the API. Delete the staged copy and repeat the export using the MinIO endpoint.
An object already in the destination bucket was replaced: aws s3 sync transfers an object whose size differs from the exported copy, even when the key already exists. Use the key-list method in “Upload only the keys absent from the destination” for a bucket that already holds data. If versioning is enabled on the bucket, recover the previous version with aws s3api list-object-versions --bucket $DEST_BUCKET --prefix "<key>".
Small files migrate but large files do not appear in the application: the bucket notification is missing s3:ObjectCreated:CompleteMultipartUpload.
The transfer stops partway with a connection error: the port-forward tunnel dropped. Re-establish it and run the export again; aws s3 sync skips objects already present in the staging directory, so the transfer resumes rather than restarting.
Objects are present in the bucket but the application reports them as missing: the configured bucket name does not match the destination bucket, or an additional prefix was introduced during the upload. Compare a key from src-keys.txt against the same object in the bucket.
