Storage and Data Cost Optimization
Why this matters
Compute waste is loud — a big VM shows up at the top of every cost chart. Storage and data waste is quiet: an orphaned disk from a deleted cluster billing $40 a month for a year, a terabyte of logs sitting in premium storage that nobody has queried since Q1, a Log Analytics workspace ingesting and retaining data at full-price for two years because nobody changed the default. None of it screams; all of it adds up. This module is the cleanup and tiering half of the Optimize phase — including the exact orphaned-disk trap you were warned about in track 14 and the Log Analytics retention cost flagged in track 12, now met head-on with the sweep-and-tier discipline that catches them systematically instead of by accident.
Concepts
Orphaned resources: paying for things nothing uses (track 14)
An orphaned resource is one that still exists and still bills but is attached
to nothing and serves no purpose. The canonical example — flagged directly in
track 14 modules 01-02 — is an unattached managed disk: a StatefulSet's
PVC used a disk with reclaimPolicy: Retain, the PVC/pod was deleted, and the
disk was left behind, still provisioned, still billing, disconnected from any
VM or node. Because it's not attached to anything, it never appears in a
compute-utilization view; it just quietly accrues. The family of orphans is larger
than disks: unattached public IPs (a static IP reserved after its load balancer
was deleted), unused NAT gateways / load balancers, empty App Service plans,
old snapshots, disks/NICs from deleted VMs, and idle databases. The
defining trait is the same: provisioned but unattached/unused, so it evades
utilization-based tools (module 03) entirely. Advisor catches some orphans, but
the reliable way to find them is a deliberate sweep — an Azure Resource Graph
query (track 17 module 06's estate-wide query tool) for resources in an unattached
state. Finding and cleaning at least one real orphan is a hard capstone requirement
precisely because everyone has one and nobody notices.
Blob storage tiers: hot, cool, cold, archive
Azure Blob Storage offers access tiers that trade retrieval cost/latency for storage cost, and using the wrong tier for the access pattern is pervasive waste:
- Hot — highest storage cost, lowest access cost. For data read/written frequently.
- Cool — lower storage cost, higher access cost, 30-day minimum. For data accessed occasionally (backups from the last month, recent logs).
- Cold — lower still, 90-day minimum, higher access cost. Infrequent access.
- Archive — cheapest storage by far, but offline: retrieval takes hours (rehydration) and costs the most. For compliance/retention data you're legally required to keep but expect never to read.
The lever is matching tier to access pattern, and doing it automatically via lifecycle management policies — rules like "move blobs to Cool after 30 days of no access, Archive after 90, delete after 365." Manually re-tiering doesn't scale; the lifecycle policy is the real tool. The trap runs both ways: leaving never-read data in Hot wastes storage cost, but archiving data you do read occasionally can cost more (early-deletion penalties + steep retrieval), so the tier has to match the actual pattern — the same "don't optimize on the average, look at the real behavior" discipline as rightsizing (module 03).
Managed disk tiers, and the "provisioned, not used" model
Managed disks (the VM/AKS block storage from tracks 07/14) also have performance tiers — Premium SSD (v1/v2), Standard SSD, Standard HDD, and Ultra — and here's the cost model that surprises people: for most disk types you pay for the provisioned size and tier, not for what you actually store or how much IOPS you use. A 1 TiB Premium SSD attached to a VM using 40 GiB bills for 1 TiB of Premium — full stop. So disk optimization has three moves: right-tier (a dev disk doesn't need Premium; Standard SSD/HDD is often fine), right-size (don't provision 1 TiB for a 40 GiB workload — though shrinking a disk is hard/risky, so size correctly up front), and — the biggest, cheapest win — delete orphans (the unattached disks above). Snapshots follow a similar "you pay for it sitting there" model and pile up from backup jobs (track 14 module 04) that never prune. The mental model that ties storage together: provisioned capacity bills whether or not you use it — the same trap as an over-provisioned VM, just quieter because storage rarely tops the chart.
Log Analytics and data-ingestion cost (track 12)
Observability has a cost tail that track 12 (module 04 logging, and the Container Insights/Log Analytics work in track 07 module 06) flagged and this module confronts. A Log Analytics workspace bills on two axes: ingestion (per GB of data you send in — the dominant cost, driven by how chatty your logs are) and retention (how long you keep it — an interactive retention window, often a default like 30-90 days included, then per-GB-per-month beyond it, plus cheaper long-term archive retention for compliance). The classic silent waste: a workspace left at a long default retention, ingesting verbose debug logs at full price, from a Container Insights integration nobody tuned. The levers: reduce ingestion (drop noisy log categories, sample, use Basic/Auxiliary table plans for high-volume low-value logs rather than the full Analytics plan), tune retention to what you actually query interactively vs. what you archive cheaply for compliance, and set a daily cap as a guardrail against a runaway log-flood blowing the bill. This is where observability (track 12) and cost meet: more telemetry is not free, and "log everything forever" is a budget decision disguised as an engineering default.
Sweep systematically: cleanup as a recurring practice, not a one-off
Every category here — orphans, wrong tiers, stale snapshots, over-retained logs —
shares a failure mode: it accumulates silently and continuously, so a one-time
cleanup fixes today and the waste is back in three months. The Operate-phase
answer (module 00) is to make the sweep recurring: a scheduled Resource Graph
query for orphans, lifecycle policies that tier/delete automatically, retention
set once as policy, and — ideally — governance policy (track 17) that prevents
some of it (e.g. deny disks above a size in dev, require lifecycle rules on new
storage accounts). Cleanup you do by hand once is a chore; cleanup encoded as a
recurring query + automatic policies is the discipline. Tie it back to tagging
(track 17 module 06): an Owner tag on a resource is what lets a sweep say
"this orphan belongs to team-payments, ask them before deleting" instead of the
scary "is anyone using this?" — which is exactly why the taxonomy matters for
cleanup, not just allocation.
Command reference
Resource Graph queries, tier reads, and Advisor are free. The disk/storage exercises create tiny resources — clean them up. Deleting an orphan stops a bill, so these commands save money.
| Command | What it does | Example |
|---|---|---|
az disk list --query "[?managedBy==null]" |
Finds orphaned (unattached) disks — managedBy==null means attached to no VM (track 14) |
az disk list --query "[?managedBy==null].{name:name, gb:diskSizeGb, rg:resourceGroup}" -o table |
az disk delete |
Deletes an orphaned disk (stops its billing) | az disk delete --ids <disk-id> --yes |
az graph query |
Estate-wide orphan sweep across subscriptions (track 17 module 06) | see breakdown below |
az network public-ip list --query "[?ipConfiguration==null]" |
Finds unattached public IPs (another common orphan) | az network public-ip list --query "[?ipConfiguration==null].name" -o table |
az storage account management-policy create |
Sets a Blob lifecycle policy (auto-tier/delete by age) | see breakdown below |
az storage blob set-tier |
Manually moves a blob between Hot/Cool/Cold/Archive | az storage blob set-tier --tier Cool --name <blob> -c <container> --account-name <sa> |
az monitor log-analytics workspace update --retention-time |
Sets Log Analytics retention days (track 12) | az monitor log-analytics workspace update -g rg -n ws --retention-time 30 |
az monitor log-analytics workspace show --query workspaceCapping |
Reads/sets the daily ingestion cap guardrail | az monitor log-analytics workspace show -g rg -n ws --query workspaceCapping |
Flag breakdown — estate-wide orphaned-disk sweep with Resource Graph (track 17 module 06's tool):
az graph query -q "Resources
| where type =~ 'microsoft.compute/disks'
| where isnull(properties.managedBy) or properties.diskState == 'Unattached'
| project name, resourceGroup, subscriptionId, sizeGb=properties.diskSizeGB, tags" -o table
type =~ 'microsoft.compute/disks'— case-insensitive match on the disk resource type across every subscription you can see.isnull(properties.managedBy) or properties.diskState == 'Unattached'— the orphan test: a disk owned by no VM / in theUnattachedstate — the track-14 trap.project ... sizeGb, tags— pull the size (to estimate the ongoing bill) and thetagsso you can see theOwner(track 17) before deleting — "whose is this?"
Flag breakdown — a Blob lifecycle policy that tiers then deletes automatically:
az storage account management-policy create --account-name <sa> -g <rg> --policy '{
"rules": [{
"name": "tier-then-expire",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": {"blobTypes": ["blockBlob"]},
"actions": {"baseBlob": {
"tierToCool": {"daysAfterModificationGreaterThan": 30},
"tierToArchive": {"daysAfterModificationGreaterThan": 90},
"delete": {"daysAfterModificationGreaterThan": 365}
}}
}
}]
}'
tierToCool: 30/tierToArchive: 90— auto-demote blobs as they age, matching tier to a time-based access assumption (validate it against real access).delete: 365— auto-expire after a year (respect any legal retention first).- The policy runs automatically — the point is you set the pattern once and
stop hand-tiering, the scalable version of
az storage blob set-tier.
Hands-on exercises
Sweeps, tier reads, and retention changes are free. The disk/storage exercises create tiny billable resources and then delete them — deleting an orphan is the lesson. Remember the 8-24h cost lag when confirming a bill stopped.
Sweep your estate for orphaned disks (the track-14 trap, for real). Run the Resource Graph orphan query from the command reference (add the extension with
az extension add --name resource-graphif needed). Every established Azure user has at least one. Write down each orphan's size and estimate its monthly cost — this is money you're spending on nothing.Deliberately create an orphan, then find and kill it. Reproduce the track-14 trap end to end:
az group create -n rg-storage-lab -l eastus az disk create -g rg-storage-lab -n orphan-demo --size-gb 32 --sku Standard_LRS # it's attached to no VM — it's born orphaned and already billing: az disk list -g rg-storage-lab --query "[?managedBy==null].{name:name, gb:diskSizeGb, state:diskState}" -o table az disk delete -g rg-storage-lab -n orphan-demo --yes # stops the billYou just felt the whole lifecycle: a disk billing while attached to nothing, and the one command that stops it. This is what track 14's warning was about.
Sweep for unattached public IPs and other orphans. Run
az network public-ip list --query "[?ipConfiguration==null].{name:name, rg:resourceGroup}" -o table. Extend the idea: write a Resource Graph query for another orphan class (old snapshots, or NICs with no VM). Note that each class needs its own "unattached" predicate — there's no single "show me waste" button, which is why the sweep is a practice.Read and change Blob tiers. Create a storage account and a blob, inspect its tier, and demote it:
SA=coststore$RANDOM az storage account create -n "$SA" -g rg-storage-lab --sku Standard_LRS az storage container create --account-name "$SA" -n data echo "cold data" > sample.txt az storage blob upload --account-name "$SA" -c data -f sample.txt -n sample.txt az storage blob show --account-name "$SA" -c data -n sample.txt --query "properties.blobTier" az storage blob set-tier --account-name "$SA" -c data -n sample.txt --tier CoolCompare Hot vs Cool vs Archive per-GB pricing (portal). Note Archive's retrieval is hours — write down when that's acceptable and when it's a trap.
Apply a lifecycle policy (automatic tiering). Attach the lifecycle policy from the command reference to
$SA. This is the scalable version of exercise 4 — you set the aging rules once and stop hand-tiering. Confirm withaz storage account management-policy show --account-name "$SA" -g rg-storage-lab.Right-tier a disk. Note that a dev disk rarely needs Premium. Compare the monthly price of
Premium_LRSvsStandardSSD_LRSvsStandard_LRSfor a given size (portal oraz disk list-skusif available). Write down the dev-disk default you'd standardize on — and how a governance policy (track 17) could deny Premium disks in dev automatically.Tune Log Analytics retention and read ingestion (track 12). For a workspace you have (from track 07 module 06 / track 12), inspect and reduce retention:
az monitor log-analytics workspace show -g <rg> -n <ws> --query "{retentionDays:retentionInDays, cap:workspaceCapping}" -o json az monitor log-analytics workspace update -g <rg> -n <ws> --retention-time 30Note the retention you started at (often a long default) and what dropping it to 30 days saves. Then reason about ingestion: which log source is chattiest, and whether a daily cap (
--query workspaceCapping) is a sensible guardrail. This is track 12's "telemetry isn't free" made into a bill.Estimate the ongoing cost of what you found. Combine exercises 1, 3, and 7: tally the monthly cost of every orphan, un-tiered blob set, and over-retained workspace you found. Write one number — "silent waste found: $X/month." That number is the elevator pitch for why the sweep is a recurring practice.
Diagnose and fix: the disk that's been quietly billing for weeks. The flagship storage incident. Scenario: Cost Analysis (module 01) shows a steady ~$X/month "Storage" line in an RG that has no running VMs. Investigate:
# the RG bills for storage but has no VMs — where's the money going? az vm list -g <rg> -o table # empty or few az disk list -g <rg> --query "[?managedBy==null].{name:name, gb:diskSizeGb, created:timeCreated}" -o tableDiagnose: an unattached disk (or several) left behind when a StatefulSet/VM was deleted with a
Retainreclaim policy (track 14) — it's been billing every day since, invisible to every compute view because it's attached to nothing. Fix: confirm it's truly orphaned (check itstimeCreated/Ownertag, ask the owning team via the track-17Ownertag — don't delete a disk that's a detached data volume someone's about to reattach), snapshot it if unsure, thenaz disk delete --ids <id> --yes. Prevent recurrence: set the reclaim policy toDeletewhere appropriate (track 14) and add the orphan sweep to a schedule. Lesson: storage waste hides because it's decoupled from compute — only a deliberate sweep (or a governance policy) finds it.Clean up.
az group delete -n rg-storage-lab --yes --no-waitKeep the retention change on your real workspace if it's a genuine improvement. Confirm the lab RG is gone with
az group show -n rg-storage-lab -o table.
Independent challenge
No commands given. Drawing on this module, the orphaned-disk lesson from track
14, the Log Analytics/observability cost from track 12, the Resource Graph
query tool and Owner tag from track 17 module 06, and Cost Analysis from
module 01, run a real storage-and-data waste audit on your subscription and
produce a remediation plan. It must: sweep for at least three orphan classes
(disks, public IPs, and one more) and total their monthly cost; identify at least
one mis-tiered blob set or over-provisioned disk and propose the right tier
with justification; inspect your Log Analytics retention and ingestion and
propose a change with an estimated saving; and — the Operate-phase part — describe
how you'd make this audit recurring (a scheduled Resource Graph query, lifecycle
policies, retention-as-policy, and one governance policy from track 17 that would
prevent a class of this waste). Actually clean up at least one real orphan you
find (safely — check the Owner tag first). The deliverable is the audit findings,
the dollar total, the remediation actions taken, and the recurring-prevention design.
Stuck? One hint
The sweep is the backbone — write one Resource Graph query per orphan class, each
with its own "unattached" predicate (isnull(properties.managedBy) for disks,
isnull(properties.ipConfiguration) for public IPs, and find the analog for
snapshots/NICs), and always project the tags so you can read the Owner
before touching anything. That Owner tag (track 17 module 06) is what turns a
scary "is anyone using this?" deletion into a safe "team-payments, are you done
with this detached volume?" — the taxonomy pays off in cleanup, not just
allocation. For prevention, remember the whole point of the Operate phase: a
one-time sweep is a chore, so pair each finding with the automatic control that
stops it recurring — a lifecycle policy for blobs, retention set as a workspace
default, a Delete reclaim policy for PVs (track 14), and a track-17 Azure Policy
that denies (say) Premium disks in dev or requires a lifecycle rule on new storage.
Common mistakes & troubleshooting
- Assuming a compute view will surface storage waste. Orphans are attached to nothing, so utilization tools (module 03) and compute charts never show them. Only a deliberate sweep (Resource Graph) or Advisor finds them.
- Deleting a disk that isn't actually orphaned. A detached disk might be a data
volume someone's about to reattach. Check
timeCreatedand theOwnertag (track 17), ask the owner, and snapshot if unsure — beforeaz disk delete. - Archiving data you actually read. Archive is cheapest to store but slow and expensive to retrieve, with early-deletion penalties. Tier to match the real access pattern; the wrong tier can cost more than Hot.
- Hand-tiering blobs instead of using lifecycle policies. Manual re-tiering doesn't scale and drifts. Encode the aging rules once as a lifecycle management policy and let it run.
- Forgetting disks bill on provisioned size/tier, not usage. A 1 TiB Premium disk storing 40 GiB bills for 1 TiB of Premium. Right-tier and right-size up front (shrinking is hard), and kill orphans.
- Leaving Log Analytics at default retention and full-price ingestion. Long default retention on verbose logs is silent money. Tune retention to what you interactively query, cut noisy ingestion, and set a daily cap (track 12).
- Treating cleanup as a one-time project. Every waste class re-accumulates. Make the sweep recurring and add governance policy (track 17) that prevents the common cases — cleanup encoded, not cleanup performed.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
Show questions
- What is an orphaned resource, what's the canonical example from track 14, and why do compute/utilization views (module 03) never catch it?
- Name the four Blob access tiers from most to least expensive to store, and the key catch about the cheapest one.
- What's the scalable way to tier blobs (vs. doing it by hand), and what's the risk of tiering purely by age?
- For managed disks, what do you actually pay for, and what are the three optimization moves — which is usually the biggest, cheapest win?
- On what two axes does a Log Analytics workspace bill (track 12), which is usually dominant, and name three levers to cut the cost.
- How does the
Ownertag from track 17 module 06 make an orphan sweep safer, and why is that a cleanup concern, not just an allocation one? - What's the Operate-phase reason a one-time cleanup is insufficient, and give two ways to make waste-prevention recurring/automatic.
- Cost Analysis shows a steady storage charge in an RG with no VMs. What's your first hypothesis and the command that confirms it?
Show answers
- A resource that still exists and bills but is attached to nothing / unused.
The canonical example (track 14) is an unattached managed disk left behind by
a
RetainPVC after its pod/VM was deleted. Compute views miss it because it's attached to no VM — it has no utilization to show. - Hot → Cool → Cold → Archive (most to least storage cost). Catch: Archive is offline — retrieval takes hours (rehydration) and costs the most, plus early-deletion penalties, so it's only for data you're keeping but expect never to read.
- A lifecycle management policy that auto-tiers/deletes by age — set once, runs automatically. Risk of age-only tiering: it assumes access follows age; data you do read occasionally can end up in Archive where reading it costs more. Match the tier to the real access pattern.
- You pay for the provisioned size and tier, not stored bytes or IOPS used. Three moves: right-tier (dev doesn't need Premium), right-size up front, and — usually biggest and cheapest — delete orphans (unattached disks/snapshots).
- Ingestion (per GB sent in — usually dominant) and retention (per GB kept beyond the included window). Levers: reduce ingestion (drop/sample noisy logs, Basic/Auxiliary table plans), tune retention (interactive vs. cheap archive), and set a daily cap guardrail.
- The
Ownertag identifies which team an orphan belongs to, turning a risky "is anyone using this?" deletion into a safe "team-X, are you done with this?" confirmation. It's a cleanup concern because deleting the wrong "orphan" (a detached volume about to be reattached) causes data loss — the tag de-risks the sweep. - Every waste class re-accumulates silently and continuously, so a one-time sweep fixes today and the waste returns. Make prevention recurring: scheduled Resource Graph sweeps, lifecycle policies, retention set as a workspace default, and governance policy (track 17) that denies/requires the common cases (e.g. deny Premium disks in dev).
- First hypothesis: an unattached (orphaned) disk (or snapshot) left behind by
a deleted VM/StatefulSet with
Retain(track 14). Confirm withaz disk list -g <rg> --query "[?managedBy==null]"(andaz vm list -g <rg>to show there are no VMs to justify the storage).
Next
Continue to 05-reservations-savings-plans-and-spot — you've cut waste by rightsizing (module 03) and sweeping storage (this module). The last big Optimize lever is pricing the compute you've decided you genuinely need: committing to it for a discount (Reservations, Savings Plans) or running interruptible workloads dirt-cheap (Spot) — and knowing which of the three fits a given workload.