DEV Community

Cover image for Multi-Cloud S3-Compatible Strategy: Avoiding Vendor Lock-In (2026)
Ethan Carter
Ethan Carter

Posted on

Multi-Cloud S3-Compatible Strategy: Avoiding Vendor Lock-In (2026)

Multi-Cloud S3-Compatible Strategy: Avoiding Vendor Lock-In (2026)

Most teams assume they're portable because their app talks the S3 API. That assumption breaks the first time a cloud jacks up prices or you actually need to leave. The API was never the trap. Everything built on top of it is.

This is a practical guide to keeping the option to move, cheaply, without re-architecting your stack.

The Lock-In Problem: Where S3 Actually Traps You

S3 compatibility cuts both ways. Every vendor implements the core API, so your PutObject and GetObject calls port in theory. In practice, "portable in theory" falls apart two layers deeper, at the parts most teams never think about:

Layer 1 is the API, and it's portable. put_object / get_object work everywhere. No issue.

Layer 2 is non-standard features. Lifecycle rules, Object Lock modes, S3 Select, bucket logging. The syntax differs per vendor. What works on AWS does not drop into MinIO or RustFS cleanly.

Layer 3 is ecosystem glue. IAM, event routing (SQS/SNS/Lambda vs Pub/Sub vs Event Grid), monitoring (CloudWatch vs Stackdriver vs Azure Monitor), CI/CD wired to one CLI. None of it moves with your data.

We've watched teams discover they were locked in at Layer 2 or 3, not Layer 1. The S3 API was fine. The forty other things they'd wired around it were not.

A Portable S3 Architecture

Abstract the endpoint from day one

Don't hardcode s3.us-east-1.amazonaws.com. Every S3 SDK takes an endpoint_url. Use it:

import os
s3 = boto3.client(
    "s3",
    endpoint_url=os.environ["S3_ENDPOINT"],   # https://s3.us-east-1.amazonaws.com or http://localhost:9000
    aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
    aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
)
Enter fullscreen mode Exit fullscreen mode

That single line is the difference between "we can move if we must" and "we're stuck."

Stick to core operations

If portability matters, restrict yourself to what every serious S3 implementation actually supports:

Safe (universal) Caution (varies) Avoid (vendor-specific)
PutObject / GetObject Lifecycle rules Event notifications (SQS/Lambda)
ListObjectsV2 Object Lock modes IAM-policy access
DeleteObject S3 Select KMS-managed keys
Multipart Upload Bucket logging Requester-pays
HeadObject Presigned URL quirks Cross-account roles
CopyObject Replication config Glacier retrieval tiers

Rule of thumb: if it isn't in the MinIO + RustFS + Ceph RGW docs, assume it won't move.

Own your orchestration layer

Stop reaching for cloud-native orchestration for anything S3-critical:

Function Cloud-native (locked) Portable
Workflows Step Functions / Logic Apps Airflow / Dagster
ETL Glue DataBrew dbt / Spark
Catalog Glue Catalog Iceberg (metadata lives in S3)
Scheduling EventBridge / CloudWatch Cron Airflow DAGs / cron
Secrets Secrets Manager / Key Vault Vault / env vars

Test portability every quarter

Don't wait for a forced migration to learn you weren't portable:

  1. Stand up MinIO or RustFS in Docker (under a minute).
  2. Point staging at it.
  3. Run your suite.
  4. Write down what broke.

Costs a couple of hours a quarter. Beats finding out during a six-month migration with a real egress bill attached.

Self-Hosting: Maximum Portability

The most portable setup is running your own S3 everywhere:

Region A (AWS us-east-1)   → RustFS on EC2
Region B (Azure westeurope) → RustFS on Azure VMs
Region C (GCP asia-east1)  → RustFS on GCE
On-prem (Tokyo)            → RustFS on bare metal
Edge (stores)              → RustFS on ARM
Enter fullscreen mode Exit fullscreen mode

Same binary, same API, same ops model. You kill the egress between your own regions, you control the upgrade cadence, and your data stays where you put it. The trade is that you run it. If you already operate K8s, Postgres, and app servers in each region, adding S3 is incremental. Not a new skill.

When Multi-Cloud Is a Waste of Time

Honest boundaries:

Scenario Verdict
Under 5TB Don't bother. Migration cost beats the savings.
Under 5 engineers Don't bother. Ops overhead beats the option value.
Heavy Lambda / SageMaker / BigQuery use Partial. Storage moves, compute doesn't.
Regulatory region lock Comply. Law beats preference.
Early-stage startup Ship first. Revisit at Series B.

TL;DR

  1. Lock-in lives at Layers 2 and 3 (features + ecosystem), not Layer 1 (the API).
  2. Abstract endpoint_url from day one. Never hardcode a region endpoint.
  3. Use core S3 operations only. Test against MinIO/RustFS every quarter.
  4. Own your orchestration (Airflow/dbt/Iceberg beat Step Functions/Glue).
  5. Self-hosted S3 across regions is max portability. Same binary, different tin.
  6. Multi-cloud isn't free. Pay for it only when the option value beats the ops cost.

RustFS runs identically on AWS, Azure, GCP, and bare metal. One binary, any infrastructure, Apache 2.0. Download.


FAQ

Is the S3 API actually portable between clouds?

Core operations (PutObject, GetObject, ListObjects, Multipart Upload) are genuinely portable. They behave identically across AWS S3, MinIO, RustFS, Ceph RGW, and Wasabi. But roughly a third of the full S3 surface is vendor extensions (event notifications, specialized encryption, IAM, request-payer config) that don't transfer. For apps using core operations only, portability is real. For apps wired into one cloud's ecosystem, it's an illusion.

How much does multi-cloud storage cost versus single-cloud?

Self-hosted S3 (RustFS or MinIO) on cheap VMs across regions usually runs 40 to 70 percent under each cloud's native service. You drop per-GB premiums, cross-instance egress, and request fees, but you pick up ops overhead. Running multiple clouds' native S3 at once normally costs more than single-cloud, because of redundant storage plus cross-cloud egress. The cheapest pattern is self-hosted S3 on the cheapest VM in each region.

What's the fastest way to test S3 portability?

Stand up MinIO (docker run -p 9000:9000 minio/minio server /data) or RustFS in Docker, point S3_ENDPOINT at localhost:9000, and run your suite. If 95 percent plus passes with no code changes, you're portable. Document the failures, because those are your lock-in points. Do it quarterly. About two hours, versus weeks of firefighting and five-figure egress during a forced move.

Top comments (0)