FinOps / Guide 6
Stop the bleeding: storage and orphaned resources
Orphaned disks, snapshots and IPs, wrong storage tiers and redundancy, missing lifecycle rules, and logs kept forever.
Some cloud costs don't come from anything running. They come from things left behind or set up with the wrong defaults: a disk nobody attached again, a storage account replicating test data to another region, backups sitting in the most expensive tier, logs kept forever. Each one is small, but together they leak money every month until someone looks. This guide is the checklist.
Orphaned resources
| Leak | Azure | AWS | Google Cloud |
|---|---|---|---|
| Disks not attached to anything | Managed disks with state Unattached | EBS volumes with state available | Persistent disks with no users |
| Old snapshots | Snapshots with no retention rule | EBS snapshots; move long-term ones to the EBS Snapshots Archive tier (up to 75% cheaper to store) | Snapshots outside a snapshot schedule |
| Unused public IP addresses | Standard public IPs bill whether attached or not (Basic SKU IPs were retired on 30 September 2025) | Every public IPv4 address bills US$0.005 an hour, attached or not | Static external IPs that are reserved but not used |
| Other leftovers | Detached NICs, empty resource groups, unused App Service plans | Idle load balancers, unused NAT gateways | Idle load balancers, unused Cloud NAT gateways |
| Script on this site | Find-AzOrphanedResources.ps1 | Find-AwsOrphanedResources.ps1 | Find-GcpOrphanedResources.ps1 |
# Quick checks from PowerShell
Get-AzDisk | Where-Object DiskState -eq 'Unattached' | Select-Object Name, ResourceGroupName, DiskSizeGB, @{n='Sku';e={$_.Sku.Name}}
Get-EC2Volume -Filter @{ Name = 'status'; Values = 'available' } | Select-Object VolumeId, Size, VolumeType, CreateTime
Get-EC2Address | Where-Object { -not $_.AssociationId } | Select-Object PublicIp, AllocationId
gcloud compute disks list --filter='-users:*' --format='table(name,zone.basename(),sizeGb,type.basename())'
gcloud compute addresses list --filter='status=RESERVED' --format='table(name,region.basename(),address)'
Before deleting a disk, check with the owner and take a final snapshot if there's any doubt. A snapshot costs a fraction of the disk, and it can be deleted later under a retention rule.
Wrong disk type
| Check | Azure | AWS | Google Cloud |
|---|---|---|---|
| Premium disks on non-production | Premium SSD on dev and test; use Standard SSD | io1/io2 where gp3 is enough | pd-ssd or extreme where pd-balanced is enough |
| Old disk type with a cheaper successor | Consider Premium SSD v2 for production disks that need performance | gp2 to gp3: about 20% cheaper per GB, changed online with no downtime | pd-standard for boot disks is slow; pd-balanced is the usual default |
# AWS: change a gp2 volume to gp3 in place (no downtime)
Edit-EC2Volume -VolumeId 'vol-0123456789abcdef0' -VolumeType gp3
Storage redundancy and tier
Object storage has two settings that quietly multiply the bill: how many copies are kept, and which tier the data sits in.
| Setting | Azure | AWS | Google Cloud |
|---|---|---|---|
| Redundancy | LRS, ZRS, GRS, RA-GRS, GZRS. Geo-redundant options roughly double storage cost and add replication charges. Use LRS or ZRS for data you can re-create (test data, caches, build output). | S3 keeps data across availability zones by default; Cross-Region Replication adds a second full copy. One Zone-IA is cheaper for re-creatable data. | Region, dual-region or multi-region buckets. Use a single region for data that doesn't need geo-redundancy. |
| Tiers and minimum storage time | Hot (none), Cool (30 days), Cold (90 days), Archive (180 days, offline, rehydration up to 15 hours) | Standard (none), Standard-IA and One Zone-IA (30 days), Glacier Instant Retrieval and Flexible Retrieval (90 days), Glacier Deep Archive (180 days) | Standard (none), Nearline (30 days), Coldline (90 days), Archive (365 days) |
| Automatic tiering | Smart tier (moves blobs between hot, cool and cold by usage) | S3 Intelligent-Tiering (small monthly monitoring fee; objects under 128 KB aren't tiered) | Autoclass |
| Account or bucket default | General-purpose v2 accounts default to Hot | Objects are written to Standard unless told otherwise | Bucket default class is Standard unless set |
Two traps: deleting or moving data before its minimum storage time is charged as if it had stayed, and the infrequent-access tiers charge for each read. Cooler tiers suit data that is written once and rarely read, such as backups, archives and old logs, not data that's read every day.
# Azure: redundancy and default tier for every storage account
Get-AzStorageAccount | Select-Object StorageAccountName, ResourceGroupName, @{n='Redundancy';e={$_.Sku.Name}}, AccessTier
Lifecycle management
Lifecycle rules move data to cheaper tiers as it ages and delete it when it's no longer needed. Without them, everything stays in the tier it was written to, forever. Check every bucket or storage account holding backups, logs, exports or uploads.
| Azure | AWS | Google Cloud | |
|---|---|---|---|
| Feature | Blob lifecycle management policy (per storage account) | S3 Lifecycle configuration (per bucket) | Object Lifecycle Management (per bucket) |
| Typical rule | Cool after 30 days, Cold after 90, Archive after 180, delete after the retention period | Standard-IA after 30 days, Glacier after 90, Deep Archive after 180, expire after the retention period | Nearline after 30 days, Coldline after 90, Archive after 365, delete after the retention period |
| Also clean up | Old blob versions and snapshots | Incomplete multipart uploads and noncurrent versions on versioned buckets | Noncurrent versions on versioned buckets; review soft delete retention |
# Find storage without lifecycle rules
Get-AzStorageAccount | ForEach-Object {
$p = Get-AzStorageAccountManagementPolicy -ResourceGroupName $_.ResourceGroupName -StorageAccountName $_.StorageAccountName -ErrorAction SilentlyContinue
[pscustomobject]@{ Account = $_.StorageAccountName; HasLifecyclePolicy = [bool]$p }
}
Get-S3Bucket | ForEach-Object {
$rules = try { Get-S3LifecycleConfiguration -BucketName $_.BucketName -ErrorAction Stop } catch { $null }
[pscustomobject]@{ Bucket = $_.BucketName; LifecycleRules = @($rules.Rules).Count }
}
gcloud storage buckets list --format='value(name)' | ForEach-Object {
$lc = gcloud storage buckets describe "gs://$_" --format='value(lifecycle_config)'
[pscustomobject]@{ Bucket = $_; HasLifecycleRules = [bool]$lc }
}
Versioning and backups that never expire
- Versioned buckets keep every overwritten and deleted object. Without a rule for noncurrent versions, a bucket that is overwritten nightly grows every night.
- Backup retention: check backup policies keep what the business needs, not "forever by default". Daily backups kept for years add up fast. The backup coverage reports help here:
Get-AzBackupCoverage.ps1,Get-AwsBackupCoverage.ps1andGet-GcpBackupCoverage.ps1. - Snapshots made by hand before a change and never removed. Name them with a date and owner, and delete them after the change is signed off.
Logs kept forever
| Azure | AWS | Google Cloud | |
|---|---|---|---|
| Where logs pile up | Log Analytics workspaces; diagnostic settings sending everything | CloudWatch Logs log groups (default retention: never expire) | Cloud Logging buckets; logs routed to storage or BigQuery |
| Fix | Set table retention; send verbose tables to the Basic or Auxiliary plan; trim diagnostic settings | Set a retention period on every log group | Set bucket retention; exclude noisy logs with exclusion filters |
# AWS: log groups with no retention, then set 90 days on one
Get-CWLLogGroup | Where-Object { -not $_.RetentionInDays } | Select-Object LogGroupName, StoredBytes
Write-CWLRetentionPolicy -LogGroupName '/aws/lambda/notes-test' -RetentionInDays 90
Data transfer
Traffic leaving the cloud to the internet, and traffic between regions, is charged per GB. Traffic between availability zones is charged on AWS and Google Cloud too. Watch for chatty services split across regions, backups copied to another region every night, and NAT gateways processing traffic that could use private endpoints or gateway endpoints instead. The networking comparison covers private access to cloud services in each provider.