Read and write Azure Blob Storage
A file source with an azblob: block takes each blob written under a prefix in an Azure Blob Storage
container as one message, and deletes it once the message is stored. A file destination with one
writes each output as a blob of its own. dir is the name prefix: inbox reads the blobs whose
names start with inbox/.
source:
file:
dir: inbox
minAge: 1s
azblob:
account: examplestore
container: inbound-adt
transform: ./transform.ts
destinations:
- file:
dir: outbox
azblob:
account: examplestore
container: outbound-lab
Install the client
Azure Blob Storage runs on Azure's storage client, and its identity client, optional peer dependencies a project installs to use them. The Docker image carries both.
npm install @azure/storage-blob @azure/identity
Identity
By default the engine signs in with Azure's default chain: a managed identity on the machine, app or
container it runs on, a workload identity, or the AZURE_CLIENT_ID, AZURE_TENANT_ID and
AZURE_CLIENT_SECRET environment variables. Nothing secret goes in the channel's YAML. Give the
identity the Storage Blob Data Contributor role on the container.
For an account without Microsoft Entra ID, connectionString reads a connection string with the
account key from a secret reference, at start:
source:
file:
dir: inbox
azblob:
account: examplestore
container: inbound-adt
connectionString: { env: AZURE_STORAGE_CONNECTION_STRING }
destinations:
- file:
dir: out
A connection string that names an http endpoint sends in the clear and is refused at start unless
the block says plaintext: true, which is never read from the environment.
How the source claims a blob
Blob Storage has no rename, so the claim is a conditional write, the same as on S3. On each pass the source lists the prefix, a page at a time, and for each blob that is ready:
- reads its size, entity tag and modification time, and leaves a blob larger than
maxSizeby that size where it is; - uploads a marker,
<dir>/.claimed/<path>, withIf-None-Match: *. The service writes it for the first engine and refuses every other, so one engine reads the blob; - reads the blob with
If-Matchon its tag, stores it under the keyazblob:<account>/<container>/<name>@<tag>@<modified>, and deletes the blob, again withIf-Match, so a blob written at that name since stays; - deletes the marker.
A blob lease would also let one engine read a blob, but it lasts at most 60 seconds unless renewed, and a large blob can take longer to store; the marker needs no renewal, and is the one rule both object stores share. Each pass first finishes the markers its own channel left, so a stop at any step stores nothing twice. A marker names its channel, and recovery is keyed on the channel's name: two engines that store into separate databases must not read one prefix with channels of the same name, or each could finish the other's markers and the blob would be stored in both. Every write to a blob gives it a new tag, so a blob written again at the same name is always taken as a new one.
A blob that has snapshots is not deleted: Blob Storage refuses the delete, and the engine stops with
ERR_PATHWAYS_AZBLOB_SNAPSHOTS_PRESENT, never as a service that is down. The blob was stored once,
and its marker stays, so once its snapshots are removed the next run finishes it without storing it
again. Do not take snapshots of blobs in a prefix a source reads.
Before its first claim the source writes one probe blob twice to check that the second write is
refused. Any other error the probe meets is given with its own code and checked again on the next
pass: ERR_PATHWAYS_AZBLOB_NOT_INSTALLED when a package is not installed, and
ERR_PATHWAYS_AZBLOB_NO_SUCH_CONTAINER when the container is not there. singleNode: true runs on a service that does not refuse it, as on
S3: safe only while one engine
and one channel read the prefix.
The keys minAge, minSize, maxSize, maxPerPoll, sortBy, stable, include, exclude,
recursive and ignoreDot work as they do on a folder. keepProcessed, doneFile, errorDir and
readTries are refused.
How the destination writes
The destination uploads each output as <dir>/<message id>-<output number>.<extension> in one
request. By default (mode: create) it uploads with If-None-Match: *, so it never replaces a blob.
A blob already there with the same bytes counts as delivered; one with other bytes rejects the
message with the reason OBJECT_EXISTS. mode: overwrite replaces it.
Azurite
Azurite, Azure's local emulator, listens on http://127.0.0.1:10000/<account>. Point a channel at it
with its connection string and plaintext: true:
source:
file:
dir: inbox
azblob:
account: devstoreaccount1
container: inbound-adt
connectionString: { env: AZURITE_CONNECTION_STRING }
plaintext: true
destinations:
- file:
dir: out
When the service cannot be reached
A request that gets no answer, an answer of an HTTP error, or no identity marks the source down
until a pass runs again, with the codes in Logs and codes.
The blobs stay where they are.
Later
The source polls on each pollInterval. Taking blobs as Event Grid announces them is later work.
Related
- Read and write message files, whose keys the source shares.
- Read and write Amazon S3.
- Configuration reference: Azure Blob block.