Background
Sections
IntroductionFoundations1. Resource Hierarchy2. Resource Manager3. Identity and RBAC4. Regions and Availability5. Naming and TaggingVirtual Machines1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetVirtual Network1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetBlob Storage1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure SQL Database1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Kubernetes Service1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Container Registry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetMicrosoft Entra ID1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure RBAC1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Functions1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAPI Management1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure App Configuration1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Machine Learning1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Monitor1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure AI Foundry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and Cheatsheet

7. Production

14 min read

The difference between "I uploaded a file in the portal" and "I run the company's data on this." Five pillars, always the same five.

[Image Prompt: 2D minimalistic diagram of five production pillars, security, cost, scaling, observability, and reliability, arranged around a central Azure storage account resource, flat design, clean vector art style, white background]


Security

Storage is where the data is, which makes it the thing attackers actually want. The good news is that the whole posture reduces to six settings and one habit.

The six settings

Setting Value Why
allowSharedKeyAccess false The highest-value setting on the account. Kills the root credential path entirely and forces every caller through Entra ID + RBAC. Also the one that separates the control plane from the data plane for real
allowBlobPublicAccess false Makes anonymous container access impossible by policy, not by hoping nobody sets a container to public. This is the setting that would have prevented most publicised "open bucket" incidents
publicNetworkAccess Disabled in prod Combined with a private endpoint, removes the account from the internet entirely
minimumTlsVersion TLS1_2 or higher Older versions are still accepted by default on older accounts
supportsHttpsTrafficOnly true Default on new accounts; verify on inherited ones
Encryption Platform-managed by default; customer-managed key in Key Vault where required Data is always encrypted at rest. CMK gives you the revocation lever and the audit story a regulator asks about. Infrastructure encryption adds a second layer if you need defence in depth

The habit: least privilege at container scope

The built-in data-plane roles:

Role Grants Use for
Storage Blob Data Reader Read and list blobs Consumers, BI, read replicas of a pipeline
Storage Blob Data Contributor Read, write, delete Ingestion jobs, application write paths
Storage Blob Data Owner The above + POSIX ACL management (HNS) Data-lake administrators only
Storage Blob Delegator Mint user-delegation SAS tokens Grant alongside a data role, to services that hand out links

And the ones that grant no data access: Owner, Contributor, Reader, Storage Account Contributor. They govern the resource. Note the asterisk: a Contributor can read the account keys, so Contributor is effectively a data owner unless allowSharedKeyAccess = false. That's the whole argument for that setting in one sentence.

When the built-in role is too broad — the common case is "read blobs but never delete them", which Data Reader gives you and Data Contributor doesn't stop at. A custom role:

{
  "Name": "Blob Writer No Delete",
  "IsCustom": true,
  "Description": "Read and write blobs, but never delete them.",
  "Actions": [],
  "NotActions": [],
  "DataActions": [
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read",
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write",
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action"
  ],
  "NotDataActions": [
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/delete"
  ],
  "AssignableScopes": ["/subscriptions/{sub}/resourceGroups/rg-data"]
}

Note DataActions rather than Actions — that's what makes it a data-plane role, and getting the two sections mixed up produces a role that grants nothing and takes an afternoon to debug.

SAS discipline

If you must hand out links:

  1. Use a user-delegation SAS (--as-user), signed via Entra ID, bounded by the signer's own permissions, and revocable by removing the role assignment.
  2. Short expiry. Hours, not months. A one-year SAS is a credential.
  3. Narrowest permissions and scope — read-only, one blob, and an IP range if you can.
  4. Never a service SAS signed with the account key without a stored access policy, because the only way to revoke it otherwise is rotating the key, which breaks everything else at the same time.
  5. Set a SAS expiry policy on the account so over-long SAS tokens are flagged in Defender for Cloud and Azure Policy.

Network isolation

  • Private endpoint — the account gets a private IP in your VNet. One per sub-resource (blob, dfs, …). Requires the privatelink.blob.core.windows.net Private DNS zone linked to every VNet that resolves it — including, easily forgotten, the one your on-prem DNS forwards from.
  • Service endpoints + VNet rules — cheaper and simpler, but traffic still uses the public endpoint (over the Azure backbone) and the account still has a public IP. Acceptable for internal-only workloads; a private endpoint is the stronger answer.
  • Firewall IP rules — for the handful of cases where a fixed external IP needs access. Note the bypass = AzureServices exception, which lets first-party services like Backup, Monitor, and Event Grid reach the account; turning it off breaks things in non-obvious ways.
  • The trusted-services + resource-instance rule — the most precise option: allow a specific Azure resource (one Data Factory, one Function App) through the firewall by its resource ID, rather than all Azure services.

