Azure Blob Storage & CDN
Hot/Cool/Cold/Archive tiers, SAS tokens, and lifecycle policies β how Azure stores and serves objects at scale.
Azure Blob Storage is an object storage service designed for unstructured data β documents, images, videos, backups, and logs. It scales to exabytes with 11β16 nines of durability depending on redundancy level. Its access tier system (Hot/Cool/Cold/Archive) lets you trade access speed for cost, automating transitions via lifecycle management policies.
A Storage Account is the top-level namespace. Containers (like buckets) hold blobs. Blob names can include '/' separators to simulate a folder hierarchy β this is a naming convention, not a real directory structure. There are three blob types: Block (files, arbitrary data), Append (logging), Page (random-access, VM disks).
Hot: highest storage cost ($0.018/GB), lowest access cost β for frequently read data. Cool: lower storage cost ($0.01/GB), higher access cost, minimum 30-day retention. Cold: even lower ($0.004/GB), minimum 90-day retention. Archive: cheapest storage ($0.001/GB) but offline β retrieval (rehydration) takes 1β15 hours.
JSON lifecycle policies run daily and automatically transition blobs between tiers or delete them based on age (last modified, last accessed, created). This is cost optimization on autopilot β e.g., move to Cool after 30 days, Archive after 180, delete after 365. Rules can target by container prefix or blob index tags.
A Shared Access Signature (SAS) is a signed URL with embedded permissions (read/write/delete/list), expiry time, IP restrictions, and optional protocol constraints. The recipient can use it without an account key. User Delegation SAS (signed by an Entra ID token) is more secure than Service SAS (signed by an account key) because it rotates automatically.
Azure CDN (or Azure Front Door) serves blobs from edge PoPs close to the user. On cache miss, the CDN fetches from the origin (Blob Storage) and caches for the duration of the Cache-Control header. Cache hits cost ~$0.0075/GB vs Blob egress ~$0.087/GB β ~12x cheaper. Cache invalidation requires a purge API call or URL versioning.
LRS: 3 copies in one datacenter. ZRS: 3 copies across 3 availability zones. GRS: LRS + async replication to a secondary region. GZRS: ZRS + secondary region. RA-GRS: GRS with read access to the secondary endpoint at all times. Higher redundancy = higher cost + higher durability SLA (up to 16 nines with RA-GZRS).
Key Concepts
Block: up to 190.7TB, immutable block composition β for files, backups. Append: optimized for log writes (append only). Page: 512-byte random-access pages, up to 8TB β backing store for Azure VM disks.
Hot β Cool β Cold β Archive. Storage cost decreases; retrieval cost and latency increase. Archive blobs are offline β reads require a rehydration request first (1β15 hours). Early deletion penalties apply.
Rehydrating from Archive: Standard priority = up to 15 hours. High priority = typically <1 hour at ~10x the cost. Choose based on urgency. Always model rehydration time into your RTO.
Service SAS: access to one storage service, signed by account key. Account SAS: access to multiple services, signed by account key. User Delegation SAS: signed by Entra ID user/managed identity token β automatically rotated, audit-logged.
A named policy on a container that a Service SAS can reference. Lets you revoke all SAS tokens referencing that policy without knowing the individual tokens β the only way to revoke account-key-signed SAS before expiry.
LRS: 3 copies, 1 zone. ZRS: 3 zones. GRS: LRS + async secondary region. GZRS: ZRS + secondary region. RA-GRS: readable secondary. Durability from 11 nines (LRS) to 16 nines (RA-GZRS).
Soft delete retains deleted blobs for a configurable retention period (1β365 days). Versioning captures a snapshot on every overwrite. Both protect against accidental deletions. Neither helps if a lifecycle policy deletes intentionally.
JSON rules that run daily and move or delete blobs based on age, last access time, or index tags. Automates tier transitions at zero operational overhead.
1// Azure Blob Storage β @azure/storage-blob SDK v122// User Delegation SAS (more secure than Account Key SAS)34import {5 BlobServiceClient,6 StorageSharedKeyCredential,7 generateBlobSASQueryParameters,8 BlobSASPermissions,9 UserDelegationKey,10} from "@azure/storage-blob";11import { DefaultAzureCredential } from "@azure/identity";1213// --- UPLOAD WITH LIFECYCLE TIER ---14const credential = new DefaultAzureCredential(); // Managed Identity β no secrets15const blobServiceClient = new BlobServiceClient(16 `https://${process.env.STORAGE_ACCOUNT}.blob.core.windows.net`,17 credential18);1920const containerClient = blobServiceClient.getContainerClient("reports");21const blockBlobClient = containerClient.getBlockBlobClient("2024/q1/report.pdf");2223await blockBlobClient.uploadData(pdfBuffer, {24 tier: "Hot", // explicit tier on upload25 blobHTTPHeaders: {26 blobContentType: "application/pdf",27 blobCacheControl: "public, max-age=86400", // CDN/browser cache hint28 },29 metadata: { generatedBy: "report-service", version: "3" },30});3132// --- USER DELEGATION SAS (preferred over Account Key SAS) ---33// User delegation key is bound to an Entra ID identity β rotates with the token34const userDelegationKey: UserDelegationKey =35 await blobServiceClient.getUserDelegationKey(36 new Date(),37 new Date(Date.now() + 60 * 60 * 1000) // key valid for 1 hour38 );3940const sasParams = generateBlobSASQueryParameters(41 {42 containerName: "reports",43 blobName: "2024/q1/report.pdf",44 permissions: BlobSASPermissions.parse("r"), // read-only45 startsOn: new Date(),46 expiresOn: new Date(Date.now() + 15 * 60 * 1000), // 15-minute window β short-lived47 ipRange: { start: "203.0.113.0", end: "203.0.113.255" }, // IP restriction48 },49 userDelegationKey,50 process.env.STORAGE_ACCOUNT!51);5253const sasUrl = `${blockBlobClient.url}?${sasParams.toString()}`;5455// --- LIFECYCLE MANAGEMENT POLICY (ARM / Bicep equivalent) ---56await blobServiceClient.setProperties({57 // Configure via Azure Portal or ARM template; SDK sets account-level properties58 // Lifecycle rules are JSON β example structure:59 // {60 // rules: [{61 // name: "archive-old-reports",62 // type: "Lifecycle",63 // definition: {64 // filters: { blobTypes: ["blockBlob"], prefixMatch: ["reports/"] },65 // actions: {66 // baseBlob: {67 // tierToCool: { daysAfterModificationGreaterThan: 30 },68 // tierToArchive: { daysAfterModificationGreaterThan: 180 },69 // delete: { daysAfterModificationGreaterThan: 365 },70 // },71 // },72 // },73 // }],74 // }75});
Blob Storage is the foundation of Azure data platforms β backing Data Lake, ML training datasets, CDN origins, VM disk images, and application file stores. Understanding the access tier economics and SAS token security model is critical: the difference between Hot and Archive tier is 18x on storage cost but the difference in access pattern can mean hours of rehydration delay in a production incident.
Common Pitfalls
1Medical imaging archive β 15-hour Archive rehydration blocked emergency access
A hospital radiology system archived DICOM images older than 6 months to Archive tier to save costs. When a patient was readmitted, the on-call radiologist needed prior scan comparisons immediately. The rehydration request took 11 hours β the patient had been in surgery for 3 hours before images were accessible.
Standard priority rehydration from Archive is documented as 'up to 15 hours' but in practice often 8β15 hours under load. The team modeled Archive as 'cheap cold storage' but didn't map the rehydration SLA to their clinical RTO of <30 minutes. There was no High priority rehydration workflow configured.
Implemented a two-tier archive: images 6β24 months old go to Cold tier (instant access, $0.004/GB). Only images >24 months go to Archive. Added an emergency rehydration workflow using High priority (BlobRehydratePriority.High) for flagged patient IDs. High priority rehydration costs ~10x but typically completes in <1 hour.
Takeaway: Archive tier means 'offline storage' β blobs are not directly readable. Always map rehydration time (Standard: up to 15hrs, High: ~1hr) against your actual access SLA before choosing Archive. For disaster recovery scenarios, Cold tier with instant access is usually the right tradeoff.
2B2B file exchange β account-key SAS in source code, credential leaked
A logistics company provided partners with SAS tokens to upload shipment manifests. The token generation code used a hardcoded storage account key. A developer accidentally committed the key to a public GitHub repository. Within 47 minutes, the account was enumerated, 200GB of manifests were downloaded, and the attacker began uploading malicious files.
Account-key SAS tokens cannot be selectively revoked β there is no registry of issued tokens. The only remediation was rotating the storage account key, which invalidated ALL SAS tokens instantly, breaking every partner integration simultaneously. Key rotation required emergency partner communication and 6 hours of coordinated downtime.
Migrated to User Delegation SAS signed by a Managed Identity. Token lifetime capped at 15 minutes per upload session. Added a Stored Access Policy on the upload container so all outstanding tokens can be instantly revoked by revoking the policy β without rotating the account key. Added GitHub secret scanning alert to the repo.
Takeaway: Account-key SAS tokens are irrevocable individually β rotation is the nuclear option. User Delegation SAS auto-rotates with the Entra ID token and can be audited in sign-in logs. Always prefer User Delegation SAS for any external-facing access. Always set a short expiry.
3Video streaming platform β CORS misconfiguration blocked browser direct upload
A video platform migrated to a browser-direct-to-Blob-Storage upload architecture (using SAS tokens) to eliminate server bandwidth costs. The feature worked perfectly in the development environment but 100% of uploads failed in production with 'CORS policy: No Access-Control-Allow-Origin header' errors in the browser console.
Azure Blob Storage CORS rules are configured per storage account, not per container. The development environment storage account had wildcard CORS enabled (added during debugging). The production account had no CORS rules. Server-side uploads from the backend worked fine; browser XMLHttpRequest/fetch uploads to Blob Storage URLs require the storage account itself to return CORS headers.
Added a CORS rule to the production storage account allowing the app origin, PUT/POST methods, and required headers (x-ms-blob-type, Content-Type). CORS rules are applied in Azure Portal under Settings > Resource sharing (CORS) or via ARM template. Added a smoke test that verifies CORS configuration is present in the production deployment pipeline.
Takeaway: Browser-direct-to-Blob-Storage uploads require CORS rules configured on the storage account itself β not the app server. This is a separate configuration from server-side access. Missing CORS causes total upload failure with no error on the server side, making it hard to diagnose from logs alone.