LFSX_STORAGE=azure keeps the objects and the locks in a Blob Storage container. Azure has no S3 API, so this is a native client rather than LFSX_STORAGE=s3 pointed at a gateway, and everything Objects in a bucket says about the layout, deduplication, collection and the local cache holds here too.

LFSX_STORAGE=azure
LFSX_AZURE_ACCOUNT=studioassets
LFSX_AZURE_CONTAINER=lfs
LFSX_AZURE_ACCOUNT_KEY=…

The container has to exist. The server never creates one, and it reports not ready until it can list the container, so a typo in the name shows up at boot rather than on the first push.

Credentials

Set at most one of these. Neither means the pod's own identity.

Variable What it does
LFSX_AZURE_ACCOUNT_KEY the storage account key. The server signs a short-lived SAS for every request with it, and it is the only credential that can sign a download URL for a client
LFSX_AZURE_SAS_TOKEN a SAS you issued, container scope, with read, add, create, write, delete and list. It is never handed to a client: it covers the whole container, and a client should get one object
neither managed identity, or workload identity on AKS when AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID and AZURE_CLIENT_ID are set, which the workload identity webhook does. The identity needs Storage Blob Data Contributor on the container

LFSX_AZURE_ENDPOINT overrides the blob endpoint, https://<account>.blob.core.windows.net by default, for Azurite, a private endpoint or a sovereign cloud.

What differs from S3

Locks use the same conditional write, If-None-Match: *, which Azure answers with 409 rather than 412. The startup probe still writes one key twice and requires the second to be refused, and locking switches off rather than pretending if it is not.

Large objects go up as blocks above 5,000 MiB, Azure's single-request ceiling, in blocks of 64 MiB grown when fifty thousand of them would not cover the object, and are committed with one block list. Uncommitted blocks expire on their own after a week, so an interrupted upload leaves nothing to clean up.

Redirects. LFSX_S3_PRESIGN=true hands downloads to the container through a SAS scoped to one blob, which needs the account key; with a SAS token or an identity, downloads keep coming through the server and the log says so. Uploads always come through the server: a signed write URL is only safe if the store refuses a body that does not hash to the object, and Azure can bind an MD5 to a write but not the SHA-256 an oid is. LFSX_S3_CACHE_DIR and LFSX_S3_CACHE_MAX_BYTES apply as they do for a bucket.

Testing against Azurite

docker run -d --name azurite -p 10000:10000 mcr.microsoft.com/azure-storage/azurite \
  azurite-blob --blobHost 0.0.0.0 --loose
az storage container create --name lfsx-test --connection-string \
  "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
LFSX_TEST_AZURE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 cargo test --test bucket

The bucket suite runs unchanged, two replicas racing for the same lock included, and skips only the tests about clients uploading straight to the store.