The gotchas specific to this service

  • Anonymous public access on a container is the classic exposure. Kill it at the account level.
  • A SAS token in a URL is in every log it passes through — client logs, proxy logs, browser history, the ticket someone pasted it into.
  • Soft-deleted data is still data. A compliance deletion request isn't satisfied until the retention window expires or the version is purged.
  • The $logs container and static-website $web container are easy to forget when auditing what's public.
  • Malware. Anything accepting third-party uploads should enable Microsoft Defender for Storage with malware scanning, and use a quarantine-then-promote pattern rather than writing straight into a container something else reads.

Cost

Blob Storage has five meters, and the cheap-looking option turns one of the others up.

Meter What drives it The trap
Capacity GB-month, at a rate that drops Hot → Cool → Cold → Archive Includes soft-deleted blobs, old versions, snapshots, and uncommitted blocks — none of which appear in a container listing
Transactions Per 10,000 operations, at a rate that rises Hot → Cool → Cold → Archive Tiering frequently-read data to Cool makes it cost more. This inversion is the number-one tiering mistake
Data retrieval Per GB read from Cool / Cold / Archive Invisible until the first month's bill after a big tiering exercise
Egress Per GB leaving the Azure region or Azure entirely A CDN or Front Door in front is often cheaper than the egress it replaces
Early deletion Deleting or re-tiering before the minimum retention period Pro-rata charge for the remainder. A lifecycle rule that archives at 30 days and deletes at 90 is quietly paying this on every object

The three biggest wins, in order

1. A lifecycle management policy. The single largest lever, and it costs you a JSON rule.

{
  "rules": [
    {
      "enabled": true,
      "name": "tier-raw-data",
      "type": "Lifecycle",
      "definition": {
        "filters": {
          "blobTypes": ["blockBlob"],
          "prefixMatch": ["raw/"]
        },
        "actions": {
          "baseBlob": {
            "tierToCool":    { "daysAfterModificationGreaterThan": 30 },
            "tierToArchive": { "daysAfterModificationGreaterThan": 365 },
            "delete":        { "daysAfterModificationGreaterThan": 2555 }
          },
          "version":  { "delete": { "daysAfterCreationGreaterThan": 90 } },
          "snapshot": { "delete": { "daysAfterCreationGreaterThan": 90 } }
        }
      }
    }
  ]
}

Two notes: rules evaluate on a schedule (roughly daily), not instantly; and daysAfterLastAccessTimeGreaterThan is available if you enable last access time tracking — better signal than modification time, at the cost of a small transaction overhead for the tracking itself.

2. Expire old versions. Versioning is the most common runaway cost in this topic because nothing in the portal makes the extra bytes obvious. Every account with versioning on needs a version-expiry rule, full stop.

3. Fewer, larger objects. Ten million 4 KB blobs cost far more in transactions than a hundred 400 MB parquet files holding the same bytes, and analytics engines read them faster too.

Secondary levers: reserved capacity (a one- or three-year commitment on stored capacity, for predictable data volumes), right-sizing redundancy (GZRS everywhere is a doubled bill for data that may only need ZRS — decide per dataset, which means per account), and killing orphaned uncommitted blocks from failed uploads.

What keeps billing when nothing is using it

Everything. Storage bills for bytes at rest regardless of access — which is a good property compared with always-on compute plans, but it means the only way the bill goes down is deleting or tiering data. "Nobody uses that container any more" is not a saving until someone acts on it. Set a monthly review, or better, a lifecycle rule that acts on last-access time.


Scaling and limits

The essential Azure-specific point: almost every limit that matters is counted per storage account, and a few are counted per subscription per region.

Limit Scope Notes
Ingress / egress bandwidth Per storage account Varies by region, redundancy, and account type ⚠️ verify against current Azure docs
Request rate (IOPS) Per storage account Standard has a ceiling; Premium block blob is much higher ⚠️ verify
Throughput to a single blob Per blob Far below the account ceiling. One hot file cannot absorb an account's bandwidth
Storage accounts Per subscription, per region A soft limit, raisable via support ⚠️ verify. This is why "one subscription per environment" matters
Max block blob size Per blob Block size × block count; tens of TB in practice ⚠️ verify
Blob index tags per blob Per blob A small fixed number ⚠️ verify
Containers per account / blobs per container Effectively unbounded Constrained by capacity, not count

Raising a limit. Soft limits go through a quota request — the Quotas blade in the portal, the az quota commands where supported, or a support ticket. Do it before the launch, not during it; quota increases are not instant and are not guaranteed.

Scaling strategy, in order of preference:

  1. Fix the key distribution. A monotonic prefix (timestamps, sequential IDs) concentrates load on one partition range. Hash or reverse the leading characters.
  2. Parallelise across blobs, not within one.
  3. Shard across accounts. The real answer past a single account's ceiling. Shard by tenant, dataset, or environment — and design the sharding key before you need it, because retrofitting it means a data migration.
  4. Premium block blob if the problem is request rate and latency, not bandwidth.
  5. CDN / Front Door for read-heavy public content — the cheapest request is the one storage never sees.
  6. Respect the retries. The SDKs already back off exponentially on 503 ServerBusy. Do not replace that with your own tight retry loop; you will turn a throttle into an outage.

Observability

