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

  1. The storage account: one resource, four services
  2. Blob Storage: containers and blob types
  3. Access tiers: Hot, Cool, Cold and Archive
  4. Lifecycle rules: Contoso's boarding passes
  5. Azure Files: the office file share
  6. Queue Storage: decoupling boarding pass issuing
  7. Table Storage and its relationship with Cosmos DB
  8. Redundancy: LRS, ZRS, GRS, GZRS and RA-GRS
  9. Access security: keys, SAS and identities
  10. Encryption, versioning, snapshots and soft delete
  11. Tools: az storage, AzCopy and Storage Explorer
  12. A complete example: the boarding pass from end to end
  13. Cleanup
  14. Common Mistakes and Tips
  15. Exercises
  16. Conclusion

  1. 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.

  1. 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 table

Note --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.

  1. 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:

  1. 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.
  2. 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.
  3. The tier applies per blob, even though the account has a default tier for new blobs.
  4. 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.

  1. 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.
  • tierToCool at 30 days, tierToArchive at 90, delete at 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 json

Two 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.

  1. 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}",serverino

On 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.

  1. 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 none

Queue 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.

  1. 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 none

The 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.

  1. 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 table

And 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.

  1. 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 none

There 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 none

Shared 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 none

The 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.

  1. 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.

  1. Tools: 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=false

A 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.

  1. 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 table

Test 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.

  1. 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 --yes

Remember: 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 prefixMatch wrong. 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-type on 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 false in 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:

  1. Production boarding passes in PDF, with a 5-year legal retention.
  2. Templates and spreadsheets that the Palma ground staff open from Windows Explorer.
  3. Boarding pass issuing requests waiting to be processed by the PDF generator.
  4. An audit record of every issue: which flight, which booking reference, at what time, to be queried by flight.
  5. Copies of the test files that Diego regenerates every week.

Exercise 2: temporary link and lifecycle

  1. Upload a test file to tarjetas-embarque in the development account with the correct content type.
  2. Generate a user delegation SAS, read only, valid for 10 minutes and over HTTPS only.
  3. Verify with curl that it works, and explain what response you expect once it expires.
  4. 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:

  1. Which storage accounts are there and with what redundancy?
  2. Does any of them allow public blob access or accept shared keys?
  3. Does any of them accept TLS below 1.2?
  4. 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
  1. Once the 10 minutes have passed, the same URL returns HTTP/1.1 403 Forbidden with the error code AuthenticationFailed: the signature includes the expiry and the service validates it on every request. There is nothing to delete or revoke.

  2. 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
done

In 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

Module 2: Core Azure Services

Module 3: Azure Databases

Module 4: Security in Azure

Module 5: Azure DevOps

Module 6: Advanced Azure Services

Module 7: Monitoring and Management

Module 8: Cost Management and Optimization

Module 9: Case Studies and Best Practices

© Copyright 2026. All rights reserved