Storage accounts & redundancy
Services, LRS/ZRS/GRS, securing access.
You have one irreplaceable notebook and a photocopier. Three copies in the same desk drawer survive a spilled coffee, but not an office fire. Spread the copies across three buildings in the same city and they survive the fire, but not the earthquake that flattens the city. Mail a set to a branch office a few hundred kilometres away and it survives the earthquake too. The catch is the post. The branch copy always runs a little behind the original. Azure storage redundancy is that exact decision, priced per copy: LRS (locally redundant storage) is the desk drawer, ZRS (zone-redundant storage) is the three buildings, GRS (geo-redundant storage) is the branch office, and the postal delay has a name, *asynchronous replication*, meaning the far copy is made in the background after your write has already been accepted.
A storage account is the Azure resource that holds the data and makes that copy decision on its behalf. Two things set it apart from a folder. First, its name has to be unique across every Azure customer on the planet, because the name becomes a public DNS name (Domain Name System, the internet's address book) like contosodata.blob.core.windows.net. Second, one account is four services under one roof: Blob for loose objects such as images, backups and logs; Files for managed network shares that speak SMB and NFS (Server Message Block and Network File System, the file-sharing protocols Windows and Linux already use); Queue for small messages passed between apps; and Table for simple key-value records. Redundancy, network rules, encryption and access keys are all set on the *account*, and they apply to everything inside it. That is why account design belongs to an administrator rather than to whoever happens to be writing the app, and why AZ-104 keeps circling back to it.
Creating an account: three choices you are stuck with
Three settings on the create command follow the account for life. Kind should be StorageV2 (general purpose v2) for almost anything you build. Version 1 exists for old accounts that were never migrated, while BlockBlobStorage and FileStorage are premium kinds backed by SSDs (solid state drives, flash storage with no moving parts), each dedicated to a single service. Performance is either Standard, which sits on spinning disks and bills you per gigabyte stored, or Premium, which sits on SSDs, is paid for up front by size, and exists for workloads that count milliseconds. Name rules bite people constantly: 3 to 24 characters, lowercase letters and digits only, unique across the whole of Azure, because that string turns into a hostname. Check the name is free before you write a script that assumes it.
# Account names are global DNS -- check availability firstaz storage account check-name --name contosodata# {# "message": null,# "nameAvailable": true,# "reason": null# }az group create -n storage-rg -l westeurope --output noneaz storage account create \--resource-group storage-rg \--name contosodata \--location westeurope \--kind StorageV2 \--sku Standard_ZRS \--access-tier Hot \--min-tls-version TLS1_2 \--allow-blob-public-access false \--query "{name:name, sku:sku.name, kind:kind, blobEndpoint:primaryEndpoints.blob}"# {# "blobEndpoint": "https://contosodata.blob.core.windows.net/",# "kind": "StorageV2",# "name": "contosodata",# "sku": "Standard_ZRS"# }
Two habits are hiding in that command. The first is --query. By default az prints a wall of JSON (JavaScript Object Notation, the text format Azure answers in), and the little query language it accepts, JMESPath, trims the reply down to the handful of fields you actually wanted. The second habit is setting the TLS floor (Transport Layer Security, the encryption that puts the S in HTTPS) and the public-access rule *at creation time* rather than later. Accounts that show up in breach write-ups are usually the ones where someone meant to tighten this next sprint.
Redundancy: how many copies, and how far apart
Every write to Azure Storage lands on at least three copies. The SKU you pick (stock keeping unit, Azure's word for a priced product tier) decides where those three copies live. LRS keeps all three inside one datacenter: eleven nines of durability (99.999999999% of objects survive a given year), the cheapest option on the menu, and gone if that one building floods. ZRS puts the three copies in three separate *availability zones*, which are distinct datacenters inside one region with their own power, cooling and network feeds. That buys twelve nines and survival of a whole zone going dark. GRS takes the LRS layout and copies everything, in the background, to Azure's designated *paired region* hundreds of kilometres away (West Europe pairs with North Europe). Sixteen nines. GZRS (geo-zone-redundant storage) is ZRS at home plus that geo copy, and it is the production default for data you would be fired for losing. The RA- prefix (read-access, as in RA-GRS and RA-GZRS) makes the far copy readable at a -secondary endpoint. Without it, the second region is a sealed vault you can only open by failing over.
The timing behind those copies decides how incident day goes, and the exam leans on it. Copies inside the primary region are synchronous: your write is not acknowledged until all three have committed. The cross-region copy is asynchronous: a background catch-up that usually lands within 15 minutes and carries no SLA (service level agreement, the promise Microsoft will actually pay out on) for how far behind it drifts.
Redundancy is not permanent, but the two directions of change behave nothing alike. Adding or removing the *geo* copy (LRS to GRS, ZRS to GZRS, or back again) is an ordinary SKU switch with az storage account update. It returns almost instantly, needs no downtime, bills a one-time egress charge while the secondary is seeded, and cannot be failed over until that first sync completes. Adding or removing *zone* redundancy is a different animal. It is a conversion, with its own dedicated command, in which Azure physically shuffles your data across zones in the background. A conversion normally *begins* within 72 hours of the request and sometimes takes longer, has no SLA on completion, and needs no downtime. Once it does finish, you have to wait at least another 72 hours before you change the account's redundancy again. Two hard walls: premium performance offers LRS and ZRS only, with no geo options, and a few newer regions have no paired region at all, so GRS is not offered there.
# What redundancy is the estate actually running?az storage account list \--query "[].{name:name, sku:sku.name, region:primaryLocation}" -o table# Name Sku Region# ----------- ------------ ----------# contosodata Standard_ZRS westeurope# legacylogs Standard_LRS westeurope# Adding geo-replication is a zero-downtime SKU switch:az storage account update -g storage-rg -n legacylogs \--sku Standard_RAGRS --query sku.name# "Standard_RAGRS"# Adding ZONE redundancy is a CONVERSION -- a dedicated command,# not an update:az storage account migration start -g storage-rg \--account-name auditarchive --sku Standard_ZRS \--name default --no-wait --yesaz storage account migration show -g storage-rg \--account-name auditarchive --name default \--query "{status:migrationStatus, target:targetSkuName}"# {# "status": "SubmittedForConversion",# "target": "Standard_ZRS"# }
Geo-replication health, and the call to fail over
If you run GRS or GZRS, one timestamp defines your disaster story: Last Sync Time. Every write stamped before it is safely in the second region. Everything after it exists only in the primary. That gap is your real RPO (recovery point objective, the amount of recent work you accept losing in a disaster). Read it *before* you fail over, because failing over is precisely the decision to abandon that work.
# How far behind is the secondary? lastSyncTime = your real RPO.az storage account show -g storage-rg -n legacylogs \--expand geoReplicationStats --query geoReplicationStats# {# "canFailover": true,# "canPlannedFailover": true,# "lastSyncTime": "2026-07-13T10:41:07+00:00",# "postFailoverRedundancy": "Standard_LRS",# "postPlannedFailoverRedundancy": "Standard_RAGRS",# "status": "Live"# }# Primary region is gone: promote the secondary (see warning below)az storage account failover -g storage-rg -n legacylogs --yes --no-wait# DR drills while the primary is still healthy: planned failover# swaps roles with no data loss expected and keeps geo-redundancyaz storage account failover -g storage-rg -n legacylogs \--failover-type Planned --yes
lastSyncTime is permanently lost. The API tells you the ending before you start: postFailoverRedundancy reads Standard_LRS. The copy in the original primary region is deleted, so your geo-redundant account becomes three copies in one datacenter until you re-enable GRS. Re-enabling it re-replicates every byte back across regions, billed as bandwidth, over hours or days. Rehearse disaster recovery with --failover-type Planned instead, which requires both regions to be healthy, swaps their roles, and keeps geo-redundancy intact. Save unplanned failover for the day nothing else is left.Who gets in the front door
Every account is born with two access keys. A key is one long string that grants read, write and delete across every service in the account, with no identity attached and no record of who used it. Treat one the way you treat the domain administrator password: never in source code or a config file, parked in Key Vault if some legacy app truly needs it, rotated on a schedule. Two keys exist for exactly that reason, so you can renew one while your apps keep running on the other. A shared access signature (SAS) is the polite version: a signed URL that grants named permissions on named resources and then expires. The answer you actually want is neither of those. Microsoft Entra ID (Azure's identity service, previously called Azure AD) with data-plane RBAC roles (role-based access control, the same grant model from the RBAC lesson, pointed at the data rather than the resource) gives access to named identities you can audit and revoke. Storage Blob Data Reader and Storage Blob Data Contributor are the two you will assign most.
Network posture is set on the account as well. Force HTTPS, put the TLS floor at 1.2, switch off anonymous blob access, and set the firewall's default action to *Deny* so only networks you named can connect. The Private Endpoints lesson closes that door the rest of the way. Encryption at rest is not a setting you can get wrong: Storage Service Encryption applies 256-bit AES (Advanced Encryption Standard, the workhorse cipher of modern security) to everything, always, and there is no off switch. Where a compliance regime insists that you hold the keys yourself, you can point the account at customer-managed keys in Key Vault.
# Baseline hardening every account should carry:az storage account update -g storage-rg -n contosodata \--https-only true \--min-tls-version TLS1_2 \--allow-blob-public-access false \--default-action Deny \--bypass AzureServices \--query "{https:enableHttpsTrafficOnly, tls:minimumTlsVersion, publicBlobs:allowBlobPublicAccess, firewall:networkRuleSet.defaultAction}"# {# "firewall": "Deny",# "https": true,# "publicBlobs": false,# "tls": "TLS1_2"# }# Keys: check their age, then rotate one at a timeaz storage account keys list -g storage-rg -n contosodata \--query "[].{key:keyName, created:creationTime}" -o table# Key Created# ----- --------------------------------# key1 2026-07-13T09:58:41.522041+00:00# key2 2026-07-13T09:58:41.522041+00:00az storage account keys renew -g storage-rg -n contosodata --key key1 --output none# Gold standard: shared-key auth off entirely -- Entra ID or nothingaz storage account update -g storage-rg -n contosodata \--allow-shared-key-access false --output none
One caution on that last command. Turning off shared-key access breaks anything still signing in with an account key or an account SAS, and that includes Azure Files SMB mounts, which authenticate with the account key. Inventory the callers first. It is the right place to end up, reached on purpose rather than by surprise.
What it costs, and where the exam sets traps
Redundancy multiplies every gigabyte you store. As a rough guide, ZRS costs about a quarter more than LRS, and the geo options roughly double the bill, on top of a per-gigabyte bandwidth charge for the cross-region post run. Scale limits rarely pinch a real workload but do turn up on the exam: one standard account holds 5 PiB by default (pebibytes; a pebibyte is a bit over a million gigabytes), and a subscription starts with 250 accounts per region. The traps AZ-104 keeps reusing: a GRS secondary is *not readable* unless you chose RA-GRS; redundancy belongs to the account, never to a container or a share; asynchronous replication has *no* lag SLA, so lastSyncTime is the only truth you have; premium performance means LRS or ZRS; and after an unplanned failover the account runs as LRS until you deliberately turn geo-redundancy back on.
The account is the vault. What you keep inside it is mostly blobs. Next you step into the Blob service itself: containers, the access tiers from Hot down to Archive, and lifecycle rules that demote aging data on their own, so the redundancy you picked here does not quietly become a billing problem. That is *Blob storage & lifecycle*.
One practical note before the lab. Pick account names from a scheme you can live with, because an account cannot be renamed. The name is a public hostname, so it is lowercase, unique worldwide, and it follows you into every connection string and every support ticket. A pattern built from organisation, workload and environment (contosobackupsprod, say) reads far better in an audit than a name someone typed at 4pm on a Friday. Give redundancy the same thought up front, since the zone half of that choice only moves through a conversion with no promise about when it finishes.
The day-one security list is short enough to memorise. HTTPS only. Minimum TLS 1.2. allowBlobPublicAccess set to false. Firewall default action Deny. A dated plan to disable shared-key access once your apps authenticate as managed identities. Private Endpoints for data sensitive enough that a public endpoint should not exist at all. And treat the two account keys as the data-plane super-user passwords they are: rotate them, keep them in Key Vault if you have to use them, and move applications onto Microsoft Entra ID authorization as quickly as you can.
Try this
Build a storage account with the TLS floor at 1.2 and public blob access switched off, then read the SKU and the transport settings back to prove they took. Notice while you are there that shared-key access is a switch you can flip later, once every caller has an identity of its own.
RG=rg-lab-saaz group create -n $RG -l eastusaz storage account create -g $RG -n contosolabsa$RANDOM -l eastus \--sku Standard_LRS --kind StorageV2 --min-tls-version TLS1_2 \--allow-blob-public-access falseaz storage account show -g $RG --name $(az storage account list -g $RG --query "[0].name" -o tsv) \--query "{sku:sku.name,https:enableHttpsTrafficOnly,tls:minimumTlsVersion,publicBlob:allowBlobPublicAccess}" -o json
$ az storage account show ... --query "{sku:sku.name,https:enableHttpsTrafficOnly,tls:minimumTlsVersion,publicBlob:allowBlobPublicAccess}" -o json{"sku": "Standard_LRS","https": true,"tls": "TLS1_2","publicBlob": false}# Sample output — LRS is cheapest; ZRS/GRS trade money for zone or region durability.
Takeaway
Hold on to this: one storage account serves blob, file, queue and table endpoints behind a single globally unique name, and redundancy (LRS, ZRS, GRS, GZRS) is a durability decision you make when you create it, for most SKUs.
Where to go next: shut the public network path, put Microsoft Entra ID with data-plane RBAC roles ahead of account keys, and give every production account a Private Endpoint.