Diagnostic settings are off by default. Until you turn them on, you have platform metrics and nothing else — no record of who read what, no per-operation latency, no way to answer "when did this blob get deleted, and by whom".

What to turn on

Category Type Why
StorageRead Log Access audit, and the only way to see which blobs are actually hot
StorageWrite Log Ingestion audit
StorageDelete Log The one you'll want during an incident, and the one nobody enables beforehand
Transaction Metric Volume, latency, and response type — where throttling shows up
Capacity Metric Blob capacity, container count, blob count. Emitted at a low frequency (roughly daily)

Route them to a Log Analytics workspace. The diagnostic setting must target <accountId>/blobServices/default, not the account resource ID — pointing it at the account gets you nothing and no error.

The metrics worth alerting on

  • Transactions split by ResponseType, alerting on ServerBusyError and ServerTimeoutError — throttling, before users report slowness.
  • Availability below 100% sustained.
  • SuccessE2ELatency / SuccessServerLatency — the gap between them is your network and client, not Azure's service.
  • BlobCapacity growth rate — a step change usually means a versioning or soft-delete rule that isn't expiring anything.
  • Egress — a spike is either a legitimate bulk read or an exfiltration, and both are worth knowing about within the hour.

The KQL query you'll actually run

// Who is calling this account, how, and how often — the first query in any investigation.
StorageBlobLogs
| where TimeGenerated > ago(24h)
| summarize
    Calls      = count(),
    Failures   = countif(StatusText !in ("Success", "SASSuccess")),
    AvgLatency = avg(DurationMs)
  by OperationName, AuthenticationType, CallerIpAddress, StatusText
| order by Calls desc

Two more worth keeping:

// Deletions in the last 7 days — the "what happened to that file" query.
StorageBlobLogs
| where TimeGenerated > ago(7d) and OperationName has "Delete"
| project TimeGenerated, Uri, AuthenticationType, RequesterObjectId, CallerIpAddress, StatusText

// Throttling over time — is this a spike or a ceiling?
StorageBlobLogs
| where TimeGenerated > ago(24h) and StatusText contains "ServerBusy"
| summarize Throttled = count() by bin(TimeGenerated, 5m)
| render timechart

The activity log covers the other half: control-plane changes such as firewall edits, key rotations, and redundancy changes, with the identity that made them. Between activity log (who changed the resource) and storage logs (who touched the data), you can answer almost any incident question — but only if both were on beforehand.


Reliability

Choosing redundancy honestly

Setting Survives Doesn't survive Pick it for
LRS Disk, node, rack failure Loss of a datacentre Dev, and reproducible data you could regenerate
ZRS Loss of an entire availability zone Loss of the region ✅ The production default in any zone-enabled region
GRS Regional loss (with manual failover, non-zero RPO) Data you must not lose, where hours of RTO is acceptable
GZRS Zone loss with no action, plus regional loss with failover Critical data. The default for anything regulated
RA-GRS / RA-GZRS The above, plus immediate read access to the secondary Read-heavy systems that can serve slightly stale data during a regional event

Three things people get wrong:

  • Redundancy is not backup. Every redundancy option faithfully replicates a deletion. Only soft delete, versioning, point-in-time restore, and Azure Backup protect against a mistake or a malicious actor.
  • Geo-replication is asynchronous. Check the last sync time metric; everything after it is lost in an unplanned failover. If your RPO is zero, storage redundancy alone will not give it to you.
  • Failover is account-wide, disruptive, and historically leaves the account as LRS afterwards, requiring a manual re-upgrade. Practise it in a non-production account so the runbook is real.

Backup and restore

Layer them; each covers a different failure:

Protection Covers Must be enabled before
Soft delete (blob and container) Accidental delete The delete
Versioning Accidental overwrite The overwrite
Point-in-time restore Bulk corruption across a container The corruption — and it needs versioning + change feed + soft delete all on
Object replication Regional copy under your control, per-container, with your own rules The event
Azure Backup for blobs Policy-driven, vaulted, with its own RBAC boundary The event
Immutable storage (WORM) Ransomware and insider deletion, plus compliance The event, and a locked policy can't be shortened

The failure drill

Run these on a schedule, in a non-production account, and write down the times:

  1. Restore a single blob from soft delete and from a version. Time it. Confirm who has permission — during the real incident it will be someone who has never done it.
  2. Point-in-time restore a container to a timestamp ten minutes ago. Confirm the prerequisites are actually enabled in prod, not just in the drill account.
  3. Initiate a customer-managed failover on a GRS test account. Observe the last-sync-time gap, the duration, and the post-failover redundancy state.
  4. Break a lease on a blob that a stuck process is holding — you will need this the first time a Terraform apply dies mid-run.
  5. Revoke access: remove a role assignment and confirm the caller loses access within the propagation window; rotate a key and confirm nothing breaks (which, if you set allowSharedKeyAccess = false, it can't).

Next: Interview Questions →

← Back to the Blob Storage overview · ← Previous: Integrations