Azure Storage
1. Azure Storage Overview
Azure Storage provides massively scalable, durable, and highly available cloud storage for unstructured, semi-structured, and structured data.
Storage services:
| Service | Data Type | Access Protocol | Use Case |
|---|---|---|---|
| Blob Storage | Unstructured (files, images, video, backups) | REST, SDKs, AzCopy, Storage Explorer | Media hosting, backups, data lake |
| Queue Storage | Messages (up to 64 KB each) | REST, SDKs | Asynchronous task dispatching |
| Table Storage | Semi-structured key-value (NoSQL) | REST, SDKs, OData | Logging, IoT telemetry, user profiles |
| Azure Files | Files (SMB/NFS shares) | SMB 3.x, NFS 4.1, REST | Lift-and-shift, shared config, home directories |
Storage account types:
| Type | Services | Performance | Use Case |
|---|---|---|---|
| General-purpose v2 (GPv2) | Blob, Queue, Table, Files | Standard (HDD) | Default for most workloads |
| Premium Block Blob | Blob (block blobs only) | Premium (SSD) | Low-latency blob access, IoT, analytics |
| Premium File Shares | Files only | Premium (SSD) | High-IOPS file workloads |
| Premium Page Blobs | Blob (page blobs only) | Premium (SSD) | VM disks, databases |
Redundancy options:
| Option | Copies | Durability | Scope |
|---|---|---|---|
| LRS (Locally Redundant) | 3 | 99.999999999% (11 nines) | Single datacenter |
| ZRS (Zone Redundant) | 3 | 99.9999999999% (12 nines) | 3 availability zones |
| GRS (Geo-Redundant) | 6 | 99.99999999999999% (16 nines) | Primary + secondary region |
| GZRS (Geo-Zone Redundant) | 6 | 99.99999999999999% (16 nines) | 3 zones + secondary region |
| RA-GRS / RA-GZRS | 6 | Same as GRS/GZRS | Read access to secondary region |
Access tiers (Blob Storage):
| Tier | Access Frequency | Min Retention | Storage Cost | Access Cost |
|---|---|---|---|---|
| Hot | Frequent | None | Highest | Lowest |
| Cool | Infrequent (30+ days) | 30 days | Lower | Higher |
| Cold | Rare (90+ days) | 90 days | Lower still | Higher still |
| Archive | Long-term (180+ days) | 180 days | Lowest | Highest (rehydration required) |
2. Storage Account Provisioning
Azure CLI
# Create a General-purpose v2 storage account
az storage account create \
--name contosostorage2024 \
--resource-group rg-contoso \
--location eastus \
--sku Standard_ZRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--https-only true \
--default-action Deny
# Create with hierarchical namespace (ADLS Gen2)
az storage account create \
--name contosodatalake2024 \
--resource-group rg-contoso \
--location eastus \
--sku Standard_ZRS \
--kind StorageV2 \
--enable-hierarchical-namespace true \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--https-only true
Bicep Template
param storageAccountName string
param location string = resourceGroup().location
param sku string = 'Standard_ZRS'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageAccountName
location: location
kind: 'StorageV2'
sku: {
name: sku
}
properties: {
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
allowBlobPublicAccess: false
allowSharedKeyAccess: false // Enforce RBAC only
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
ipRules: []
virtualNetworkRules: []
}
encryption: {
services: {
blob: { enabled: true, keyType: 'Account' }
file: { enabled: true, keyType: 'Account' }
queue: { enabled: true, keyType: 'Account' }
table: { enabled: true, keyType: 'Account' }
}
keySource: 'Microsoft.Storage'
}
}
}
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' = {
parent: storageAccount
name: 'default'
properties: {
deleteRetentionPolicy: {
enabled: true
days: 30
}
containerDeleteRetentionPolicy: {
enabled: true
days: 30
}
isVersioningEnabled: true
changeFeed: {
enabled: true
}
}
}
output storageAccountId string = storageAccount.id
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob
output queueEndpoint string = storageAccount.properties.primaryEndpoints.queue
output tableEndpoint string = storageAccount.properties.primaryEndpoints.table
output fileEndpoint string = storageAccount.properties.primaryEndpoints.file
Network Rules
# Deny all public access by default
az storage account update \
--name contosostorage2024 \
--resource-group rg-contoso \
--default-action Deny
# Allow a VNet subnet
az storage account network-rule add \
--account-name contosostorage2024 \
--resource-group rg-contoso \
--vnet-name vnet-contoso \
--subnet subnet-apps
# Allow trusted Azure services
az storage account update \
--name contosostorage2024 \
--resource-group rg-contoso \
--bypass AzureServices
# Create a private endpoint
az network private-endpoint create \
--name pe-contosostorage-blob \
--resource-group rg-contoso \
--vnet-name vnet-contoso \
--subnet subnet-private-endpoints \
--private-connection-resource-id $(az storage account show --name contosostorage2024 --resource-group rg-contoso --query id -o tsv) \
--group-ids blob \
--connection-name pec-contosostorage-blob
3. Blob Storage
Azure Blob Storage is optimized for storing massive amounts of unstructured data.
Blob Types
| Type | Max Size | Use Case | Write Pattern |
|---|---|---|---|
| Block blob | 190.7 TiB (50,000 blocks x 4000 MiB) | Files, images, video, backups | Upload in blocks, commit |
| Append blob | 195 GiB (50,000 blocks x 4 MiB) | Log files, audit trails | Append-only writes |
| Page blob | 8 TiB | VM disks, databases | Random read/write at page (512 B) offsets |
Container Operations
import { BlobServiceClient, ContainerClient } from "@azure/storage-blob";
import { DefaultAzureCredential } from "@azure/identity";
const blobService = new BlobServiceClient(
`https://${accountName}.blob.core.windows.net`,
new DefaultAzureCredential()
);
// Create a container
const containerClient = blobService.getContainerClient("documents");
await containerClient.create();
// List containers
for await (const container of blobService.listContainers()) {
console.log(`Container: ${container.name}, Last Modified: ${container.properties.lastModified}`);
}
// Delete a container
await containerClient.delete();
Upload and Download
const containerClient = blobService.getContainerClient("documents");
const blockBlobClient = containerClient.getBlockBlobClient("reports/q4-2024.pdf");
// Upload from file
await blockBlobClient.uploadFile("./local/q4-2024.pdf", {
blobHTTPHeaders: { blobContentType: "application/pdf" },
metadata: { department: "finance", quarter: "Q4" },
tags: { category: "reports", year: "2024" },
tier: "Cool", // Set access tier on upload
});
// Upload from stream (large files)
import { createReadStream } from "fs";
const stream = createReadStream("./local/large-dataset.csv");
const fileSize = (await stat("./local/large-dataset.csv")).size;
await blockBlobClient.uploadStream(stream, fileSize, 4 * 1024 * 1024, {
maxSingleShotSize: 256 * 1024 * 1024, // 256 MB threshold for single PUT
concurrency: 4, // Parallel block uploads
});
// Upload from buffer
const data = Buffer.from("Hello, Azure Storage!");
await blockBlobClient.upload(data, data.length);
// Download to file
const downloadResponse = await blockBlobClient.downloadToFile("./local/downloaded.pdf");
// Download to buffer
const buffer = await blockBlobClient.downloadToBuffer();
// Download specific range
const rangeResponse = await blockBlobClient.download(0, 1024); // First 1 KB
Blob Index Tags
Blob index tags enable key-value indexing for filtering blobs across containers.
// Set tags on upload or existing blob
await blockBlobClient.setTags({
project: "contoso-web",
status: "processed",
uploadDate: "2024-11-15",
});
// Find blobs by tag filter (works across containers!)
for await (const blob of blobService.findBlobsByTags(
`"project" = 'contoso-web' AND "status" = 'processed'`
)) {
console.log(`${blob.containerName}/${blob.name} — tags: ${JSON.stringify(blob.tags)}`);
}
Versioning and Soft Delete
# Enable blob versioning
az storage account blob-service-properties update \
--account-name contosostorage2024 \
--resource-group rg-contoso \
--enable-versioning true
# Enable blob soft delete (30 days)
az storage account blob-service-properties update \
--account-name contosostorage2024 \
--resource-group rg-contoso \
--enable-delete-retention true \
--delete-retention-days 30
# Enable container soft delete (30 days)
az storage account blob-service-properties update \
--account-name contosostorage2024 \
--resource-group rg-contoso \
--enable-container-delete-retention true \
--container-delete-retention-days 30
// List blob versions
for await (const blob of containerClient.listBlobsFlat({ includeVersions: true })) {
console.log(`${blob.name} | Version: ${blob.versionId} | Current: ${blob.isCurrentVersion}`);
}
// Create a snapshot
const snapshotResponse = await blockBlobClient.createSnapshot();
console.log(`Snapshot created: ${snapshotResponse.snapshot}`);
Blob Lease
Leases provide exclusive write/delete access to a blob or container.
const leaseClient = blockBlobClient.getBlobLeaseClient();
// Acquire a 30-second lease
const lease = await leaseClient.acquireLease(30);
console.log(`Lease ID: ${lease.leaseId}`);
// Renew the lease
await leaseClient.renewLease();
// Release the lease
await leaseClient.releaseLease();
4. Queue Storage
Azure Queue Storage provides reliable messaging between application components.
Limits:
- Max message size: 64 KB (base64 encoding reduces effective size to ~48 KB)
- Max TTL: 7 days (or -1 for infinite)
- Max messages in a queue: Unlimited (up to storage account capacity)
- Max visibility timeout: 7 days
- Max dequeue count: Unlimited
Queue Operations
import { QueueServiceClient, QueueClient } from "@azure/storage-queue";
import { DefaultAzureCredential } from "@azure/identity";
const queueService = new QueueServiceClient(
`https://${accountName}.queue.core.windows.net`,
new DefaultAzureCredential()
);
// Create a queue
const queueClient = queueService.getQueueClient("work-items");
await queueClient.create();
// List queues
for await (const queue of queueService.listQueues()) {
console.log(`Queue: ${queue.name}`);
}
Message Operations
// Send a message
await queueClient.sendMessage("Process order #12345");
// Send JSON payload
const payload = { orderId: 12345, action: "ship", priority: "high" };
await queueClient.sendMessage(Buffer.from(JSON.stringify(payload)).toString("base64"));
// Send with delayed visibility (visible after 60s)
await queueClient.sendMessage("Delayed task", {
visibilityTimeout: 60,
messageTimeToLive: 3600, // 1 hour TTL
});
// Receive messages (makes them invisible to other consumers)
const response = await queueClient.receiveMessages({
numberOfMessages: 5,
visibilityTimeout: 30,
});
for (const msg of response.receivedMessageItems) {
const body = Buffer.from(msg.messageText, "base64").toString();
console.log(`Processing: ${body} (attempt ${msg.dequeueCount})`);
// Process the message...
await processWorkItem(JSON.parse(body));
// Delete after successful processing
await queueClient.deleteMessage(msg.messageId, msg.popReceipt);
}
// Peek messages (read without hiding)
const peeked = await queueClient.peekMessages({ numberOfMessages: 5 });
// Update a message (change content or extend visibility)
await queueClient.updateMessage(
messageId,
popReceipt,
"Updated content",
60 // New visibility timeout
);
// Clear all messages from a queue
await queueClient.clearMessages();
// Get approximate message count
const properties = await queueClient.getProperties();
console.log(`Approximate count: ${properties.approximateMessagesCount}`);
Queue Storage vs Service Bus
| Feature | Queue Storage | Service Bus |
|---|---|---|
| Max message size | 64 KB | 256 KB (Standard) / 100 MB (Premium) |
| Max queue size | Up to storage account limit | 1-80 GB |
| Ordering | FIFO (best effort) | FIFO (guaranteed with sessions) |
| Duplicate detection | No | Yes |
| Dead-letter queue | Manual (poison queue) | Built-in |
| Topics / subscriptions | No | Yes |
| Transactions | No | Yes |
| Sessions | No | Yes |
| Pricing | Very low (per transaction) | Higher (per message + unit) |
When to choose Queue Storage: Simple, high-volume message passing; cost-sensitive; no ordering guarantees needed.
When to choose Service Bus: Complex routing (topics), guaranteed FIFO, transactions, dead-letter, large messages.
5. Table Storage
Azure Table Storage is a NoSQL key-value store for semi-structured data at petabyte scale.
Design principles:
- Partition key: Groups related entities for efficient queries. Queries filtering on partition key are fast (single partition scan).
- Row key: Uniquely identifies an entity within a partition. Combined with partition key, forms the primary key.
- Hot partition avoidance: Distribute writes across partitions to avoid throttling. Avoid monotonically increasing keys (timestamps) as partition keys.
Table Operations
import { TableServiceClient, TableClient, odata } from "@azure/data-tables";
import { DefaultAzureCredential } from "@azure/identity";
const tableService = new TableServiceClient(
`https://${accountName}.table.core.windows.net`,
new DefaultAzureCredential()
);
// Create a table
const tableClient = tableService.getTableClient("Customers");
await tableService.createTable("Customers");
// Or use TableClient directly
const client = TableClient.fromNamedKeyCredential(
`https://${accountName}.table.core.windows.net`,
"Customers",
new DefaultAzureCredential()
);
Entity Operations
const tableClient = new TableClient(
`https://${accountName}.table.core.windows.net`,
"Customers",
new DefaultAzureCredential()
);
// Insert an entity
await tableClient.createEntity({
partitionKey: "US-West",
rowKey: "CUST-001",
name: "Contoso Ltd",
email: "contact@contoso.com",
revenue: 1500000,
isActive: true,
createdAt: new Date(),
});
// Get a single entity
const entity = await tableClient.getEntity("US-West", "CUST-001");
console.log(`${entity.name} — ${entity.email}`);
// Update an entity (merge -- only updates specified properties)
await tableClient.updateEntity(
{ partitionKey: "US-West", rowKey: "CUST-001", revenue: 1750000 },
"Merge"
);
// Replace an entity (full replace)
await tableClient.updateEntity(
{
partitionKey: "US-West",
rowKey: "CUST-001",
name: "Contoso Corporation",
email: "info@contoso.com",
revenue: 1750000,
isActive: true,
},
"Replace"
);
// Upsert an entity (insert or replace)
await tableClient.upsertEntity(
{ partitionKey: "US-West", rowKey: "CUST-002", name: "Fabrikam Inc", email: "hello@fabrikam.com" },
"Merge"
);
// Delete an entity
await tableClient.deleteEntity("US-West", "CUST-001");
Query Entities
// Query by partition key (fast -- single partition scan)
const entities = tableClient.listEntities({
queryOptions: { filter: odata`PartitionKey eq 'US-West'` },
});
for await (const entity of entities) {
console.log(`${entity.rowKey}: ${entity.name}`);
}
// Query with multiple filters
const filtered = tableClient.listEntities({
queryOptions: {
filter: odata`PartitionKey eq 'US-West' and revenue gt 1000000 and isActive eq true`,
select: ["rowKey", "name", "revenue"],
},
});
// Cross-partition query (slower -- full table scan)
const allActive = tableClient.listEntities({
queryOptions: { filter: odata`isActive eq true` },
});
// Top N results
const topCustomers = tableClient.listEntities({
queryOptions: {
filter: odata`PartitionKey eq 'US-West'`,
},
});
// Use iterator to get first N
let count = 0;
for await (const entity of topCustomers) {
if (++count > 10) break;
console.log(entity.name);
}
Batch Operations
// Batch operations (up to 100 entities, same partition key)
const actions: TransactionAction[] = [
["create", { partitionKey: "US-West", rowKey: "CUST-010", name: "Acme Corp" }],
["create", { partitionKey: "US-West", rowKey: "CUST-011", name: "Globex Inc" }],
["update", { partitionKey: "US-West", rowKey: "CUST-001", revenue: 2000000 }, "Merge"],
["delete", { partitionKey: "US-West", rowKey: "CUST-005" }],
];
await tableClient.submitTransaction(actions);
Partition Key Design Patterns
| Pattern | Partition Key | Row Key | Example |
|---|---|---|---|
| Region/tenant | Region or tenant ID | Entity ID | US-West / CUST-001 |
| Time-bucketed | Date bucket (YYYYMM) | Timestamp + ID | 202411 / 20241115T103000-EVT001 |
| Reversed timestamp | Constant or category | Reversed tick count | logs / (MaxTicks - Ticks)-ID |
| Composite | Category | Sub-category + ID | Orders-2024 / Shipped-ORD123 |
6. Azure Files
Azure Files provides fully managed file shares accessible via SMB and NFS protocols.
Tier comparison:
| Feature | Standard (GPv2) | Premium |
|---|---|---|
| Media | HDD | SSD |
| Max share size | 100 TiB | 100 TiB |
| Max IOPS | 20,000 (per share) | 100,000 (per share) |
| Throughput | Up to 300 MiB/s | Up to 10 GiB/s |
| Protocol | SMB 3.x, NFS 4.1 | SMB 3.x, NFS 4.1 |
| Redundancy | LRS, ZRS, GRS, GZRS | LRS, ZRS |
Share Operations
# Create a file share
az storage share-rm create \
--storage-account <storage-name> \
--resource-group <rg-name> \
--name documents \
--quota 1024 \
--enabled-protocols SMB \
--access-tier Hot
# List shares
az storage share-rm list \
--storage-account <storage-name> \
--resource-group <rg-name> \
--output table
# Upload a file
az storage file upload \
--account-name <storage-name> \
--share-name documents \
--source ./local-file.pdf \
--path "reports/q4-report.pdf" \
--auth-mode login
# Download a file
az storage file download \
--account-name <storage-name> \
--share-name documents \
--path "reports/q4-report.pdf" \
--dest ./downloaded.pdf \
--auth-mode login
Mount SMB Share
Windows:
# Mount as network drive
$connectTestResult = Test-NetConnection -ComputerName <storage-name>.file.core.windows.net -Port 445
if ($connectTestResult.TcpTestSucceeded) {
net use Z: \\<storage-name>.file.core.windows.net\documents /user:Azure\<storage-name> <storage-key>
}
Linux:
sudo mkdir -p /mnt/documents
sudo mount -t cifs //<storage-name>.file.core.windows.net/documents /mnt/documents \
-o vers=3.0,username=<storage-name>,password=<storage-key>,dir_mode=0777,file_mode=0777,serverino
Azure File Sync
Azure File Sync enables centralizing file shares in Azure Files while maintaining local access performance.
# Register a server with the Storage Sync Service
az storagesync registered-server create \
--resource-group <rg-name> \
--storage-sync-service <sync-service-name> \
--server-id <server-id>
# Create a sync group
az storagesync sync-group create \
--resource-group <rg-name> \
--storage-sync-service <sync-service-name> \
--name <sync-group-name>
# Add cloud endpoint (Azure file share)
az storagesync sync-group cloud-endpoint create \
--resource-group <rg-name> \
--storage-sync-service <sync-service-name> \
--sync-group-name <sync-group-name> \
--name <endpoint-name> \
--storage-account <storage-name> \
--azure-file-share-name documents
Snapshots
# Create a share snapshot
az storage share snapshot \
--account-name <storage-name> \
--name documents
# List snapshots
az storage share list \
--account-name <storage-name> \
--include-snapshots \
--output table
7. Security & Access Control
SAS Token Types
| Type | Signed With | Scope | Best For |
|---|---|---|---|
| Account SAS | Account key | All services (blob, queue, table, file) | Broad delegated access |
| Service SAS | Account key | Single service (container, queue, table, share) | Scoped delegated access |
| User Delegation SAS | Azure AD credentials | Blob / Data Lake only | Most secure delegated access |
Always prefer User Delegation SAS -- it uses Azure AD instead of account keys and can be revoked by revoking the delegation key.
User Delegation SAS
import {
BlobServiceClient,
BlobSASPermissions,
generateBlobSASQueryParameters,
SASProtocol,
} from "@azure/storage-blob";
import { DefaultAzureCredential } from "@azure/identity";
const blobService = new BlobServiceClient(
`https://${accountName}.blob.core.windows.net`,
new DefaultAzureCredential()
);
const startsOn = new Date();
const expiresOn = new Date(startsOn.getTime() + 60 * 60 * 1000); // 1 hour
// Get user delegation key
const delegationKey = await blobService.getUserDelegationKey(startsOn, expiresOn);
// Generate SAS for a blob
const sasParams = generateBlobSASQueryParameters(
{
containerName: "documents",
blobName: "reports/q4-2024.pdf",
permissions: BlobSASPermissions.parse("r"),
startsOn,
expiresOn,
protocol: SASProtocol.Https,
},
delegationKey,
accountName
);
const sasUrl = `${blobService.getContainerClient("documents").getBlobClient("reports/q4-2024.pdf").url}?${sasParams}`;
Stored Access Policies
Stored access policies define reusable permission sets that can be revoked server-side.
# Create a stored access policy on a container
az storage container policy create \
--account-name <storage-name> \
--container-name documents \
--name read-only-policy \
--permissions r \
--expiry 2025-12-31T00:00:00Z \
--auth-mode login
# Generate a service SAS using the stored policy
az storage blob generate-sas \
--account-name <storage-name> \
--container-name documents \
--name "reports/q4-2024.pdf" \
--policy-name read-only-policy \
--auth-mode login
# Revoke access by deleting or modifying the policy
az storage container policy delete \
--account-name <storage-name> \
--container-name documents \
--name read-only-policy \
--auth-mode login
RBAC Roles
| Role | Scope | Permissions |
|---|---|---|
| Storage Blob Data Owner | Blob | Full access including ACLs |
| Storage Blob Data Contributor | Blob | Read, write, delete blobs |
| Storage Blob Data Reader | Blob | Read blobs only |
| Storage Blob Delegator | Account | Generate user delegation keys |
| Storage Queue Data Contributor | Queue | Send, receive, delete messages |
| Storage Queue Data Reader | Queue | Read and peek messages |
| Storage Queue Data Message Processor | Queue | Peek, receive, delete messages |
| Storage Queue Data Message Sender | Queue | Send messages only |
| Storage Table Data Contributor | Table | Read, write, delete entities |
| Storage Table Data Reader | Table | Read entities only |
| Storage File Data SMB Share Contributor | Files | Read, write, delete in SMB shares |
| Storage File Data SMB Share Reader | Files | Read access to SMB shares |
| Storage File Data SMB Share Elevated Contributor | Files | Read, write, delete, modify ACLs |
# Assign role
az role assignment create \
--assignee <principal-id> \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<account>"
# Disable shared key access (enforce RBAC only)
az storage account update \
--name <storage-name> \
--resource-group <rg-name> \
--allow-shared-key-access false
Private Endpoints
# Create a private endpoint for blob service
az network private-endpoint create \
--name pe-storage-blob \
--resource-group <rg-name> \
--vnet-name <vnet-name> \
--subnet <pe-subnet> \
--private-connection-resource-id <storage-account-resource-id> \
--group-ids blob \
--connection-name pec-storage-blob
# Create private DNS zone
az network private-dns zone create \
--resource-group <rg-name> \
--name "privatelink.blob.core.windows.net"
# Link DNS zone to VNet
az network private-dns zone link create \
--resource-group <rg-name> \
--zone-name "privatelink.blob.core.windows.net" \
--name link-vnet \
--virtual-network <vnet-name> \
--registration-enabled false
Encryption
- SSE (Storage Service Encryption): All data encrypted at rest by default with Microsoft-managed keys (256-bit AES).
- CMK (Customer-Managed Keys): Use Azure Key Vault to manage encryption keys.
- Infrastructure encryption: Double encryption (service-level + infrastructure-level).
- Client-side encryption: Encrypt data before uploading using the SDK.
# Enable CMK encryption
az storage account update \
--name <storage-name> \
--resource-group <rg-name> \
--encryption-key-source Microsoft.Keyvault \
--encryption-key-vault <keyvault-uri> \
--encryption-key-name <key-name> \
--encryption-key-version <key-version>
# Enable infrastructure encryption (double encryption)
az storage account create \
--name <storage-name> \
--resource-group <rg-name> \
--location eastus \
--sku Standard_ZRS \
--kind StorageV2 \
--require-infrastructure-encryption true
8. Lifecycle Management
Lifecycle management policies automate tiering and deletion of blobs to optimize costs.
Policy Structure
{
"rules": [
{
"enabled": true,
"name": "rule-name",
"type": "Lifecycle",
"definition": {
"actions": {
"baseBlob": {
"tierToCool": { "daysAfterModificationGreaterThan": 30 },
"tierToCold": { "daysAfterModificationGreaterThan": 90 },
"tierToArchive": { "daysAfterModificationGreaterThan": 180 },
"delete": { "daysAfterModificationGreaterThan": 365 },
"enableAutoTierToHotFromCool": true
},
"snapshot": {
"tierToCool": { "daysAfterCreationGreaterThan": 30 },
"delete": { "daysAfterCreationGreaterThan": 90 }
},
"version": {
"tierToCool": { "daysAfterCreationGreaterThan": 30 },
"delete": { "daysAfterCreationGreaterThan": 90 }
}
},
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["logs/", "backups/"],
"blobIndexMatch": [
{ "name": "status", "op": "==", "value": "archived" }
]
}
}
}
]
}
Lifecycle Triggers
| Trigger | Description |
|---|---|
daysAfterModificationGreaterThan |
Days since blob was last modified |
daysAfterCreationGreaterThan |
Days since blob/snapshot/version was created |
daysAfterLastAccessTimeGreaterThan |
Days since last read (requires access tracking) |
daysAfterLastTierChangeGreaterThan |
Days since tier was last changed |
# Enable last access time tracking (required for access-based tiering)
az storage account blob-service-properties update \
--account-name <storage-name> \
--resource-group <rg-name> \
--enable-last-access-tracking true
# Apply lifecycle policy
az storage account management-policy create \
--account-name <storage-name> \
--resource-group <rg-name> \
--policy @lifecycle-policy.json
Immutability Policies
| Type | Description | Reversible? |
|---|---|---|
| Time-based retention (Unlocked) | Prevents deletion for N days | Can extend or delete policy |
| Time-based retention (Locked) | Prevents deletion for N days | IRREVERSIBLE -- cannot reduce period |
| Legal hold | Prevents deletion until tags removed | Yes -- remove tags |
# Set container-level immutability (365 days)
az storage container immutability-policy create \
--account-name <storage-name> \
--container-name compliance-docs \
--period 365
# Set legal hold
az storage container legal-hold set \
--account-name <storage-name> \
--container-name compliance-docs \
--tags "audit-2024" "sec-investigation"
Blob Inventory
Blob inventory reports provide a complete list of blobs and their metadata.
# Create an inventory policy
az storage account blob-inventory-policy create \
--account-name <storage-name> \
--resource-group <rg-name> \
--policy @inventory-policy.json
9. Static Website Hosting
Azure Blob Storage can serve static websites directly from a $web container.
Enable Static Website
az storage blob service-properties update \
--account-name <storage-name> \
--static-website true \
--index-document index.html \
--404-document 404.html
Static website endpoint: https://<account>.z13.web.core.windows.net
Deploy Content
az storage blob upload-batch \
--account-name <storage-name> \
--destination '$web' \
--source ./dist \
--overwrite true \
--auth-mode login
Custom Domain + CDN
# Create CDN profile and endpoint
az cdn profile create --name cdn-profile --resource-group <rg-name> --sku Standard_Microsoft
az cdn endpoint create \
--name cdn-endpoint \
--resource-group <rg-name> \
--profile-name cdn-profile \
--origin <account>.z13.web.core.windows.net \
--origin-host-header <account>.z13.web.core.windows.net \
--enable-compression true
# Add custom domain
az cdn custom-domain create \
--name www-contoso \
--resource-group <rg-name> \
--profile-name cdn-profile \
--endpoint-name cdn-endpoint \
--hostname www.contoso.com
# Enable HTTPS
az cdn custom-domain enable-https \
--name www-contoso \
--resource-group <rg-name> \
--profile-name cdn-profile \
--endpoint-name cdn-endpoint
SPA Routing
For single-page applications (React, Angular, Vue), set both index and error documents to index.html so that client-side routing works:
az storage blob service-properties update \
--account-name <storage-name> \
--static-website true \
--index-document index.html \
--404-document index.html
10. Data Lake Storage Gen2
Azure Data Lake Storage Gen2 (ADLS Gen2) extends Blob Storage with a hierarchical namespace, POSIX-like ACLs, and atomic directory operations -- optimized for big data analytics.
Hierarchical Namespace
ADLS Gen2 organizes blobs into a true directory structure (not just prefix-based).
# Create a storage account with hierarchical namespace
az storage account create \
--name contosodatalake \
--resource-group rg-contoso \
--location eastus \
--sku Standard_ZRS \
--kind StorageV2 \
--enable-hierarchical-namespace true
Directory Operations
import { DataLakeServiceClient } from "@azure/storage-file-datalake";
import { DefaultAzureCredential } from "@azure/identity";
const datalakeService = new DataLakeServiceClient(
`https://${accountName}.dfs.core.windows.net`,
new DefaultAzureCredential()
);
// Create a file system (container)
const fileSystemClient = datalakeService.getFileSystemClient("analytics");
await fileSystemClient.create();
// Create directories
const directoryClient = fileSystemClient.getDirectoryClient("raw/2024/11");
await directoryClient.create();
// Upload a file
const fileClient = directoryClient.getFileClient("events.parquet");
await fileClient.create();
await fileClient.append(data, 0, data.length);
await fileClient.flush(data.length);
// Rename / move a directory (atomic operation)
await directoryClient.rename("analytics", "processed/2024/11");
// Delete a directory recursively
await directoryClient.delete(true);
ACLs (Access Control Lists)
ADLS Gen2 supports POSIX-like ACLs for fine-grained access control.
# Set ACL on a directory (rwx for owner, r-x for group, --- for other)
az storage fs access set \
--account-name contosodatalake \
--file-system analytics \
--path raw/2024/11 \
--acl "user::rwx,group::r-x,other::---" \
--auth-mode login
# Set ACL recursively
az storage fs access set-recursive \
--account-name contosodatalake \
--file-system analytics \
--path raw/ \
--acl "user::rwx,group::r-x,other::---" \
--auth-mode login
# Grant specific user access
az storage fs access set \
--account-name contosodatalake \
--file-system analytics \
--path raw/ \
--acl "user:<user-object-id>:r-x" \
--auth-mode login
Integration with Analytics
| Service | Integration | Use Case |
|---|---|---|
| Azure Databricks | Direct mount or ABFS driver | Spark-based ETL, ML |
| Azure Synapse Analytics | External tables, OPENROWSET | SQL queries on data lake |
| Azure Data Factory | Copy activity, dataflows | ETL/ELT pipelines |
| Azure HDInsight | ABFS driver | Hadoop/Spark clusters |
| Power BI | Dataflows, DirectQuery via Synapse | Business intelligence |
ABFS URI format: abfss://<container>@<account>.dfs.core.windows.net/<path>
11. Monitoring
Diagnostic Settings
# Enable diagnostic logging for blob service
az monitor diagnostic-settings create \
--name storage-diagnostics \
--resource $(az storage account show --name <storage-name> --resource-group <rg-name> --query id -o tsv)/blobServices/default \
--workspace <log-analytics-workspace-id> \
--logs '[
{"category":"StorageRead","enabled":true,"retentionPolicy":{"enabled":false}},
{"category":"StorageWrite","enabled":true,"retentionPolicy":{"enabled":false}},
{"category":"StorageDelete","enabled":true,"retentionPolicy":{"enabled":false}}
]' \
--metrics '[{"category":"Transaction","enabled":true,"retentionPolicy":{"enabled":false}}]'
Azure Monitor Metrics
| Metric | Description | Useful For |
|---|---|---|
Transactions |
Number of storage requests | Traffic analysis |
Ingress |
Bytes received | Upload volume |
Egress |
Bytes sent | Download volume / cost |
SuccessServerLatency |
Average server-side latency | Performance monitoring |
SuccessE2ELatency |
Average end-to-end latency | User experience |
Availability |
Service availability percentage | SLA monitoring |
UsedCapacity |
Storage used in bytes | Capacity planning |
Alerts
# Alert on high latency (> 500ms average)
az monitor metrics alert create \
--name high-latency-alert \
--resource-group <rg-name> \
--scopes <storage-account-resource-id> \
--condition "avg SuccessServerLatency > 500" \
--window-size 5m \
--evaluation-frequency 1m \
--action-group <action-group-id>
# Alert on capacity threshold (> 80% of 5 TB)
az monitor metrics alert create \
--name capacity-alert \
--resource-group <rg-name> \
--scopes <storage-account-resource-id>/blobServices/default \
--condition "avg BlobCapacity > 4398046511104" \
--window-size 1h \
--evaluation-frequency 1h \
--action-group <action-group-id>
Storage Analytics (Classic)
# Enable Storage Analytics logging
az storage logging update \
--account-name <storage-name> \
--log rwd \
--retention 30 \
--services bqt \
--auth-mode login
# Enable Storage Analytics metrics
az storage metrics update \
--account-name <storage-name> \
--hour true \
--minute true \
--retention 30 \
--services bqt \
--auth-mode login
12. Common Patterns
Pattern 1: Image Upload Pipeline with Blob Trigger
Upload images to a container, trigger an Azure Function to generate thumbnails.
// Azure Function: Blob trigger
import { app, InvocationContext } from "@azure/functions";
import sharp from "sharp";
import { BlobServiceClient } from "@azure/storage-blob";
import { DefaultAzureCredential } from "@azure/identity";
const blobService = new BlobServiceClient(
process.env.STORAGE_ACCOUNT_URL!,
new DefaultAzureCredential()
);
app.storageBlob("generateThumbnail", {
path: "images/{name}",
connection: "AzureWebJobsStorage",
handler: async (blob: Buffer, context: InvocationContext) => {
const blobName = context.triggerMetadata.name as string;
context.log(`Processing image: ${blobName}`);
// Generate thumbnail (150x150)
const thumbnail = await sharp(blob)
.resize(150, 150, { fit: "cover" })
.toBuffer();
// Upload thumbnail to thumbnails container
const thumbnailClient = blobService
.getContainerClient("thumbnails")
.getBlockBlobClient(blobName);
await thumbnailClient.upload(thumbnail, thumbnail.length, {
blobHTTPHeaders: { blobContentType: "image/jpeg" },
});
context.log(`Thumbnail created: thumbnails/${blobName}`);
},
});
Pattern 2: Queue-Based Work Dispatcher
Distribute work items across multiple workers using Queue Storage.
import { QueueServiceClient } from "@azure/storage-queue";
import { DefaultAzureCredential } from "@azure/identity";
const queueService = new QueueServiceClient(
process.env.STORAGE_QUEUE_URL!,
new DefaultAzureCredential()
);
// Producer: enqueue work items
async function enqueueWorkItems(items: WorkItem[]) {
const queueClient = queueService.getQueueClient("work-items");
for (const item of items) {
const encoded = Buffer.from(JSON.stringify(item)).toString("base64");
await queueClient.sendMessage(encoded);
}
}
// Consumer: process work items with poison message handling
async function processWorkItems() {
const workQueue = queueService.getQueueClient("work-items");
const poisonQueue = queueService.getQueueClient("work-items-poison");
await poisonQueue.createIfNotExists();
while (true) {
const response = await workQueue.receiveMessages({
numberOfMessages: 5,
visibilityTimeout: 120,
});
if (response.receivedMessageItems.length === 0) {
await sleep(5000);
continue;
}
for (const msg of response.receivedMessageItems) {
if (msg.dequeueCount > 5) {
await poisonQueue.sendMessage(msg.messageText);
await workQueue.deleteMessage(msg.messageId, msg.popReceipt);
continue;
}
try {
const item: WorkItem = JSON.parse(
Buffer.from(msg.messageText, "base64").toString()
);
await processItem(item);
await workQueue.deleteMessage(msg.messageId, msg.popReceipt);
} catch (err) {
console.error(`Failed to process ${msg.messageId}:`, err);
}
}
}
}
Pattern 3: Static Website with CDN and Custom Domain
Full setup for a production static website.
# 1. Create storage account
az storage account create \
--name contosowebsite \
--resource-group rg-contoso \
--location eastus \
--sku Standard_LRS \
--kind StorageV2
# 2. Enable static website
az storage blob service-properties update \
--account-name contosowebsite \
--static-website true \
--index-document index.html \
--404-document index.html # SPA routing
# 3. Deploy built assets
az storage blob upload-batch \
--account-name contosowebsite \
--destination '$web' \
--source ./dist \
--overwrite true
# 4. Create CDN
az cdn profile create --name cdn-contoso --resource-group rg-contoso --sku Standard_Microsoft
az cdn endpoint create \
--name www-contoso \
--resource-group rg-contoso \
--profile-name cdn-contoso \
--origin contosowebsite.z13.web.core.windows.net \
--origin-host-header contosowebsite.z13.web.core.windows.net \
--enable-compression true
# 5. Map custom domain (after
…(truncated)