The Ultimate Guide to Finding and Safely Deleting Azure Orphaned Resources
Introduction: The Hidden Cost of Orphaned Resources
Every Azure environment has them—orphaned resources silently draining your budget. Unattached disks from deleted VMs. Public IPs that haven’t been used in months. Network interfaces connected to nothing. Snapshots from projects that ended years ago.
In our investigations, we’ve found that orphaned resources typically account for 15-25% of Azure waste in mature environments. That’s thousands of dollars monthly going to absolutely nothing.
This guide provides a complete, technical approach to finding and safely removing these orphaned resources. Every script has been battle-tested in production environments managing millions in Azure spend.
What Are Orphaned Resources?
Orphaned resources are Azure resources that exist in your subscription but serve no functional purpose. They’re not attached to any active resource, not serving any traffic, and not providing any value—yet they’re still billing you.
Common Types of Orphaned Resources
| Resource Type | Average Monthly Cost | How They Get Orphaned |
|---|---|---|
| Managed Disks | $50-500 per disk | VM deleted without deleting disks |
| Public IP Addresses | $3.65-5.00 each | Load balancer or VM removed |
| Network Interfaces | $0 (but clutter) | VM deleted, NIC remains |
| Snapshots | $5-50 per snapshot | Forgotten after backup restore |
| Network Security Groups | $0 (but risky) | Subnet or NIC deleted |
| Route Tables | $0 (but confusing) | VNet restructuring |
| Load Balancers | $25+ each | Application decommissioned |
| Application Gateways | $200+ each | App migration to different solution |
Real-World Impact
Case Study: A SaaS company discovered:
- 234 unattached Premium SSD disks (P30/P40)
- Total monthly cost: $42,000
- How long orphaned: 8-18 months
- Total waste before discovery: ~$500,000
Part 1: Finding Orphaned Disks
Unattached managed disks are the most expensive orphaned resource. Here’s a comprehensive approach to finding them.
Basic Detection Script
# Connect to Azure
Connect-AzAccount
# Get all subscriptions
$subscriptions = Get-AzSubscription
$allOrphanedDisks = @()
foreach ($subscription in $subscriptions) {
Write-Host "Checking subscription: $($subscription.Name)" -ForegroundColor Cyan
Set-AzContext -SubscriptionId $subscription.Id
# Find all unattached managed disks
$orphanedDisks = Get-AzDisk | Where-Object {$_.ManagedBy -eq $null}
foreach ($disk in $orphanedDisks) {
# Calculate monthly cost based on disk type and size
$monthlyCost = switch($disk.Sku.Name) {
'Premium_LRS' {
# Premium SSD pricing (approximate)
switch([int]$disk.DiskSizeGB) {
{$_ -le 32} { 5.28 } # P4
{$_ -le 64} { 10.21 } # P6
{$_ -le 128} { 19.71 } # P10
{$_ -le 256} { 38.40 } # P15
{$_ -le 512} { 73.22 } # P20
{$_ -le 1024} { 135.17 } # P30
{$_ -le 2048} { 259.05 } # P40
{$_ -le 4096} { 494.02 } # P50
default { 494.02 }
}
}
'StandardSSD_LRS' {
# Standard SSD pricing
$disk.DiskSizeGB * 0.075
}
'Standard_LRS' {
# Standard HDD pricing
$disk.DiskSizeGB * 0.045
}
'UltraSSD_LRS' {
# Ultra disk (complex pricing, using estimate)
$disk.DiskSizeGB * 0.32
}
default { $disk.DiskSizeGB * 0.05 }
}
$diskInfo = [PSCustomObject]@{
Subscription = $subscription.Name
DiskName = $disk.Name
ResourceGroup = $disk.ResourceGroupName
Location = $disk.Location
DiskSizeGB = $disk.DiskSizeGB
DiskType = $disk.Sku.Name
CreationTime = $disk.TimeCreated
DaysSinceCreation = [math]::Round((New-TimeSpan -Start $disk.TimeCreated -End (Get-Date)).TotalDays)
LastModified = $disk.LastModifiedTime
EstimatedMonthlyCost = [math]::Round($monthlyCost, 2)
Tags = $disk.Tags | ConvertTo-Json -Compress
}
$allOrphanedDisks += $diskInfo
}
}
# Generate report
$totalMonthlyCost = ($allOrphanedDisks | Measure-Object -Property EstimatedMonthlyCost -Sum).Sum
$totalAnnualCost = $totalMonthlyCost * 12
Write-Host "`n========== ORPHANED DISK REPORT ==========" -ForegroundColor Yellow
Write-Host "Total Orphaned Disks Found: $($allOrphanedDisks.Count)"
Write-Host "Total Monthly Cost: `$$([math]::Round($totalMonthlyCost, 2))"
Write-Host "Total Annual Cost: `$$([math]::Round($totalAnnualCost, 2))"
Write-Host "==========================================" -ForegroundColor Yellow
# Export to CSV for review
$allOrphanedDisks | Export-Csv -Path "OrphanedDisks_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
# Show top 10 most expensive
Write-Host "`nTop 10 Most Expensive Orphaned Disks:" -ForegroundColor Cyan
$allOrphanedDisks | Sort-Object EstimatedMonthlyCost -Descending | Select-Object -First 10 | Format-Table DiskName, DiskSizeGB, DiskType, EstimatedMonthlyCost
Advanced Safety Validation
Before deleting any disk, validate it’s truly safe:
function Test-DiskSafeToDelete {
param(
[Parameter(Mandatory=$true)]
[string]$DiskName,
[Parameter(Mandatory=$true)]
[string]$ResourceGroupName
)
$disk = Get-AzDisk -ResourceGroupName $ResourceGroupName -DiskName $DiskName
$safetyChecks = @()
# Check 1: Age of disk
$age = (New-TimeSpan -Start $disk.TimeCreated -End (Get-Date)).TotalDays
$safetyChecks += [PSCustomObject]@{
Check = "Age Check"
Result = if($age -gt 30) {"PASS"} else {"FAIL"}
Details = "Disk age: $([math]::Round($age)) days (Safe if > 30 days)"
}
# Check 2: Recent modifications
$lastModified = (New-TimeSpan -Start $disk.LastModifiedTime -End (Get-Date)).TotalDays
$safetyChecks += [PSCustomObject]@{
Check = "Last Modified"
Result = if($lastModified -gt 7) {"PASS"} else {"FAIL"}
Details = "Last modified: $([math]::Round($lastModified)) days ago (Safe if > 7 days)"
}
# Check 3: Check for "Do Not Delete" tags
$protectedTags = @("DoNotDelete", "Protected", "Keep", "Production", "Critical")
$hasProtectedTag = $false
foreach ($tag in $protectedTags) {
if ($disk.Tags.Keys -contains $tag -or $disk.Tags.Values -contains $tag) {
$hasProtectedTag = $true
break
}
}
$safetyChecks += [PSCustomObject]@{
Check = "Protected Tags"
Result = if(!$hasProtectedTag) {"PASS"} else {"FAIL"}
Details = "Protected tags found: $hasProtectedTag"
}
# Check 4: Snapshot existence
$snapshots = Get-AzSnapshot -ResourceGroupName $ResourceGroupName | Where-Object {$_.CreationData.SourceResourceId -eq $disk.Id}
$safetyChecks += [PSCustomObject]@{
Check = "Recent Snapshots"
Result = if($snapshots.Count -eq 0) {"PASS"} else {"WARNING"}
Details = "Snapshots found: $($snapshots.Count)"
}
# Check 5: Disk encryption
$isEncrypted = $disk.Encryption.Type -ne "EncryptionAtRestWithPlatformKey"
$safetyChecks += [PSCustomObject]@{
Check = "Encryption"
Result = if(!$isEncrypted) {"PASS"} else {"WARNING"}
Details = "Custom encryption: $isEncrypted (May contain sensitive data)"
}
# Display results
Write-Host "`nSafety Validation for Disk: $DiskName" -ForegroundColor Yellow
Write-Host "Resource Group: $ResourceGroupName" -ForegroundColor Yellow
$safetyChecks | Format-Table Check, Result, Details -AutoSize
# Overall recommendation
$failures = ($safetyChecks | Where-Object {$_.Result -eq "FAIL"}).Count
$warnings = ($safetyChecks | Where-Object {$_.Result -eq "WARNING"}).Count
if ($failures -eq 0 -and $warnings -eq 0) {
Write-Host "✓ SAFE TO DELETE" -ForegroundColor Green
return $true
} elseif ($failures -eq 0) {
Write-Host "⚠ PROBABLY SAFE (Review warnings)" -ForegroundColor Yellow
return $true
} else {
Write-Host "✗ NOT SAFE TO DELETE" -ForegroundColor Red
return $false
}
}
# Example usage
Test-DiskSafeToDelete -DiskName "VM-PROD-DISK-01" -ResourceGroupName "RG-PRODUCTION"
Bulk Deletion Script (With Safety)
function Remove-OrphanedDisksWithValidation {
param(
[Parameter(Mandatory=$true)]
[string]$CsvPath,
[switch]$WhatIf = $true # Default to WhatIf mode for safety
)
$disks = Import-Csv -Path $CsvPath
$deletionLog = @()
$totalSavings = 0
foreach ($disk in $disks) {
Write-Host "`nProcessing: $($disk.DiskName)" -ForegroundColor Cyan
# Run safety validation
$isSafe = Test-DiskSafeToDelete -DiskName $disk.DiskName -ResourceGroupName $disk.ResourceGroup
if ($isSafe) {
if ($WhatIf) {
Write-Host "WHATIF: Would delete disk $($disk.DiskName) - Saving `$$($disk.EstimatedMonthlyCost)/month" -ForegroundColor Yellow
} else {
try {
Remove-AzDisk -ResourceGroupName $disk.ResourceGroup -DiskName $disk.DiskName -Force
Write-Host "✓ Deleted disk $($disk.DiskName) - Saved `$$($disk.EstimatedMonthlyCost)/month" -ForegroundColor Green
$deletionLog += [PSCustomObject]@{
Timestamp = Get-Date
DiskName = $disk.DiskName
ResourceGroup = $disk.ResourceGroup
Status = "Deleted"
MonthlySavings = $disk.EstimatedMonthlyCost
}
$totalSavings += $disk.EstimatedMonthlyCost
} catch {
Write-Host "✗ Failed to delete $($disk.DiskName): $_" -ForegroundColor Red
$deletionLog += [PSCustomObject]@{
Timestamp = Get-Date
DiskName = $disk.DiskName
ResourceGroup = $disk.ResourceGroup
Status = "Failed"
Error = $_.Exception.Message
}
}
}
} else {
Write-Host "⊘ Skipping $($disk.DiskName) - Failed safety validation" -ForegroundColor Magenta
}
}
# Save deletion log
$logPath = "DiskDeletionLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$deletionLog | Export-Csv -Path $logPath -NoTypeInformation
Write-Host "`n========== DELETION SUMMARY ==========" -ForegroundColor Green
Write-Host "Total Monthly Savings: `$$totalSavings"
Write-Host "Annual Savings: `$$($totalSavings * 12)"
Write-Host "Deletion log saved to: $logPath"
Write-Host "======================================" -ForegroundColor Green
}
# Run in WhatIf mode first
Remove-OrphanedDisksWithValidation -CsvPath "OrphanedDisks_20240122.csv" -WhatIf
# After review, run actual deletion
# Remove-OrphanedDisksWithValidation -CsvPath "OrphanedDisks_20240122.csv" -WhatIf:$false
Part 2: Finding Other Orphaned Resources
Orphaned Public IPs
function Find-OrphanedPublicIPs {
$orphanedIPs = @()
$subscriptions = Get-AzSubscription
foreach ($subscription in $subscriptions) {
Set-AzContext -SubscriptionId $subscription.Id
$publicIPs = Get-AzPublicIpAddress | Where-Object {
$_.IpConfiguration -eq $null -and
$_.NatGateway -eq $null -and
$_.PublicIpPrefix -eq $null
}
foreach ($ip in $publicIPs) {
# Check if it's associated with any resource
$isOrphaned = $true
# Check for Load Balancer association
if ($ip.IpConfiguration) { $isOrphaned = $false }
# Check for VPN Gateway association
if ($ip.Id -match "vpn" -or $ip.Id -match "gateway") {
# Extra caution for VPN resources
$isOrphaned = $false
}
if ($isOrphaned) {
$orphanedIPs += [PSCustomObject]@{
Subscription = $subscription.Name
Name = $ip.Name
ResourceGroup = $ip.ResourceGroupName
Location = $ip.Location
IPAddress = $ip.IpAddress
SKU = $ip.Sku.Name
AllocationMethod = $ip.PublicIpAllocationMethod
CreatedDate = $ip.Tag['CreatedDate'] # If tagged
MonthlyCost = if($ip.Sku.Name -eq 'Standard') { 5.00 } else { 3.65 }
}
}
}
}
# Generate report
$totalMonthlyCost = ($orphanedIPs | Measure-Object -Property MonthlyCost -Sum).Sum
Write-Host "`nOrphaned Public IPs Found: $($orphanedIPs.Count)" -ForegroundColor Yellow
Write-Host "Total Monthly Cost: `$$totalMonthlyCost" -ForegroundColor Yellow
$orphanedIPs | Export-Csv -Path "OrphanedPublicIPs_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
return $orphanedIPs
}
# Find and display orphaned IPs
$orphanedIPs = Find-OrphanedPublicIPs
$orphanedIPs | Format-Table Name, ResourceGroup, IPAddress, MonthlyCost
Orphaned Network Interfaces
function Find-OrphanedNetworkInterfaces {
$orphanedNICs = @()
$nics = Get-AzNetworkInterface | Where-Object {
$_.VirtualMachine -eq $null -and
$_.PrivateEndpoint -eq $null -and
$_.NetworkSecurityGroup -eq $null
}
foreach ($nic in $nics) {
# Additional validation
$lastActivity = Get-AzLog -ResourceId $nic.Id -StartTime (Get-Date).AddDays(-30) -ErrorAction SilentlyContinue
$orphanedNICs += [PSCustomObject]@{
Name = $nic.Name
ResourceGroup = $nic.ResourceGroupName
Location = $nic.Location
PrivateIP = $nic.IpConfigurations[0].PrivateIpAddress
Subnet = $nic.IpConfigurations[0].Subnet.Id.Split('/')[-1]
VNet = $nic.IpConfigurations[0].Subnet.Id.Split('/')[8]
LastActivity = if($lastActivity) { $lastActivity[0].EventTimestamp } else { "Unknown" }
DaysSinceActivity = if($lastActivity) {
[math]::Round((New-TimeSpan -Start $lastActivity[0].EventTimestamp -End (Get-Date)).TotalDays)
} else { 999 }
}
}
Write-Host "`nOrphaned Network Interfaces Found: $($orphanedNICs.Count)" -ForegroundColor Yellow
# NICs don't have direct cost but create clutter and confusion
$orphanedNICs | Where-Object {$_.DaysSinceActivity -gt 30} |
Export-Csv -Path "OrphanedNICs_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
return $orphanedNICs
}
$orphanedNICs = Find-OrphanedNetworkInterfaces
$orphanedNICs | Where-Object {$_.DaysSinceActivity -gt 30} | Format-Table Name, ResourceGroup, PrivateIP, DaysSinceActivity
Orphaned Snapshots
function Find-OrphanedSnapshots {
$orphanedSnapshots = @()
$allSnapshots = Get-AzSnapshot
foreach ($snapshot in $allSnapshots) {
$isOrphaned = $false
# Check if source disk still exists
if ($snapshot.CreationData.SourceResourceId) {
try {
$sourceDisk = Get-AzResource -ResourceId $snapshot.CreationData.SourceResourceId -ErrorAction Stop
} catch {
# Source disk no longer exists
$isOrphaned = $true
}
}
# Calculate age
$ageInDays = (New-TimeSpan -Start $snapshot.TimeCreated -End (Get-Date)).TotalDays
# Consider snapshots older than 90 days as candidates
if ($ageInDays -gt 90 -or $isOrphaned) {
# Calculate approximate monthly cost (snapshot pricing is complex)
$sizeGB = if($snapshot.DiskSizeGB) { $snapshot.DiskSizeGB } else { 100 } # Default estimate
$monthlyCost = $sizeGB * 0.05 # Approximate snapshot storage cost
$orphanedSnapshots += [PSCustomObject]@{
Name = $snapshot.Name
ResourceGroup = $snapshot.ResourceGroupName
Location = $snapshot.Location
SourceDisk = $snapshot.CreationData.SourceResourceId.Split('/')[-1]
CreationTime = $snapshot.TimeCreated
AgeInDays = [math]::Round($ageInDays)
SizeGB = $sizeGB
SourceExists = !$isOrphaned
EstimatedMonthlyCost = [math]::Round($monthlyCost, 2)
}
}
}
$totalMonthlyCost = ($orphanedSnapshots | Measure-Object -Property EstimatedMonthlyCost -Sum).Sum
Write-Host "`nOrphaned/Old Snapshots Found: $($orphanedSnapshots.Count)" -ForegroundColor Yellow
Write-Host "Total Monthly Cost: `$$totalMonthlyCost" -ForegroundColor Yellow
$orphanedSnapshots | Export-Csv -Path "OrphanedSnapshots_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
return $orphanedSnapshots
}
$snapshots = Find-OrphanedSnapshots
$snapshots | Sort-Object AgeInDays -Descending | Select-Object -First 20 | Format-Table Name, AgeInDays, SizeGB, EstimatedMonthlyCost
Part 3: Automation Strategy
Azure Automation Runbook for Continuous Cleanup
Create an Azure Automation runbook that runs weekly:
# Azure Automation Runbook: Clean-OrphanedResources.ps1
param(
[Parameter(Mandatory=$false)]
[bool]$AutoDelete = $false, # Set to true only after testing
[Parameter(Mandatory=$false)]
[string]$EmailRecipient = "sre-team@company.com"
)
# Authenticate using Managed Identity
Connect-AzAccount -Identity
$report = @{
RunDate = Get-Date
DisksFound = 0
DisksSavings = 0
IPsFound = 0
IPsSavings = 0
NICsFound = 0
SnapshotsFound = 0
SnapshotsSavings = 0
TotalSavings = 0
ResourcesDeleted = @()
}
# Find orphaned disks
Write-Output "Scanning for orphaned disks..."
$orphanedDisks = Get-AzDisk | Where-Object {$_.ManagedBy -eq $null}
$report.DisksFound = $orphanedDisks.Count
foreach ($disk in $orphanedDisks) {
$age = (New-TimeSpan -Start $disk.TimeCreated -End (Get-Date)).TotalDays
if ($age -gt 30) { # Only consider disks older than 30 days
# Calculate cost
$monthlyCost = switch($disk.Sku.Name) {
'Premium_LRS' { $disk.DiskSizeGB * 0.135 }
'StandardSSD_LRS' { $disk.DiskSizeGB * 0.075 }
'Standard_LRS' { $disk.DiskSizeGB * 0.045 }
default { $disk.DiskSizeGB * 0.05 }
}
$report.DisksSavings += $monthlyCost
if ($AutoDelete) {
try {
Remove-AzDisk -ResourceGroupName $disk.ResourceGroupName -DiskName $disk.Name -Force
$report.ResourcesDeleted += "Disk: $($disk.Name)"
Write-Output "Deleted disk: $($disk.Name)"
} catch {
Write-Error "Failed to delete disk $($disk.Name): $_"
}
}
}
}
# Find orphaned Public IPs
Write-Output "Scanning for orphaned public IPs..."
$orphanedIPs = Get-AzPublicIpAddress | Where-Object {$_.IpConfiguration -eq $null}
$report.IPsFound = $orphanedIPs.Count
$report.IPsSavings = $orphanedIPs.Count * 3.65 # Basic SKU cost
if ($AutoDelete) {
foreach ($ip in $orphanedIPs) {
try {
Remove-AzPublicIpAddress -ResourceGroupName $ip.ResourceGroupName -Name $ip.Name -Force
$report.ResourcesDeleted += "PublicIP: $($ip.Name)"
Write-Output "Deleted public IP: $($ip.Name)"
} catch {
Write-Error "Failed to delete IP $($ip.Name): $_"
}
}
}
# Find orphaned NICs
Write-Output "Scanning for orphaned network interfaces..."
$orphanedNICs = Get-AzNetworkInterface | Where-Object {$_.VirtualMachine -eq $null}
$report.NICsFound = $orphanedNICs.Count
if ($AutoDelete) {
foreach ($nic in $orphanedNICs) {
# Extra safety check - ensure NIC has been orphaned for at least 7 days
$logs = Get-AzLog -ResourceId $nic.Id -StartTime (Get-Date).AddDays(-7) -ErrorAction SilentlyContinue
if (-not $logs -or $logs.Count -eq 0) {
try {
Remove-AzNetworkInterface -ResourceGroupName $nic.ResourceGroupName -Name $nic.Name -Force
$report.ResourcesDeleted += "NIC: $($nic.Name)"
Write-Output "Deleted NIC: $($nic.Name)"
} catch {
Write-Error "Failed to delete NIC $($nic.Name): $_"
}
}
}
}
# Calculate total savings
$report.TotalSavings = $report.DisksSavings + $report.IPsSavings + $report.SnapshotsSavings
# Generate email report
$emailBody = @"
<html>
<head>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
.savings { color: green; font-weight: bold; }
</style>
</head>
<body>
<h2>Azure Orphaned Resources Report</h2>
<p>Run Date: $($report.RunDate)</p>
<table>
<tr>
<th>Resource Type</th>
<th>Count Found</th>
<th>Monthly Savings</th>
<th>Action Taken</th>
</tr>
<tr>
<td>Managed Disks</td>
<td>$($report.DisksFound)</td>
<td class="savings">`$$([math]::Round($report.DisksSavings, 2))</td>
<td>$(if($AutoDelete) {"Deleted"} else {"Report Only"})</td>
</tr>
<tr>
<td>Public IPs</td>
<td>$($report.IPsFound)</td>
<td class="savings">`$$([math]::Round($report.IPsSavings, 2))</td>
<td>$(if($AutoDelete) {"Deleted"} else {"Report Only"})</td>
</tr>
<tr>
<td>Network Interfaces</td>
<td>$($report.NICsFound)</td>
<td>N/A</td>
<td>$(if($AutoDelete) {"Deleted"} else {"Report Only"})</td>
</tr>
</table>
<h3>Total Monthly Savings: <span class="savings">`$$([math]::Round($report.TotalSavings, 2))</span></h3>
<h3>Annual Savings: <span class="savings">`$$([math]::Round($report.TotalSavings * 12, 2))</span></h3>
$(if($report.ResourcesDeleted.Count -gt 0) {
"<h3>Resources Deleted:</h3><ul>"
$report.ResourcesDeleted | ForEach-Object { "<li>$_</li>" }
"</ul>"
})
</body>
</html>
"@
# Send email report
Send-MailMessage `
-To $EmailRecipient `
-From "azure-automation@company.com" `
-Subject "Azure Orphaned Resources Report - $($report.RunDate.ToString('yyyy-MM-dd'))" `
-Body $emailBody `
-BodyAsHtml `
-SmtpServer "smtp.company.com"
Write-Output "Report sent to $EmailRecipient"
Write-Output "Total potential monthly savings: `$$($report.TotalSavings)"
Setting Up the Automation
- Create Automation Account:
# Create automation account
New-AzAutomationAccount `
-ResourceGroupName "RG-Automation" `
-Name "AA-CostOptimization" `
-Location "East US" `
-Plan "Basic"
# Enable Managed Identity
Set-AzAutomationAccount `
-ResourceGroupName "RG-Automation" `
-Name "AA-CostOptimization" `
-AssignSystemIdentity
- Grant Permissions:
# Get the Managed Identity principal ID
$automation = Get-AzAutomationAccount -ResourceGroupName "RG-Automation" -Name "AA-CostOptimization"
$principalId = $automation.Identity.PrincipalId
# Assign Reader role (for reporting only)
New-AzRoleAssignment `
-ObjectId $principalId `
-RoleDefinitionName "Reader" `
-Scope "/subscriptions/$subscriptionId"
# For auto-deletion, assign Contributor on specific resource groups only
New-AzRoleAssignment `
-ObjectId $principalId `
-RoleDefinitionName "Contributor" `
-Scope "/subscriptions/$subscriptionId/resourceGroups/RG-Dev"
- Schedule the Runbook:
# Create schedule
New-AzAutomationSchedule `
-ResourceGroupName "RG-Automation" `
-AutomationAccountName "AA-CostOptimization" `
-Name "WeeklyOrphanedResourceCleanup" `
-StartTime (Get-Date).AddDays(1) ` Get Your Free Cloud Cost-Cutting Checklist
Join the weekly 'Cloud Sleuth' briefing and get my 10-point checklist to find hidden cloud waste, delivered instantly to your inbox.
Related Articles
The SRE's Guide to Azure Cost Optimization: A Taming Strategy for 'Dirty' Environments
A practical, step-by-step strategy for finding and safely removing waste in real-world, complex Azure environments - even with no documentation or tagging.
How to Manage Azure Costs When Your Tagging is a Complete Mess
Practical strategies for cost management and resource attribution in Azure environments with poor or nonexistent tagging. Learn detective techniques that actually work.