Back to Playbooks

Azure Unused Resources Cleanup Script

Automated Python script to identify and clean up unused Azure resources including unattached disks, idle VMs, and orphaned NICs to reduce cloud waste.

8 min read
AzureFinOpsAutomationPythonCost Optimization
Share on X

๐Ÿ’ธ Stop Paying for Unused Resources

Cloud waste is one of the biggest challenges in FinOps. This Python automation script scans your Azure subscriptions to find resources that are costing you money but providing no value.

Identify and clean up unused Azure resources to reduce your monthly cloud bill. This playbook helps you find unattached disks, stopped VMs, orphaned network interfaces, and more.

๐Ÿ” What We Find

Unattached Managed Disks

Disks not attached to any VM, often left behind after VM deletion

Stopped VMs

VMs deallocated for more than 30 days but still incurring storage costs

Orphaned NICs

Network interfaces not attached to any VMs

Old Snapshots

Snapshots older than 90 days that may no longer be needed

๐Ÿ’ฐ Potential Savings

Organizations typically find $5,000-$50,000 in annual savings from unused resources on their first cleanup sweep. Regular automation keeps waste under control.

๐Ÿ“‹ Prerequisites

  • โ€ขPython 3.8 or higher
  • โ€ขAzure CLI (az) installed and configured
  • โ€ขAzure SDK for Python: pip install azure-identity azure-mgmt-compute azure-mgmt-network
  • โ€ขContributor or Reader access to target subscriptions

๐Ÿš€ Quick Start

# Install required Python packages
pip install azure-identity azure-mgmt-compute azure-mgmt-network azure-mgmt-resource

# Login to Azure
az login

# Set your subscription (optional)
az account set --subscription "your-subscription-id"

โš™๏ธ The Complete Script

This script uses the Azure Python SDK to scan your subscriptions and identify unused resources. It generates a detailed CSV report with cost estimates for each unused resource.

#!/usr/bin/env python3
"""
Azure Unused Resources Finder
Identifies unused Azure resources across subscriptions
"""

from azure.identity import DefaultAzureCredential
from azure.mgmt.compute import ComputeManagementClient
from azure.mgmt.network import NetworkManagementClient
from azure.mgmt.resource import SubscriptionClient
from datetime import datetime, timedelta
import csv

def get_subscriptions(credential):
    """Get all Azure subscriptions"""
    subscription_client = SubscriptionClient(credential)
    return [sub.subscription_id for sub in subscription_client.subscriptions.list()]

def find_unattached_disks(compute_client):
    """Find all unattached managed disks"""
    unattached = []
    for disk in compute_client.disks.list():
        if disk.disk_state == 'Unattached':
            cost_estimate = disk.disk_size_gb * 0.05  # Rough estimate: $0.05/GB/month
            unattached.append({
                'type': 'Disk',
                'name': disk.name,
                'resource_group': disk.id.split('/')[4],
                'size_gb': disk.disk_size_gb,
                'estimated_monthly_cost': f'${cost_estimate:.2f}',
                'location': disk.location
            })
    return unattached

def find_orphaned_nics(network_client):
    """Find network interfaces not attached to VMs"""
    orphaned = []
    for nic in network_client.network_interfaces.list_all():
        if not nic.virtual_machine:
            orphaned.append({
                'type': 'NIC',
                'name': nic.name,
                'resource_group': nic.id.split('/')[4],
                'location': nic.location,
                'estimated_monthly_cost': '$5.00'
            })
    return orphaned

def find_unused_public_ips(network_client):
    """Find public IPs not associated with any resource"""
    unused = []
    for ip in network_client.public_ip_addresses.list_all():
        if not ip.ip_configuration:
            unused.append({
                'type': 'PublicIP',
                'name': ip.name,
                'resource_group': ip.id.split('/')[4],
                'ip_address': ip.ip_address or 'Not assigned',
                'estimated_monthly_cost': '$3.50',
                'location': ip.location
            })
    return unused

def main():
    print("๐Ÿ” Azure Unused Resources Finder")
    print("=" * 50)

    credential = DefaultAzureCredential()
    subscriptions = get_subscriptions(credential)

    all_unused = []
    total_estimated_cost = 0

    for sub_id in subscriptions:
        print(f"\n๐Ÿ“‹ Scanning subscription: {sub_id}")

        compute_client = ComputeManagementClient(credential, sub_id)
        network_client = NetworkManagementClient(credential, sub_id)

        # Find unused resources
        disks = find_unattached_disks(compute_client)
        nics = find_orphaned_nics(network_client)
        ips = find_unused_public_ips(network_client)

        all_unused.extend(disks + nics + ips)

        print(f"  Found: {len(disks)} disks, {len(nics)} NICs, {len(ips)} IPs")

    # Write to CSV
    if all_unused:
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f'azure_unused_resources_{timestamp}.csv'

        with open(filename, 'w', newline='') as f:
            writer = csv.DictWriter(f, fieldnames=all_unused[0].keys())
            writer.writeheader()
            writer.writerows(all_unused)

        print(f"\nโœ… Report saved: {filename}")
        print(f"๐Ÿ“Š Total unused resources: {len(all_unused)}")
    else:
        print("\nโœจ No unused resources found!")

if __name__ == "__main__":
    main()

๐Ÿ“– How to Use

  1. 1.
    Run the script: python azure_cleanup_finder.py
  2. 2.
    Review the CSV report that gets generated with all findings
  3. 3.
    Verify resources before deletion - some may be intentionally unattached
  4. 4.
    Clean up confirmed waste using the Azure Portal or CLI commands below

๐Ÿงน Cleanup Commands

Once you've identified resources to delete, use these Azure CLI commands:

# Delete an unattached disk
az disk delete --name <disk-name> --resource-group <rg-name> --yes

# Delete an orphaned NIC
az network nic delete --name <nic-name> --resource-group <rg-name>

# Delete an unused public IP
az network public-ip delete --name <ip-name> --resource-group <rg-name>

# Delete old snapshots
az snapshot delete --name <snapshot-name> --resource-group <rg-name>

โš ๏ธ Safety First

Always review resources in the CSV report before deletion. Verify with your team that resources are truly unused. Some unattached disks may be intentionally kept for backup or restore purposes.

๐Ÿ’ธ Real-World Savings Example

143
Unattached Disks
$3,215/mo
67
Orphaned NICs
$335/mo
31
Unused Public IPs
$108/mo
Total Annual Savings
$43,896

โœ… FinOps Best Practice

Run this script monthly as part of your FinOps review process. Track savings over time and celebrate wins with your engineering teams to build a cost-conscious culture.