The previous lesson left a clear rule: an application that scales horizontally cannot store anything on its local disk. And Contoso Airlines generates one file per sale: the boarding pass in PDF. That file has to live somewhere every instance can reach, somewhere that copes with millions of objects, that is cheap and that lets you give the customer a download link without opening the whole store to the internet.
That somewhere is Azure Storage. In module 1 you already created the sttarjetascontosodev account with its tarjetas-embarque container, mandatory HTTPS and anonymous access disabled, but you left one important decision pending: which redundancy to use in development and which one in production. This lesson picks up that loose end and ties it off.
You will learn what the four services that fit inside a storage account are, how Blob Storage works in detail (blob types, access tiers and lifecycle rules), what Azure Files, Queue Storage and Table Storage are for, how access is protected with shared access signatures and identities, and which tool to use in each case. At the end you will build Contoso's real flow: upload a boarding pass, generate a temporary link for the passenger and schedule the file to get cheaper by itself over time.
Cost warning: storage is billed per GB per month, per operation and per data transfer out. The examples in this lesson move kilobytes and cost cents, but forgotten storage accounts pile up. The cleanup is at the end.
Contents
- The storage account: one resource, four services
- Blob Storage: containers and blob types
- Access tiers: Hot, Cool, Cold and Archive
- Lifecycle rules: Contoso's boarding passes
- Azure Files: the office file share
- Queue Storage: decoupling boarding pass issuing
- Table Storage and its relationship with Cosmos DB
- Redundancy: LRS, ZRS, GRS, GZRS and RA-GRS
- Access security: keys, SAS and identities
- Encryption, versioning, snapshots and soft delete
- Tools:
az storage, AzCopy and Storage Explorer - A complete example: the boarding pass from end to end
- Cleanup
- Common Mistakes and Tips
- Exercises
- Conclusion
- The storage account: one resource, four services
A storage account is the billing, security and configuration container for up to four different data services, each with its own endpoint:
| Service | What it is for | Endpoint |
|---|---|---|
| Blob | Unstructured objects: PDFs, images, backups, logs | https://<account>.blob.core.windows.net |
| File | Network file shares over SMB or NFS | https://<account>.file.core.windows.net |
| Queue | Simple message queues for decoupling processes | https://<account>.queue.core.windows.net |
| Table | A very cheap key-value NoSQL store | https://<account>.table.core.windows.net |
When you create the account you choose two things that condition everything else:
| Decision | Options | What it implies |
|---|---|---|
| Account type | StorageV2 (general purpose v2), BlockBlobStorage (premium for blobs), FileStorage (premium for files) |
StorageV2 is the default option and the right one unless you have a specific need |
| Performance | Standard (HDD underneath) or Premium (SSD, low latency) | Standard supports all four services; Premium specializes by type |
And a restriction that surprises everyone: the account name is globally unique, between 3 and 24 characters, lowercase letters and numbers only. No hyphens. That is why Contoso's accounts are called sttarjetascontosodev and sttarjetascontosopro and do not follow the hyphenated pattern of the rest of the resources.
A reminder of what was created in module 1, with the complete command in case you need to rebuild it:
az storage account create \
--resource-group rg-contoso-reservas-dev \
--name sttarjetascontosodev \
--location westeurope \
--sku Standard_LRS \
--kind StorageV2 \
--https-only true \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--tags entorno=desarrollo proyecto=contoso-reservas \
centro-coste=CC-1042 [email protected] \
--output table--allow-blob-public-access false is the line that stops anyone from opening a container to the public by mistake. With it enabled at the account level, no container configuration can expose the files anonymously.
- Blob Storage: containers and blob types
The Blob Storage hierarchy has three levels and no more:
Storage account (sttarjetascontosopro)
└── Container (tarjetas-embarque)
└── Blob (2026/08/IB3241-20260814-A7K2.pdf)There are no real folders: what looks like a hierarchy is really blob names with slashes. Tools display it as if it were a tree, but to the service it is a flat name. This has a useful practical consequence: you can list "a folder" with a prefix, and it is an efficient operation.
The three blob types
| Type | How it works | Maximum size | Use cases |
|---|---|---|---|
| Block | Made up of blocks uploaded in parallel and then committed | ~190 TiB | 95% of cases: PDFs, images, video, backups, ZIPs |
| Append | Only allows appending at the end; optimized for sequential writes | ~195 GiB | Audit trails, log files |
| Page | Random access in 512-byte pages | 8 TiB | VM disks (managed disks are page blobs underneath) |
For Contoso's boarding passes: block blobs, without a doubt.
Basic operations with the CLI
ACCOUNT="sttarjetascontosodev"
CONTAINER="tarjetas-embarque"
# Create the container. --auth-mode login uses your Entra ID identity
# instead of the account key: it is the recommended way.
az storage container create \
--account-name "${ACCOUNT}" \
--name "${CONTAINER}" \
--auth-mode login \
--public-access off \
--output table
# Upload a boarding pass.
az storage blob upload \
--account-name "${ACCOUNT}" \
--container-name "${CONTAINER}" \
--name "2026/08/IB3241-20260814-A7K2.pdf" \
--file ./boarding-pass.pdf \
--content-type "application/pdf" \
--auth-mode login \
--overwrite
# List the August 2026 boarding passes using the prefix.
az storage blob list \
--account-name "${ACCOUNT}" \
--container-name "${CONTAINER}" \
--prefix "2026/08/" \
--auth-mode login \
--query "[].{Name:name, Bytes:properties.contentLength, Tier:properties.blobTier}" \
--output tableNote --content-type "application/pdf": without it, the browser may download the file instead of displaying it. It is a small detail that generates a lot of support tickets.
Naming design tip: 2026/08/IB3241-20260814-A7K2.pdf includes year, month, flight, date and booking reference. That scheme lets you list by period with prefixes and apply lifecycle rules per "folder". A flat name like pass12345.pdf works just as well for reading, but it leaves you with no tools for managing the lifecycle.
- Access tiers: Hot, Cool, Cold and Archive
Here is the mechanism that makes Azure Storage genuinely cheap. Each blob has an access tier that trades storage cost against retrieval cost and time.
| Tier | Storage cost | Access cost | Minimum retention | Availability | Intended use |
|---|---|---|---|---|---|
| Hot | The highest | The lowest | None | Immediate | Data in active use |
| Cool | ~50% less than Hot | Higher per operation and per GB read | 30 days | Immediate | Data from the last few months, occasional access |
| Cold | Lower than Cool | Higher than Cool | 90 days | Immediate | Data that is barely touched but must be available right away |
| Archive | By far the lowest | The highest, and with a wait | 180 days | Requires rehydration: 1 to 15 hours | Legal retention, historical backups |
Four rules that prevent nasty surprises:
- The minimum retention is billed even if you delete earlier. If you upload a blob to Cool and delete it after 3 days, you pay for 30. Changing tier too early also counts as early deletion.
- Archive cannot be read. A blob in Archive is offline: you have to rehydrate it (
az storage blob set-tier --rehydrate-priority High) and wait hours. It is useless for anything a customer might request on the spot. - The tier applies per blob, even though the account has a default tier for new blobs.
- Moving down a tier saves on storage but makes each read more expensive. If a file is read every week, Cool can work out more expensive than Hot.
The usage pattern of Contoso's boarding passes, measured by Diego Salas:
| Age of the boarding pass | Downloads | Appropriate tier |
|---|---|---|
| 0–7 days (before and during the flight) | Very high: the passenger opens it several times | Hot |
| 8–90 days (complaints, expense receipts) | Low but real | Cool |
| 91 days – 5 years (legal and tax retention) | Almost nil | Archive |
| More than 5 years | None | Delete |
That pattern translates directly into a lifecycle rule.
- Lifecycle rules: Contoso's boarding passes
A lifecycle management rule is a policy that Azure runs daily over the account's blobs, moving them between tiers or deleting them according to their age. It is automatic and free: you only pay for the operations it generates.
{
"rules": [
{
"enabled": true,
"name": "ciclo-tarjetas-embarque",
"type": "Lifecycle",
"definition": {
"filters": {
"blobTypes": [ "blockBlob" ],
"prefixMatch": [ "tarjetas-embarque/" ]
},
"actions": {
"baseBlob": {
"tierToCool": { "daysAfterModificationGreaterThan": 30 },
"tierToArchive": { "daysAfterModificationGreaterThan": 90 },
"delete": { "daysAfterModificationGreaterThan": 1825 }
},
"snapshot": {
"delete": { "daysAfterCreationGreaterThan": 90 }
}
}
}
}
]
}Reading the document, section by section:
filters.blobTypes: it only affects block blobs (a disk's page blobs must never be moved to Cool).filters.prefixMatch: the prefix starts with the container name, not with the blob name. It is the most frequent syntax mistake in these rules.tierToCoolat 30 days,tierToArchiveat 90,deleteat 1825 (5 years): exactly Contoso's retention policy.snapshot.delete: old snapshots also take up space and are billed.
Applying the rule:
# Save the JSON above as ciclo-tarjetas.json and apply it to the account.
az storage account management-policy create \
--account-name sttarjetascontosopro \
--resource-group rg-contoso-reservas-pro \
--policy @ciclo-tarjetas.json \
--output none
# Check the active policy.
az storage account management-policy show \
--account-name sttarjetascontosopro \
--resource-group rg-contoso-reservas-pro \
--output jsonTwo operational warnings: the policy takes up to 24 hours to run for the first time (do not expect to see the change in five minutes), and daysAfterModificationGreaterThan counts from the last modification, not from creation. If some process rewrites the files, the clock resets and nothing ever moves down a tier.
- Azure Files: the office file share
Azure Files offers network file shares reachable over SMB (the Windows protocol, also supported on Linux and macOS) and over NFS (premium accounts only). Unlike blobs, here there really is a file system, with folders, permissions and file locking.
Contoso has a clear case: in the Barcelona and Palma offices there is a \\office-server\operations file share with incident reports, templates and spreadsheets that the ground staff open daily from Windows. That share can be moved to Azure Files as it is, without changing the way anyone works.
| When to use | Blob Storage | Azure Files |
|---|---|---|
| An application uploading and downloading objects over HTTPS | Yes | It can, but it is not the natural fit |
| A file share mounted as a network drive | No | Yes |
| Legacy software that demands a file system path | No | Yes |
| Millions of objects accessed by URL | Yes | No |
| Cost per GB | Lower | Higher |
# 1. Create the file share with a 100 GiB quota.
az storage share-rm create \
--resource-group rg-contoso-reservas-pro \
--storage-account stoperacionescontosopro \
--name compartido-operaciones \
--quota 100 \
--output table
# 2. Mount it on Linux (the cifs-utils package must be installed).
sudo mkdir -p /mnt/operations
sudo mount -t cifs \
//stoperacionescontosopro.file.core.windows.net/compartido-operaciones \
/mnt/operations \
-o vers=3.1.1,username=stoperacionescontosopro,password="${ACCOUNT_KEY}",serverinoOn Windows it would be a net use Z: \\stoperacionescontosopro.file.core.windows.net\compartido-operaciones. Two important considerations:
- Port 445 (SMB) is blocked by many home and corporate internet providers. That is why mounting from an office demands, in practice, private connectivity: VPN or ExpressRoute. That is exactly what we will see in lesson 02-06.
- There is also Azure File Sync, which keeps an on-premises file server in sync with the Azure share and leaves only recently used files locally. It is the usual route for migrating an office file server without changing anything for the user.
- Queue Storage: decoupling boarding pass issuing
Queue Storage is a simple message queue service: a producer leaves a message, a consumer picks it up and processes it. Messages of up to 64 KB, up to millions in one queue, with "at least once" semantics.
Contoso's case: when a passenger buys a ticket, generating the boarding pass PDF takes a couple of seconds. Doing it inside the web request means the customer waits; if the generator fails, the purchase fails. With a queue, the website leaves a message and responds immediately; a separate process generates the PDF and uploads it to the container.
# Create the queue and enqueue an issuing request.
az storage queue create \
--account-name sttarjetascontosopro \
--name cola-emision-tarjetas \
--auth-mode login --output none
az storage message put \
--account-name sttarjetascontosopro \
--queue-name cola-emision-tarjetas \
--content '{"bookingRef":"A7K2","flight":"IB3241","date":"2026-08-14"}' \
--auth-mode login --output noneQueue Storage is the basic and cheap option. When you need publish/subscribe topics, sessions, transactions, guaranteed ordering or messages larger than 64 KB, the answer is Azure Service Bus; and for large-scale events, Event Grid and Event Hubs. All three are compared in lesson 06-05; here it is enough that you know the queue exists inside the storage account and what it is for.
- Table Storage and its relationship with Cosmos DB
Table Storage is a key-value NoSQL store with a flexible schema. Each entity has:
- PartitionKey: it groups entities; it determines distribution and performance.
- RowKey: it identifies the entity within the partition.
- Up to 252 more properties, with no fixed schema.
Its virtue is the price: storing millions of simple rows costs a fraction of what a relational database would cost. Its limit is querying: it is only fast when searching by PartitionKey + RowKey. Any other query scans the table.
az storage table create --account-name sttarjetascontosopro \
--name registroembarques --auth-mode login --output none
# An audit entity: partition by flight, row by booking reference.
az storage entity insert \
--account-name sttarjetascontosopro \
--table-name registroembarques \
--entity PartitionKey=IB3241 RowKey=A7K2 \
issued=2026-08-14T09:12:00Z gate=B14 \
--auth-mode login --output noneThe relationship with Cosmos DB: Azure Cosmos DB for Table offers the same API with global distribution, SLA-backed latency, indexes over every property and provisioned throughput. It is the natural evolution when Table Storage falls short. The full comparison and when to make the jump are in lesson 03-03.
A quick rule: if your data is a cheap audit table with key-based access, Table Storage. If you need varied queries, guaranteed latency or a global presence, Cosmos DB.
- Redundancy: LRS, ZRS, GRS, GZRS and RA-GRS
This is the loose end module 1 left behind. Azure Storage always keeps several copies of your data; what you choose is where those copies are, and that determines which failure you are protected against.
| Option | Copies and location | Protects against | Read access in the secondary region | Relative cost |
|---|---|---|---|---|
| LRS (local) | 3 copies in a single datacenter | Disk, rack or server failure | — | € |
| ZRS (zone) | 3 copies across three zones of the region | The loss of a complete datacenter | — | €€ |
| GRS (geo) | 3 local copies + 3 in the paired region | A regional disaster | No (the secondary is only readable after failover) | €€ |
| GZRS (zone + geo) | 3 zones in the primary + 3 local in the pair | Zone loss and regional disaster | No | €€€ |
| RA-GRS / RA-GZRS | Like GRS/GZRS, with read access to the secondary | The same, and it also allows reading from the secondary | Yes, through a -secondary endpoint |
€€€€ |
Details to know before deciding:
- The paired region of West Europe is North Europe (settled in lesson 01-02). Geo-replication is asynchronous: in a disaster you may lose the last few minutes of writes (the so-called recovery point, RPO, of around 15 minutes).
- Failing over to the paired region is an operation you initiate (
az storage account failover), not something instantaneous and automatic. - With RA-GRS you can always read from the secondary at
https://<account>-secondary.blob.core.windows.net, but that data may lag behind. It is fine for reports or lag-tolerant reads, not for serving a boarding pass that was just issued. - Redundancy can be changed later (
az storage account update --sku), although some jumps (to ZRS, for example) may require a requested migration.
Contoso Airlines' decision
| Account | Environment | Redundancy | Justification |
|---|---|---|---|
sttarjetascontosodev |
Development | LRS (Standard_LRS) |
The data is disposable and can be regenerated. Paying for geo-redundancy on test files is throwing money away |
sttarjetascontosopro |
Production | GZRS (Standard_GZRS) |
Boarding passes are a passenger document with a retention obligation. GZRS covers the loss of a zone (Contoso's policy) and a regional disaster |
stoperacionescontosopro |
Production, office files | ZRS (Standard_ZRS) |
Important operational content, but rebuildable from the source systems; zone protection is enough |
# Apply the decision in production.
az storage account update \
--resource-group rg-contoso-reservas-pro \
--name sttarjetascontosopro \
--sku Standard_GZRS \
--output table
# Check the redundancy of every account in the subscription.
az storage account list \
--query "[].{Account:name, Redundancy:sku.name, Region:location, Group:resourceGroup}" \
--output tableAnd a warning that has to be said out loud: redundancy is not a backup. GZRS faithfully replicates deletions and overwrites. If some process deletes August's boarding passes, they are deleted in all six copies at once. What protects against that is the versioning and soft delete of section 10, and Azure Backup in lesson 07-05.
- Access security: keys, SAS and identities
There are three ways to authorize access to the data, and they are listed from worst to best.
Account keys
Each account has two 512-bit keys that give full control over all the content, with no expiry and no associated identity.
# View the keys (and why that should make you uncomfortable).
az storage account keys list \
--resource-group rg-contoso-reservas-dev \
--account-name sttarjetascontosodev \
--output table
# Rotate the primary key.
az storage account keys renew \
--resource-group rg-contoso-reservas-dev \
--account-name sttarjetascontosodev \
--key primary --output noneThere are two keys precisely so that you can rotate without downtime: you point the applications at the secondary, renew the primary, switch the applications and renew the secondary. Even so, the recommendation is not to use them at all: if a key leaks into a repository, anyone can read and delete everything. You can disable them completely:
az storage account update \
--resource-group rg-contoso-reservas-pro \
--name sttarjetascontosopro \
--allow-shared-key-access false --output noneShared access signatures (SAS)
A SAS is a URL with limited permissions and an expiry. It is the way to give a passenger access to their boarding pass and nothing else.
| SAS type | How it is signed | Scope | Revocation |
|---|---|---|---|
| Service SAS | With the account key | One specific resource (a blob, a container) | By rotating the key or with a stored access policy |
| Account SAS | With the account key | Several services and management operations | By rotating the key |
| User delegation SAS | With an Entra ID key, not with the account key | Blobs | By revoking the delegation key, without touching the account |
The user delegation one is the recommended type: it does not require the application to know the account key, it is tied to an identity and it can be revoked without breaking everything else.
# User delegation SAS: read access to one specific blob, valid for 15 minutes.
EXPIRY=$(date -u -d "15 minutes" '+%Y-%m-%dT%H:%MZ')
SAS=$(az storage blob generate-sas \
--account-name sttarjetascontosopro \
--container-name tarjetas-embarque \
--name "2026/08/IB3241-20260814-A7K2.pdf" \
--permissions r \
--expiry "${EXPIRY}" \
--https-only \
--as-user --auth-mode login \
--full-uri --output tsv)
echo "Temporary link: ${SAS}"A breakdown of the options, because each one is a security decision:
| Option | Effect |
|---|---|
--permissions r |
Read only. Permissions are composed of letters: r read, w write, d delete, l list, a add, c create |
--expiry |
The expiry. Always short: minutes or hours, never months |
--https-only |
The URL does not work over HTTP |
--as-user --auth-mode login |
A user delegation SAS, signed with Entra ID and not with the key |
--full-uri |
Returns the complete URL, ready to hand over |
SAS good practices that Contoso applies: a 15-minute expiry for passenger downloads, minimum permissions, generation on the server (never in the browser), and stored access policies at the container level when bulk revocation is needed.
Access through a Microsoft Entra ID identity
The right option for an application to access the data: no keys, no SAS, with specific RBAC permissions and auditing.
# Give the bookings website permission to write boarding passes, using its managed identity.
APP_ID=$(az webapp identity assign \
--resource-group rg-contoso-reservas-pro \
--name app-contoso-reservas-pro \
--query principalId --output tsv)
ACCOUNT_ID=$(az storage account show \
--resource-group rg-contoso-reservas-pro \
--name sttarjetascontosopro --query id --output tsv)
az role assignment create \
--assignee "${APP_ID}" \
--role "Storage Blob Data Contributor" \
--scope "${ACCOUNT_ID}" --output noneThe most used data roles: Storage Blob Data Reader (read), Storage Blob Data Contributor (read and write) and Storage Blob Data Owner (plus managing POSIX permissions). Watch out for a classic confusion: Azure's Contributor role allows managing the account, but it does not grant access to the data except through the keys. Managed identities and RBAC are developed in lesson 04-02.
- Encryption, versioning, snapshots and soft delete
Encryption at rest: everything in Azure Storage is encrypted with 256-bit AES, always and at no cost. You can use Microsoft-managed keys (the default) or your own keys in Key Vault (04-03) when regulatory compliance demands it.
Encryption in transit: --https-only true and --min-tls-version TLS1_2, already applied to Contoso's account back in module 1.
Protection against deletion, which is what redundancy does not cover:
ACCOUNT="sttarjetascontosopro"
GROUP="rg-contoso-reservas-pro"
# 1. Blob soft delete: 30 days to recover whatever was deleted.
az storage account blob-service-properties update \
--account-name "${ACCOUNT}" --resource-group "${GROUP}" \
--enable-delete-retention true --delete-retention-days 30 --output none
# 2. Soft delete for whole containers.
az storage account blob-service-properties update \
--account-name "${ACCOUNT}" --resource-group "${GROUP}" \
--enable-container-delete-retention true --container-delete-retention-days 30 --output none
# 3. Versioning: every overwrite keeps the previous version.
az storage account blob-service-properties update \
--account-name "${ACCOUNT}" --resource-group "${GROUP}" \
--enable-versioning true --output none| Mechanism | What it protects against | Cost |
|---|---|---|
| Soft delete | Accidental deletion of blobs or containers | The space of the deleted data is billed during the retention period |
| Versioning | Accidental overwrite or ransomware encryption | Every version takes space and is billed: combine it with lifecycle rules |
| Snapshots | A manual rollback point before a change | Only the modified blocks |
| Immutability lock (WORM) | Deliberate tampering; legal compliance | The blob cannot be deleted or modified during the set period |
Contoso enables 30-day soft delete and versioning in production, and adds the deletion of old versions to the lifecycle rule so that versioning does not blow up the bill.
- Tools:
az storage, AzCopy and Storage Explorer
az storage, AzCopy and Storage Explorer| Tool | When to use it | Strength |
|---|---|---|
az storage (Azure CLI) |
Automation, scripts, pipelines | You already have it installed; it integrates with the rest of the commands |
| AzCopy | Bulk transfers and synchronization | Much faster: it parallelizes, resumes transfers and synchronizes |
| Azure Storage Explorer | Visual exploration and debugging | A cross-platform graphical interface, very handy for seeing what is really there |
| SDKs (Java, .NET, Python, JS) | From the application's code | Retries, streams and identity-based authentication built in |
# AzCopy with Entra ID sign-in (no keys).
azcopy login
# Copy a whole directory of historical boarding passes, recursively.
azcopy copy "./boarding-passes-2025/" \
"https://sttarjetascontosopro.blob.core.windows.net/tarjetas-embarque/2025/" \
--recursive=true
# Sync: it only uploads what has changed. Ideal for repeated migrations.
azcopy sync "./boarding-passes-2025/" \
"https://sttarjetascontosopro.blob.core.windows.net/tarjetas-embarque/2025/" \
--recursive=true --delete-destination=falseA practical rule: to move more than a few hundred megabytes or more than a few hundred files, use AzCopy. az storage blob upload-batch works, but it is noticeably slower.
- A complete example: the boarding pass from end to end
This script brings everything above together in Contoso's real flow: prepare the account with the redundancy decided on, upload the boarding pass, hand a temporary link to the passenger and schedule the automatic cost reduction.
#!/usr/bin/env bash
set -euo pipefail
# ---------- Parameters ----------
GROUP="rg-contoso-reservas-dev"
ACCOUNT="sttarjetascontosodev"
CONTAINER="tarjetas-embarque"
BOOKING_REF="A7K2"
FLIGHT="IB3241"
FLIGHT_DATE="2026-08-14"
BLOB="$(date -d "${FLIGHT_DATE}" '+%Y/%m')/${FLIGHT}-$(date -d "${FLIGHT_DATE}" '+%Y%m%d')-${BOOKING_REF}.pdf"
# ---------- 1. Private container (idempotent) ----------
az storage container create \
--account-name "${ACCOUNT}" --name "${CONTAINER}" \
--public-access off --auth-mode login --output none
# ---------- 2. Upload the boarding pass with its content type ----------
az storage blob upload \
--account-name "${ACCOUNT}" --container-name "${CONTAINER}" \
--name "${BLOB}" --file "./boarding-pass-${BOOKING_REF}.pdf" \
--content-type "application/pdf" \
--content-disposition "inline; filename=\"boarding-pass-${FLIGHT}.pdf\"" \
--tier Hot \
--auth-mode login --overwrite --output none
echo "Boarding pass uploaded as ${BLOB}"
# ---------- 3. Temporary link for the passenger (15 minutes, read only) ----------
EXPIRY=$(date -u -d "15 minutes" '+%Y-%m-%dT%H:%MZ')
LINK=$(az storage blob generate-sas \
--account-name "${ACCOUNT}" --container-name "${CONTAINER}" --name "${BLOB}" \
--permissions r --expiry "${EXPIRY}" --https-only \
--as-user --auth-mode login --full-uri --output tsv)
echo "Link for the passenger (expires ${EXPIRY}): ${LINK}"
# ---------- 4. Lifecycle rule: Cool after 30 days ----------
cat > ciclo-tarjetas.json <<'EOF'
{
"rules": [
{
"enabled": true,
"name": "ciclo-tarjetas-embarque",
"type": "Lifecycle",
"definition": {
"filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["tarjetas-embarque/"] },
"actions": {
"baseBlob": {
"tierToCool": { "daysAfterModificationGreaterThan": 30 },
"tierToArchive": { "daysAfterModificationGreaterThan": 90 },
"delete": { "daysAfterModificationGreaterThan": 1825 }
}
}
}
}
]
}
EOF
az storage account management-policy create \
--account-name "${ACCOUNT}" --resource-group "${GROUP}" \
--policy @ciclo-tarjetas.json --output none
echo "Lifecycle policy applied."
# ---------- 5. Verification ----------
az storage blob show \
--account-name "${ACCOUNT}" --container-name "${CONTAINER}" --name "${BLOB}" \
--auth-mode login \
--query "{Name:name, Tier:properties.blobTier, Bytes:properties.contentLength, Type:properties.contentSettings.contentType}" \
--output tableTest the link with curl -I "${LINK}": it should return HTTP/1.1 200 OK. Wait for it to expire and repeat: you will get a 403 with the code AuthenticationFailed. That is exactly the protection we are after: the link is good for downloading the boarding pass now, not forever.
- Cleanup
# Delete the test blobs under a prefix.
az storage blob delete-batch \
--account-name sttarjetascontosodev \
--source tarjetas-embarque \
--pattern "2026/08/*" \
--auth-mode login
# Remove the lifecycle policy if it was only a test.
az storage account management-policy delete \
--account-name sttarjetascontosodev \
--resource-group rg-contoso-reservas-dev
# Delete the whole account (lab only).
# az storage account delete --name sttarjetascontosodev \
# --resource-group rg-contoso-reservas-dev --yesRemember: with soft delete enabled, deleted blobs keep taking up space and being billed for the retention days. That is the right thing for production, but bear it in mind when cleaning up a lab.
Common Mistakes and Tips
- Handing out the account key to give access to one file. It grants full control over everything. Use user delegation SAS with a short expiry, or managed identities.
- Generating SAS with an expiry of months or years. A URL with a year of life ends up in an email, in a ticket and in a search engine. Minutes, not months.
- Uploading to Archive anything a customer might request today. Archive is offline: rehydration takes hours. Never for tomorrow's flight boarding passes.
- Forgetting the minimum retention. Moving to Cool and deleting after three days costs the full 30 days. Tune the policy to the reality of your data.
- Confusing redundancy with backup. GZRS replicates deletions faithfully. Turn on soft delete and versioning.
- Getting the lifecycle rule's
prefixMatchwrong. It starts with the container name, not with the blob's. It is the most common and most silent failure: the rule does not error, it simply does nothing. - Expecting the rule to act instantly. It runs daily and the first time it can take 24 hours.
- Not setting the
content-typeon upload. The PDF gets downloaded instead of displayed; the support tickets follow. - Using Azure Files where blobs are enough. Files costs more per GB and adds a dependency on port 445.
- Turning on versioning without lifecycle rules. Every overwrite creates a version that is billed forever. Always combine the two.
- Tip: set
--allow-shared-key-access falsein production. It forces all access through Entra ID and removes an entire class of leaked-key incidents in one go. - Tip: design the blob name with a date hierarchy (
yyyy/mm/). It gives you prefix-based listings and per-period lifecycle policies practically for free.
Exercises
Exercise 1: choosing service, tier and redundancy
For each piece of Contoso Airlines data, state which service of the storage account you would use, which access tier and which redundancy, with your justification:
- Production boarding passes in PDF, with a 5-year legal retention.
- Templates and spreadsheets that the Palma ground staff open from Windows Explorer.
- Boarding pass issuing requests waiting to be processed by the PDF generator.
- An audit record of every issue: which flight, which booking reference, at what time, to be queried by flight.
- Copies of the test files that Diego regenerates every week.
Exercise 2: temporary link and lifecycle
- Upload a test file to
tarjetas-embarquein the development account with the correct content type. - Generate a user delegation SAS, read only, valid for 10 minutes and over HTTPS only.
- Verify with
curlthat it works, and explain what response you expect once it expires. - Write the lifecycle policy that moves to Cool at 30 days, to Archive at 90 and deletes at 5 years.
Exercise 3: storage audit
Write the Azure CLI commands that answer these questions about the subscription:
- Which storage accounts are there and with what redundancy?
- Does any of them allow public blob access or accept shared keys?
- Does any of them accept TLS below 1.2?
- Which production accounts have no lifecycle policy?
Solutions
Solution 1:
| Data | Service | Tier | Redundancy | Justification |
|---|---|---|---|---|
| 1. Production boarding passes | Blob (block) | Hot → Cool (30 d) → Archive (90 d) | GZRS | A passenger document with legal retention: zone and geo protection; the lifecycle rule makes long retention cheap |
| 2. Office templates | File (SMB) | — | ZRS | It needs to be mounted as a network drive from Windows; the content is rebuildable, so zone protection is enough |
| 3. Pending requests | Queue | — | The account's (ZRS/GZRS) | Ephemeral messages that decouple the website from the PDF generator |
| 4. Audit record by flight | Table | — | The account's | Cheap key-value: PartitionKey = flight, RowKey = booking reference, which is exactly the query pattern |
| 5. Diego's test files | Blob | Hot | LRS | Disposable, regenerable data: geo-redundancy would be pure waste |
Solution 2:
#!/usr/bin/env bash
set -euo pipefail
ACCOUNT="sttarjetascontosodev"
CONTAINER="tarjetas-embarque"
BLOB="2026/08/TEST-20260814-DEMO.pdf"
# 1. Upload with the correct content type.
az storage blob upload \
--account-name "${ACCOUNT}" --container-name "${CONTAINER}" \
--name "${BLOB}" --file ./test.pdf \
--content-type "application/pdf" \
--auth-mode login --overwrite --output none
# 2. User delegation SAS, read, 10 minutes, HTTPS only.
EXPIRY=$(date -u -d "10 minutes" '+%Y-%m-%dT%H:%MZ')
LINK=$(az storage blob generate-sas \
--account-name "${ACCOUNT}" --container-name "${CONTAINER}" --name "${BLOB}" \
--permissions r --expiry "${EXPIRY}" --https-only \
--as-user --auth-mode login --full-uri --output tsv)
# 3. Verification.
curl -I "${LINK}" # Expected now: HTTP/1.1 200 OK-
Once the 10 minutes have passed, the same URL returns
HTTP/1.1 403 Forbiddenwith the error codeAuthenticationFailed: the signature includes the expiry and the service validates it on every request. There is nothing to delete or revoke. -
The policy:
{
"rules": [{
"enabled": true,
"name": "ciclo-tarjetas-embarque",
"type": "Lifecycle",
"definition": {
"filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["tarjetas-embarque/"] },
"actions": {
"baseBlob": {
"tierToCool": { "daysAfterModificationGreaterThan": 30 },
"tierToArchive": { "daysAfterModificationGreaterThan": 90 },
"delete": { "daysAfterModificationGreaterThan": 1825 }
}
}
}
}]
}Solution 3:
# 1. Accounts and redundancy.
az storage account list \
--query "[].{Account:name, Redundancy:sku.name, Region:location, Group:resourceGroup}" \
--output table
# 2. Public blob access or shared keys allowed.
az storage account list \
--query "[?allowBlobPublicAccess==\`true\` || allowSharedKeyAccess==\`true\`].{Account:name, Public:allowBlobPublicAccess, SharedKey:allowSharedKeyAccess}" \
--output table
# 3. TLS below 1.2.
az storage account list \
--query "[?minimumTlsVersion!='TLS1_2'].{Account:name, TLS:minimumTlsVersion}" \
--output table
# 4. Production accounts with no lifecycle policy (a loop, because
# the policy is a subresource and does not appear in the account list).
for ACCOUNT in $(az storage account list \
--query "[?tags.entorno=='produccion'].name" -o tsv); do
GROUP=$(az storage account show -n "${ACCOUNT}" --query resourceGroup -o tsv)
if ! az storage account management-policy show \
--account-name "${ACCOUNT}" --resource-group "${GROUP}" \
--output none 2>/dev/null; then
echo "NO LIFECYCLE POLICY: ${ACCOUNT} (${GROUP})"
fi
doneIn module 4 you will see how to turn these checks into Azure Policy so that they do not depend on somebody remembering to run them.
Conclusion
You have tied off the loose end module 1 left behind and, along the way, learned Azure's most cross-cutting data service. You know that a storage account hosts four services — Blob, File, Queue and Table — each with its own endpoint, and that its name is globally unique and lowercase. You know Blob Storage in detail: containers, the flat hierarchy with prefixes, the three blob types and the Hot, Cool, Cold and Archive access tiers with their trade-off between storage and retrieval cost, the minimum retention periods and Archive rehydration. You have translated the real usage pattern of Contoso's boarding passes — heavy in the first week, almost nothing afterwards — into a lifecycle rule that makes them cheaper by itself. You know when Azure Files makes sense for the office file share, what Queue Storage is for as a decoupler and what relationship Table Storage has with Cosmos DB. And you have settled Contoso's redundancy decision: LRS in development, GZRS for the production boarding passes and ZRS for the operations files, with the essential warning that redundancy is not a backup, which is why you turned on soft delete and versioning. On access security, you have walked down the ladder from bad to good: account keys (avoid and disable), service, account and user delegation SAS with short expiries, and access through a Microsoft Entra ID identity, which is the final destination.
Look at what you already have running: elastic compute, published applications and storage with its lifecycle policy. But all of that, as it stands, talks over the internet. The website reaches the API by its public name, the application reaches storage through its public endpoint, and the database coming in module 3 would be just as exposed. No serious architecture stays like that.
In the next lesson, Azure Networking: Virtual Networks, Subnets and NSGs, we take that step: you will plan the address space of vnet-contoso-pro with CIDR arithmetic explained from scratch, you will design the snet-web, snet-app, snet-datos and snet-gestion subnets, you will write network security group rules that open only what is strictly necessary, you will understand VNet peering and the hub-and-spoke pattern, and you will see the decisive difference between service endpoints and Azure Private Link — which is exactly what will stop the sttarjetascontosopro account and the bookings database from being reachable from the internet.
Azure Course
Module 1: Introduction to Azure
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
