Migrate from Azure Blob Storage to Tigris
Azure Blob Storage is not S3-compatible. Tigris shadow buckets require an S3-compatible source. You cannot point a Tigris shadow bucket at Azure Blob Storage directly.
To migrate from Azure Blob Storage, you must put an S3-compatible shim in front of your Azure Blob account. This guide uses s3proxy, an open-source proxy that exposes an S3 API and forwards requests to Azure Blob Storage. You point the Tigris shadow bucket at the s3proxy endpoint. Tigris then migrates your data with zero downtime.
This setup adds a component that you must run and operate. s3proxy sits between Tigris and Azure Blob Storage. For a native S3 source, such as AWS S3, Google Cloud Storage, Cloudflare R2, or MinIO, see the provider-specific guides. Those sources need no shim.
Why an S3 shim is required
Tigris is S3-compatible. Shadow bucket migration speaks the S3 API to your source bucket. Azure Blob Storage uses a different API and does not accept S3 requests. Tigris cannot read from it directly.
s3proxy solves this. s3proxy presents an S3 endpoint to Tigris. s3proxy translates each S3 request into an Azure Blob Storage request. Tigris sees a standard S3 source. Azure Blob Storage stays unchanged.
Migration approach
Tigris supports lazy migration with shadow buckets. Tigris does not copy all your data upfront. Instead, Tigris fetches objects from the s3proxy endpoint on demand. It caches them for future access. No downtime is required.
You can also enable write-through mode, which syncs new writes back through s3proxy to your Azure Blob container. Your Azure Blob container stays up to date throughout the migration. You can take as long as you need before you complete the cutover.
Prerequisites
Before you start, verify that you have:
- A Tigris account with a bucket created
- An Azure Storage account and the container you want to migrate from
- An Entra ID identity with the Storage Blob Data Reader role on that container (recommended), or the storage account access key
- A host to run s3proxy that Tigris can reach over the public internet
- s3proxy 4.0.0 or newer, and Java 17 or newer on that host
- No blobs in the Archive access tier (see the next section)
This guide uses the configuration properties of s3proxy 4.0.0. Releases before 4.0.0 use a different Azure backend with different properties and support account keys only.
Rehydrate archived blobs before you migrate
s3proxy cannot read blobs in the Azure Archive access tier. Archive-tier
blobs are offline in Azure. When the migration reaches an archived blob, the
tigris buckets migrate command fails with the message
an internal error occurred, try again. The message does not name the archived
blob as the cause.
Containers with lifecycle management policies commonly contain archived blobs, because those policies move cold objects to the Archive tier automatically. No S3 shim reads archived blobs transparently. You must rehydrate them to the Hot or Cool tier before you migrate.
A retry after a failed drain is expensive. The tigris buckets migrate command
walks the full container again from the start on each run. Rehydrate all
archived blobs first, then run the drain once.
Rehydration changes blob data. Run the commands in this section with your own
Azure identity, not with the read-only migration identity from the
prerequisites. The commands use --auth-mode login to authenticate as the
identity from az login. That identity must have the Storage Blob Data
Contributor role, or a higher role, on the container. The Storage Blob Data
Reader role can list blobs but cannot change their tier.
To find the archived blobs in a container:
az storage blob list \
--account-name <azure-storage-account-name> \
--container-name <container-name> \
--auth-mode login \
--query "[?properties.blobTier=='Archive'].name" \
--output tsv
To rehydrate one blob, set its tier to Hot or Cool:
az storage blob set-tier \
--account-name <azure-storage-account-name> \
--container-name <container-name> \
--auth-mode login \
--name <blob-name> \
--tier Hot
To rehydrate every archived blob, pipe the list into a loop that runs the same command one time for each blob:
az storage blob list \
--account-name <azure-storage-account-name> \
--container-name <container-name> \
--auth-mode login \
--query "[?properties.blobTier=='Archive'].name" \
--output tsv |
while IFS= read -r blob_name; do
az storage blob set-tier \
--account-name <azure-storage-account-name> \
--container-name <container-name> \
--auth-mode login \
--name "$blob_name" \
--tier Hot
done
The loop reads one blob name from each line and keeps spaces, quotes, and backslashes in the name intact. A blob name that contains a newline character is the one exception. Rehydrate such a blob with the single-blob command.
Rehydration is not immediate. A standard-priority rehydration can take up to 15
hours. Wait until no blob reports Archive before you start the migration.
Step 1: Configure s3proxy for Azure Blob Storage
s3proxy reads its settings from a properties file. Create a file named
s3proxy.conf with the Azure Blob backend.
s3proxy 4.0.0 authenticates to Azure Blob Storage in two ways. Entra ID is the recommended way because you can scope it to one container with a read-only role. The storage account access key is the fallback.
Do not deploy the example credential values. Anyone with these public values can
read your Azure Blob objects. The s3proxy endpoint must stay publicly
reachable. Generate your own strong, unique S3 credentials for
s3proxy.identity and s3proxy.credential. Use the same credential values in
both the s3proxy config and the Tigris shadow-bucket command. The
s3proxy.identity value must match the --access-key value. The
s3proxy.credential value must match the --secret-key value. Replace the
example credential values with your own secrets.
- Entra ID (recommended)
- Account key (fallback)
Create a service principal with the Storage Blob Data Reader role. Scope the role to the one container that you migrate from:
az ad sp create-for-rbac \
--name s3proxy-migration \
--role "Storage Blob Data Reader" \
--scopes "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Storage/storageAccounts/<azure-storage-account-name>/blobServices/default/containers/<container-name>"
Leave jclouds.identity and jclouds.credential empty in the properties file.
s3proxy then authenticates with DefaultAzureCredential from the Azure SDK:
s3proxy.endpoint=http://0.0.0.0:8080
s3proxy.authorization=aws-v2-or-v4
s3proxy.identity=local-identity
s3proxy.credential=local-credential
s3proxy.read-only-blobstore=true
jclouds.provider=azureblob
jclouds.identity=
jclouds.credential=
jclouds.endpoint=https://<azure-storage-account-name>.blob.core.windows.net
Give the service principal credentials to s3proxy through environment variables. Set them in the environment that starts s3proxy in Step 2:
export AZURE_CLIENT_ID=<service-principal-app-id>
export AZURE_TENANT_ID=<entra-tenant-id>
export AZURE_CLIENT_SECRET=<service-principal-secret>
DefaultAzureCredential also supports managed identities. If s3proxy runs on
an Azure VM with a managed identity, assign the Storage Blob Data Reader
role to that identity and set no environment variables.
An Azure storage account access key cannot be scoped to one container and
cannot be made read-only. Each key grants full control of every container in
the storage account, and the holder can mint SAS tokens. If the key leaks
through your public s3proxy endpoint, the whole storage account is exposed.
Use Entra ID instead when you can, and always set
s3proxy.read-only-blobstore=true.
Set your Azure storage account name and account access key in the properties file:
s3proxy.endpoint=http://0.0.0.0:8080
s3proxy.authorization=aws-v2-or-v4
s3proxy.identity=local-identity
s3proxy.credential=local-credential
s3proxy.read-only-blobstore=true
jclouds.provider=azureblob
jclouds.identity=<azure-storage-account-name>
jclouds.credential=<azure-storage-account-key>
jclouds.endpoint=https://<azure-storage-account-name>.blob.core.windows.net
The two credential pairs serve different roles:
s3proxy.identityands3proxy.credentialare the S3 access key and secret that Tigris uses to authenticate to s3proxy. You set them to values you choose. You enter these same values in the shadow bucket configuration in Step 3.jclouds.identityandjclouds.credentialare your Azure storage account name and account access key. When you leave them empty, s3proxy reads Entra ID credentials from its environment instead.
Set s3proxy.read-only-blobstore=true in every setup. Migration only reads from
the source. With this property, s3proxy rejects write and delete requests with a
NotImplemented error, and read requests continue to work. This limits the
damage if your s3proxy credentials leak.
Write-through mode (Step 6) writes back to Azure through s3proxy. If you plan
to use write-through, remove s3proxy.read-only-blobstore=true, and give the
Entra ID identity the Storage Blob Data Contributor role instead.
Step 2: Run s3proxy
Start s3proxy with your properties file.
java -jar s3proxy --properties s3proxy.conf
s3proxy now listens on the s3proxy.endpoint address. Test it with an S3 client
before you continue.
The example endpoint uses plain HTTP on port 8080. Tigris is a hosted service. Tigris must reach s3proxy over the public internet with a valid TLS certificate. Before you configure the shadow bucket, put s3proxy behind a public HTTPS endpoint. See the s3proxy SSL support guide for TLS options.
Step 3: Configure the shadow bucket
Point the shadow bucket at your s3proxy endpoint. Use the s3proxy.identity and
s3proxy.credential values from Step 1 as the access key and secret key.
- Dashboard
- CLI
- Go to the Tigris Dashboard
- Click Buckets in the left menu
- Select the bucket you want to migrate data into
- Click Settings
- Find Enable Data Migration and toggle it on
- Enter your s3proxy connection details:
- Endpoint: The public HTTPS URL of your s3proxy instance
- Region:
auto(s3proxy does not require a region) - Access Key ID: The
s3proxy.identityvalue from Step 1 - Secret Access Key: The
s3proxy.credentialvalue from Step 1 - Bucket: The Azure Blob container name
The CLI flow is two commands: configure the shadow source, then optionally drain it.
1. Configure the shadow bucket with
tigris buckets set-migration:
tigris buckets set-migration my-bucket \
--bucket azure-container-name \
--endpoint https://<your-s3proxy-endpoint> \
--region auto \
--access-key local-identity \
--secret-key local-credential
Add --write-through for write-through mode, or --disable to clear the
migration configuration.
2. Actively migrate. Lazy migration copies objects only when a request
touches them. To copy every remaining object server-side, run
tigris buckets migrate:
tigris buckets migrate my-bucket
The command runs in the foreground and reports progress as it runs. This step is required before you disable the shadow configuration and decommission Azure Blob Storage. If you keep Azure Blob Storage as the source of truth, this step is optional.
s3proxy maps each S3 bucket name to an Azure Blob container. Set the shadow bucket Bucket field to the name of the Azure Blob container that holds your data.
Step 4: Update your application
Tigris exposes an S3-compatible API, not the Azure Blob Storage API. You must change your application to use the Tigris SDK or an AWS S3 SDK. The Azure Blob Storage SDK will not work against Tigris. Set the endpoint and credentials for Tigris in your S3 client configuration.
With the AWS CLI:
aws s3 ls s3://your-bucket/ \
--endpoint-url https://t3.storage.dev \
--region auto
Or with boto3:
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://t3.storage.dev",
region_name="auto",
aws_access_key_id="your-tigris-access-key",
aws_secret_access_key="your-tigris-secret-key",
)
Your bucket names and object keys stay the same after migration. You must change your application source code to use the Tigris SDK or an AWS S3 SDK, not the Azure Blob Storage SDK.
Step 5: Verify the migration
Once your application points to Tigris, objects migrate on first access. To verify:
- Request an object that exists in your Azure Blob container
- Verify that it returns successfully through Tigris
- Verify that later requests are served directly from Tigris
Step 6: Enable write-through (optional)
To keep your Azure Blob container in sync during the migration, enable write-through in the shadow bucket settings. With write-through enabled:
- New objects written to Tigris are also written through s3proxy to Azure Blob Storage
- Deletes apply to both Tigris and Azure Blob Storage
- Object listings include the full contents of the Azure Blob container
This keeps your Azure Blob container current so you can fall back at any point.
Write-through needs write access through s3proxy. Remove
s3proxy.read-only-blobstore=true from s3proxy.conf and restart s3proxy. If
you use Entra ID, give the identity the Storage Blob Data Contributor role
on the container.
Step 7: Complete the migration
Run tigris buckets migrate and verify that it completes before you disable the
shadow bucket configuration. The command copies all objects to Tigris
server-side. Lazy migration copies an object only on first access. Objects that
no request ever touches stay only in Azure Blob Storage. If you disable the
shadow configuration and decommission Azure Blob Storage before the full
migration completes, Tigris cannot serve those objects. The data becomes
unavailable.
Once your workloads run well on Tigris, disable the shadow bucket configuration. Tigris becomes your primary object store. You can then shut down s3proxy and decommission your Azure Blob Storage.
Access Tigris from your application
Tigris exposes an S3-compatible API, not the Azure Blob Storage API. After you
move to Tigris, your application must talk to Tigris through the Tigris SDK or
an AWS S3 SDK. The Azure Blob Storage SDK will not work against Tigris. Set the
endpoint to https://t3.storage.dev and the region to auto. The SDK reads
your credentials from the environment variables AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY.
The examples below configure an S3 client against the Tigris endpoint and list the objects in a bucket.
- JavaScript
- Python
- Go
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
const S3 = new S3Client({
region: "auto",
endpoint: "https://t3.storage.dev",
s3ForcePathStyle: false,
});
const response = await S3.send(
new ListObjectsV2Command({ Bucket: "your-bucket" }),
);
for (const object of response.Contents ?? []) {
console.log(object.Key);
}
import boto3
from botocore.client import Config
svc = boto3.client(
"s3",
endpoint_url="https://t3.storage.dev",
region_name="auto",
config=Config(s3={"addressing_style": "virtual"}),
)
response = svc.list_objects_v2(Bucket="your-bucket")
for obj in response.get("Contents", []):
print(obj["Key"])
package main
import (
"context"
"fmt"
"log"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
func main() {
ctx := context.Background()
sdkConfig, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Fatalf("Couldn't load default configuration: %v", err)
}
svc := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://t3.storage.dev")
o.Region = "auto"
o.UsePathStyle = false
})
resp, err := svc.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String("your-bucket"),
})
if err != nil {
log.Fatalf("Unable to list objects: %v", err)
}
for _, obj := range resp.Contents {
fmt.Println(aws.ToString(obj.Key))
}
}
Azure-specific considerations
- s3proxy is an extra component. You must run, secure, and monitor it for the length of the migration. If s3proxy goes down, Tigris cannot reach objects that are not yet migrated.
- Public reachability and TLS. Tigris must reach s3proxy over the public internet with a valid TLS certificate. Do not expose the plain HTTP example endpoint to production traffic.
- Container-to-bucket mapping. s3proxy maps an S3 bucket name to an Azure Blob container. Use the container name as the shadow bucket Bucket value.
- Credential handling. The
s3proxy.conffile and the s3proxy environment hold your Azure credentials. Restrict access to both. Tigris never receives the Azure credentials. Tigris authenticates to s3proxy with the separates3proxy.identityands3proxy.credentialvalues. - Least privilege. Prefer an Entra ID identity with the Storage Blob Data
Reader role on one container. An account access key grants full control of
the whole storage account and cannot be restricted. Set
s3proxy.read-only-blobstore=truein every setup that does not use write-through. - Authentication support. s3proxy 4.0.0 supports account keys and Entra ID
through
DefaultAzureCredential, which includes service principals and managed identities. Releases before 4.0.0 default to an older backend that supports account keys only. - Archived blobs block the migration. s3proxy cannot read blobs in the Archive access tier. Rehydrate them before you migrate. See Rehydrate archived blobs before you migrate.
FAQ
Can Tigris read from Azure Blob Storage directly?
No. Azure Blob Storage is not S3-compatible. A Tigris shadow bucket needs an S3-compatible source. You must front Azure Blob Storage with an S3 shim such as s3proxy.
Why does tigris buckets migrate fail with "an internal error occurred, try again"?
A blob in the Azure Archive access tier is a common cause. s3proxy cannot read archived blobs, and the error message does not name them as the cause. List the archived blobs in the container and rehydrate them, then run the drain again. See Rehydrate archived blobs before you migrate. Each retry walks the full container again, so rehydrate all archived blobs before you retry.
Does migration require downtime?
No. Shadow bucket migration happens transparently. Your application continues to serve requests while objects migrate on first access.
Do I need to change my application code?
Yes. You need to change your application code. Tigris is not compatible with the Azure Blob Storage API. You must change your application source code to use the Tigris SDK or S3 API instead of the Azure Blob Storage API.
Can I roll back to Azure Blob Storage?
Yes. If you enable write-through mode, your Azure Blob container stays in sync with all new writes. You can revert your endpoint configuration to switch back at any point.
What happens to objects I do not access?
They remain in your Azure Blob container. Tigris only copies objects when they are first requested. Objects that are never accessed are never transferred.
Can I remove s3proxy after the migration?
Yes. s3proxy is only required while Tigris migrates from Azure Blob Storage. Once you disable the shadow bucket configuration, you can shut down s3proxy.