All Posts Infrastructure

How to Audit Your Cloud Costs and Stop Overpaying (Azure/AWS Script)

You can audit your cloud costs in under 30 minutes using free Python scripts that scan your Azure or AWS account for unused resources, orphaned disks, idle VMs, and wasted spending.

Most small businesses overpay for cloud services by 20 to 35% due to zombie resources — orphaned disks, idle VMs, and unused public IPs that accumulate charges silently. Two free, read-only Python scripts (one for Azure, one for AWS) scan your account in under 30 minutes and produce a report identifying every dollar of waste. Businesses across Daytona Beach and Volusia County typically find hundreds to thousands of dollars in annual savings after a single audit.

You can audit your cloud costs in under 30 minutes using free Python scripts that scan your Azure or AWS account for unused resources, orphaned disks, idle VMs, and wasted spending. Most small businesses in Deltona, Daytona Beach, and across Volusia County are overpaying for cloud services by 20-35%, and the fix is not switching providers or downgrading plans. The fix is finding the waste you do not know exists and eliminating it.

Here is a pattern I see constantly with businesses across Central Florida: they migrate to the cloud, set up their infrastructure, and then never look at the bill again. They pay the invoice every month because the total seems “about right” compared to what they were spending on-premise. But “about right” is hiding real money. An unattached disk here, a stopped VM there, a public IP address nobody is using. Individually, these are small charges. Together, they add up to hundreds or even thousands of dollars per year in pure waste.

The cloud providers are not going to tell you about this. Azure and AWS both offer cost management tools, but they are buried in dashboards that most small business owners never visit. And the default posture of both platforms is to keep charging you for resources that exist, whether you are using them or not. A virtual machine that you stopped but did not deallocate? Azure keeps charging you. An EBS volume that you detached from an EC2 instance? AWS keeps charging you. A static public IP that is not associated with anything? Both platforms charge you for that too.

Today we are going to fix that with two scripts: one for Azure and one for AWS. Both are free, both are read-only (they will not touch your resources), and both will produce a report that tells you exactly where your money is going and where you can save.

The Most Common Cloud Waste for Small Businesses

Before we get into the scripts, let me walk through the five most common ways small businesses in Deltona, Port Orange, and the greater Daytona Beach area waste money on cloud services. Understanding what to look for makes the audit results much more actionable.

1. Zombie Resources

Zombie resources are cloud assets that were created for a purpose, served that purpose, and then were abandoned but never deleted. The classic example is a virtual machine that was spun up for a one-time project, used for two weeks, stopped when the project ended, and then forgotten. The VM is no longer doing anything, but its disk, network interface, and public IP address are still racking up charges.

I worked with a business in Deltona that had six zombie VMs consuming $180 per month. Nobody on the team even remembered what they were for. When we traced them back, they were test servers created by a contractor who had left the company eight months earlier.

2. Oversized VMs

Cloud VMs come in dozens of sizes, from tiny instances with 1 CPU and 0.5GB of RAM to massive machines with 96 CPUs and 384GB of RAM. Small businesses almost always start with a VM that is too large because they are nervous about performance. A B4ms ($60/month) running a website that could easily run on a B2s ($30/month) is one of the most common forms of cloud overspending.

The fix is right-sizing: checking CPU and memory utilization over the past 30 days and downgrading to a smaller VM if utilization is consistently below 30%. Both Azure and AWS provide utilization metrics in their portals.

3. Unattached Storage

When you delete a VM in Azure, the managed disk it was using is not automatically deleted. The same is true in AWS with EBS volumes. These orphaned disks sit in your account, accruing storage charges, forever. A 128GB Standard SSD in Azure costs about $6.40 per month. A 100GB gp3 volume in AWS costs $8 per month. One or two orphaned disks are cheap. A dozen of them (which is not uncommon for businesses that have gone through several rounds of VM creation and deletion) adds up. For related strategies, check out Azure vs AWS for Small Businesses in Florida: An Honest Comparison.

4. Unused Public IPs

Both Azure and AWS charge for static public IP addresses that are not associated with a running resource. The charge is identical on both platforms: about $3.65 per month per unused IP. Businesses in Ormond Beach and DeLand that have cycled through multiple VM configurations often have a handful of these sitting around, costing $15-20 per month for literally nothing.

5. Storage Tier Mismatches

Azure Blob Storage and AWS S3 both offer multiple storage tiers at different price points. Hot/Standard storage is for frequently accessed data. Cool/Infrequent Access is for data accessed less than once a month. Archive is for data you almost never access but need to retain.

Many businesses store everything in the hot tier because that is the default, even though 80% of their stored data has not been accessed in six months. Moving cold data from hot to cool storage reduces costs by 40-60%. Moving it to archive reduces costs by 80-90%.

Running the Azure Cost Audit

Before running the script, you need the Azure CLI installed and authenticated. If you have not done this before, here is the setup:

Install Azure CLI (Ubuntu/Debian)

curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

Or on macOS

brew install azure-cli

Log in to your Azure account

az login

This opens a browser window for authentication

Verify you're connected to the right subscription

az account show --query "{name:name, id:id}" --output table

output:

Name              Id

----------------  ------------------------------------

My Subscription   12345678-abcd-efgh-ijkl-1234567890ab
Now save the following script as <strong>azure_cost_audit.py</strong> and run it:</p>
<p>

python

!/usr/bin/env python3

“””azure_cost_audit.py – Audit Azure spending and find savings opportunities.
Uses Azure CLI for authentication and REST API for cost data.
Python 3.8+ stdlib only (uses az cli subprocess calls).”””

from datetime import datetime, timedelta

def az_cmd(args):
“””Run an Azure CLI command and return JSON output.”””
cmd = [“az”] + args + [“–output”, “json”]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f”Error: {result.stderr.strip()}”, file=sys.stderr)
return None
return json.loads(result.stdout) if result.stdout.strip() else None

def get_subscription_id():
“””Get the current Azure subscription ID.”””
account = az_cmd([“account”, “show”])
return account[“id”] if account else None

def find_unused_resources():
“””Find resources that may be unused or underutilized.”””
findings = []

# Check for unattached disks
disks = az_cmd(["disk", "list"]) or []
for disk in disks:
    if disk.get("diskState") == "Unattached":
        size_gb = disk.get("diskSizeGb", 0)
        monthly_cost = size_gb * 0.05
        findings.append({
            "type": "Unattached Disk",
            "name": disk["name"],
            "resource_group": disk["resourceGroup"],
            "detail": f"{size_gb}GB, ~${monthly_cost:.2f}/month",
            "action": "Delete if not needed, or attach to a VM",
        })

# Check for stopped VMs still incurring charges
vms = az_cmd(["vm", "list", "-d"]) or []
for vm in vms:
    power = vm.get("powerState", "")
    if power == "VM stopped":
        findings.append({
            "type": "Stopped VM (Not Deallocated)",
            "name": vm["name"],
            "resource_group": vm["resourceGroup"],
            "detail": f"Size: {vm.get('hardwareProfile', {}).get('vmSize', 'unknown')}",
            "action": "Deallocate (az vm deallocate) or delete",
        })

# Check for unused public IPs
ips = az_cmd(["network", "public-ip", "list"]) or []
for ip in ips:
    if not ip.get("ipConfiguration"):
        findings.append({
            "type": "Unused Public IP",
            "name": ip["name"],
            "resource_group": ip["resourceGroup"],
            "detail": "~$3.65/month (static)",
            "action": "Delete if not associated with any resource",
        })

# Check for unused NICs
nics = az_cmd(["network", "nic", "list"]) or []
for nic in nics:
    if not nic.get("virtualMachine"):
        findings.append({
            "type": "Unused Network Interface",
            "name": nic["name"],
            "resource_group": nic["resourceGroup"],
            "detail": "No VM attached",
            "action": "Delete if orphaned",
        })

return findings

def generate_report():
“””Generate a complete cost audit report.”””
print(“=” * 60)
print(” AZURE COST AUDIT REPORT”)
print(f” Generated: {datetime.now().strftime(‘%Y-%m-%d %H:%M’)}”)
print(“=” * 60)

sub_id = get_subscription_id()
if not sub_id:
    print("ERROR: Not logged in. Run 'az login' first.")
    return

print(f"nSubscription: {sub_id}")

print("n--- UNUSED RESOURCES ---")
findings = find_unused_resources()
if findings:
    for f in findings:
        print(f"n  [{f['type']}] {f['name']}")
        print(f"    Resource Group: {f['resource_group']}")
        print(f"    Detail: {f['detail']}")
        print(f"    Action: {f['action']}")
    print(f"n  Total findings: {len(findings)}")
else:
    print("  No unused resources found. Nice work!")

print("n--- RECOMMENDATIONS ---")
print("  1. Review all unattached disks - delete or snapshot")
print("  2. Deallocate stopped VMs or delete if unused")
print("  3. Remove unused public IPs ($3.65/month each)")
print("  4. Consider Reserved Instances for always-on VMs (save 40-72%)")
print("  5. Enable auto-shutdown for dev/test VMs")
print("  6. Review Azure Advisor recommendations in the portal")

print("n" + "=" * 60)

if name == “main“:
generate_report()

Let me walk through the key parts. The <strong>az_cmd</strong> function is a wrapper around the Azure CLI that runs commands and returns parsed JSON. This approach means you do not need to install any Python packages beyond the standard library. As long as you have the Azure CLI installed and are logged in, the script works.</p>
<p>The <strong>find_unused_resources</strong> function checks four categories of waste:</p>
<p><strong>Unattached disks</strong> are found by listing all managed disks and filtering for those with a <strong>diskState</strong> of "Unattached." The cost estimate uses $0.05 per GB per month, which is the approximate rate for Azure Standard SSD managed disks. Premium SSDs cost more, so the actual savings could be higher.</p>
<p><strong>Stopped VMs</strong> are different from deallocated VMs. In Azure, a VM in the "VM stopped" state (stopped from inside the OS) is still incurring compute charges. Only the "VM deallocated" state stops the billing. This is one of the most common surprises for Azure users. The script flags any VM in the "stopped" state so you can deallocate it properly.</p>
<p><strong>Unused public IPs</strong> are static IP addresses not associated with any running resource. At $3.65 per month each, they are cheap individually but add up quickly across multiple abandoned resources.</p>
<p><strong>Unused network interfaces</strong> are NICs that were created with a VM but left behind when the VM was deleted. They do not cost money directly, but they clutter your environment and can cause confusion during audits.</p>
<p>Run the script with:</p>
<p>

bash
python3 azure_cost_audit.py

output:

============================================================

AZURE COST AUDIT REPORT

Generated: 2026-03-19 14:30

============================================================

Subscription: 12345678-abcd-efgh-ijkl-1234567890ab

— UNUSED RESOURCES —

[Unattached Disk] test-vm-disk-01

Resource Group: dev-resources

Detail: 128GB, ~$6.40/month

Action: Delete if not needed, or attach to a VM

[Unused Public IP] old-webserver-ip

Resource Group: production

Detail: ~$3.65/month (static)

Action: Delete if not associated with any resource

Total findings: 2

Running the AWS Cost Audit

For AWS, the setup is similar. Install the AWS CLI and configure your credentials:

Install AWS CLI v2 (Linux)

curl “https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip” -o “awscliv2.zip”
unzip awscliv2.zip
sudo ./aws/install

Configure credentials

aws configure

Enter your Access Key ID, Secret Access Key, region (us-east-1), and output format (json)

Verify connection

aws sts get-caller-identity

output: {“UserId”: “…”, “Account”: “123456789012”, “Arn”: “arn:aws:iam::…”}

Now save this as <strong>aws_cost_audit.py</strong>:</p>
<p>

python

!/usr/bin/env python3

“””aws_cost_audit.py – Audit AWS spending and find savings.
Uses AWS CLI for authentication. Python 3.8+ stdlib only.”””

from datetime import datetime, timedelta

def aws_cmd(service, operation, **kwargs):
“””Run an AWS CLI command and return JSON output.”””
cmd = [“aws”, service, operation, “–output”, “json”]
for key, value in kwargs.items():
cmd.extend([f”–{key.replace(‘_’, ‘-‘)}”, value])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f”Error: {result.stderr.strip()}”, file=sys.stderr)
return None
return json.loads(result.stdout) if result.stdout.strip() else None

def find_unused_resources():
“””Find unused AWS resources.”””
findings = []

# Unattached EBS volumes
volumes = aws_cmd("ec2", "describe-volumes",
    filters="Name=status,Values=available")
if volumes:
    for vol in volumes.get("Volumes", []):
        size = vol.get("Size", 0)
        vol_type = vol.get("VolumeType", "gp3")
        monthly = size * 0.08 if vol_type == "gp3" else size * 0.10
        findings.append({
            "type": "Unattached EBS Volume",
            "id": vol["VolumeId"],
            "detail": f"{size}GB {vol_type}, ~${monthly:.2f}/month",
            "action": "Snapshot and delete if not needed",
        })

# Unused Elastic IPs
eips = aws_cmd("ec2", "describe-addresses")
if eips:
    for eip in eips.get("Addresses", []):
        if not eip.get("AssociationId"):
            findings.append({
                "type": "Unused Elastic IP",
                "id": eip.get("AllocationId", "N/A"),
                "detail": f"IP: {eip.get('PublicIp', 'N/A')}, ~$3.65/month",
                "action": "Release if not needed",
            })

# Stopped EC2 instances
instances = aws_cmd("ec2", "describe-instances",
    filters="Name=instance-state-name,Values=stopped")
if instances:
    for res in instances.get("Reservations", []):
        for inst in res.get("Instances", []):
            findings.append({
                "type": "Stopped EC2 Instance",
                "id": inst["InstanceId"],
                "detail": f"Type: {inst.get('InstanceType', 'N/A')} - EBS still charged",
                "action": "Terminate if unused, or start if needed",
            })

return findings

def generate_report():
“””Generate AWS cost audit report.”””
print(“=” * 60)
print(” AWS COST AUDIT REPORT”)
print(f” Generated: {datetime.now().strftime(‘%Y-%m-%d %H:%M’)}”)
print(“=” * 60)

findings = find_unused_resources()
print("n--- UNUSED RESOURCES ---")
if findings:
    for f in findings:
        print(f"n  [{f['type']}] {f['id']}")
        print(f"    Detail: {f['detail']}")
        print(f"    Action: {f['action']}")
    print(f"n  Total findings: {len(findings)}")
else:
    print("  No unused resources found. Nice work!")

print("n--- RECOMMENDATIONS ---")
print("  1. Delete unattached EBS volumes (snapshot first)")
print("  2. Release unused Elastic IPs ($3.65/month each)")
print("  3. Terminate long-stopped instances")
print("  4. Use Savings Plans for consistent workloads (save 30-72%)")
print("  5. Enable AWS Cost Anomaly Detection (free)")
print("  6. Review AWS Trusted Advisor recommendations")
print("n" + "=" * 60)

if name == “main“:
generate_report()

The AWS script follows the same pattern as the Azure one. The <strong>aws_cmd</strong> function wraps the AWS CLI, and the <strong>find_unused_resources</strong> function checks for unattached EBS volumes, unused Elastic IPs, and stopped EC2 instances.</p>
<p>One important difference with AWS: stopped EC2 instances do not incur compute charges (unlike Azure's "stopped" vs "deallocated" distinction), but their attached EBS volumes continue to be charged. The script flags stopped instances so you can decide whether to terminate them and free up the storage.</p>
<h2>What to Do with the Results</h2>
<p>After running either script, you will have a list of findings. Here is how to act on each type:</p>
<h3>Unattached Disks/Volumes</h3>
<p>Before deleting an unattached disk, take a snapshot first. Snapshots cost a fraction of disk storage and give you a safety net if you later realize you needed something on that disk.</p>
<p>

bash

Azure: Snapshot then delete

az snapshot create –resource-group mygroup –source disk-name –name disk-snapshot
az disk delete –resource-group mygroup –name disk-name –yes

AWS: Snapshot then delete

aws ec2 create-snapshot –volume-id vol-1234567890abcdef0 –description “Pre-delete backup”
aws ec2 delete-volume –volume-id vol-1234567890abcdef0

Stopped/Idle VMs

If a VM has been stopped for more than 30 days and nobody has asked about it, it is almost certainly safe to delete. Check with your team first, but do not let fear of deleting something useful keep you paying for something useless.

Unused Public IPs

These are the easiest to clean up. If the IP is not associated with a running resource, delete it. There is no snapshot needed for IP addresses since you can always allocate a new one if needed.

Right-Sizing Recommendations

After cleaning up unused resources, look at your running VMs. Both Azure and AWS provide utilization metrics:

Azure: Check VM CPU utilization over 30 days

az monitor metrics list --resource /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{vm} --metric "Percentage CPU" --interval PT1H --start-time 2026-02-19 --end-time 2026-03-19

AWS: Check EC2 CPU utilization

aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization --dimensions Name=InstanceId,Value=i-1234567890abcdef0 --start-time 2026-02-19T00:00:00 --end-time 2026-03-19T00:00:00 --period 86400 --statistics Average
If average CPU utilization is below 20% over 30 days, you are almost certainly oversized. Drop down one VM size and monitor for a week. If performance is fine, you just saved 30-50% on that VM.</p>
<h2>The Reserved Instance Question</h2>
<p>Both Azure and AWS offer significant discounts for committing to one or three years of usage. Azure calls them Reserved VM Instances. AWS calls them Reserved Instances and Savings Plans. The savings are substantial:</p>
<table>
<thead>
<tr>
<th>Commitment</th>
<th>Azure Savings</th>
<th>AWS Savings</th>
</tr>
</thead>
<tbody>
<tr>
<td>1-year reserved</td>
<td>30-40%</td>
<td>30-40%</td>
</tr>
<tr>
<td>3-year reserved</td>
<td>55-72%</td>
<td>55-72%</td>
</tr>
<tr>
<td>Pay-as-you-go</td>
<td>0% (baseline)</td>
<td>0% (baseline)</td>
</tr>
</tbody>
</table>
<p>For a business in Deltona running a B2s VM 24/7, the pay-as-you-go cost is about $30 per month ($360/year). With a 1-year reservation, that drops to about $21 per month ($252/year). With a 3-year reservation, it drops to about $13 per month ($156/year). Over three years, the reservation saves $612 compared to pay-as-you-go.</p>
<p>The catch is commitment. If you reserve a VM and then realize you do not need it six months later, you are stuck paying for it. For businesses in New Smyrna Beach and DeLand that are still figuring out their cloud architecture, pay-as-you-go gives you flexibility. For businesses in Daytona Beach and Ormond Beach with stable, predictable workloads, reservations are a no-brainer.</p>
<h2>Automating the Audit</h2>
<p>Running the audit manually once is useful. Running it automatically every month is powerful. Here is how to schedule it:</p>
<p>

bash

Linux: Add to crontab (runs first of every month at 8 AM)

crontab -e

Add this line:

0 8 1 * * python3 /opt/scripts/azure_cost_audit.py >> /var/log/cloud-audit.log 2>&1

To email the results, pipe through mail:

0 8 1 * * python3 /opt/scripts/azure_cost_audit.py | mail -s “Monthly Cloud Audit” [email protected]

For Windows, use Task Scheduler to run the script monthly. The audit takes less than a minute to run and uses negligible resources, so there is no cost impact from running it regularly.

The first audit usually finds the most waste. Subsequent monthly audits catch new zombie resources before they accumulate. Think of it as a monthly financial review for your cloud infrastructure, the same way you review your bank statements and credit card charges.

Real Savings: What to Expect

Based on audits I have run for businesses across Volusia County, here is what typical savings look like:

Business Size
Monthly Cloud Spend
Typical Waste Found
Annual Savings

5-10 employees
$200-500
15-25%
$360-1,500

10-20 employees
$500-1,500
20-35%
$1,200-6,300

20-50 employees
$1,500-5,000
25-40%
$4,500-24,000

These numbers are not hypothetical. They come from real audits of real businesses in Deltona, Daytona Beach, Port Orange, and the surrounding communities. The waste exists because cloud platforms are designed to make it easy to create resources and difficult to realize you are paying for things you do not use.

The businesses that save the most are those that have been in the cloud for more than a year without ever running an audit. They have had the most time to accumulate zombie resources, oversized VMs, and storage tier mismatches.

Beyond the Script: Azure Advisor and AWS Trusted Advisor

Both cloud platforms provide their own cost optimization recommendations. After running our scripts, check these built-in tools for additional savings:

Azure Advisor (portal.azure.com > Advisor > Cost) analyzes your usage patterns and suggests right-sizing opportunities, reserved instance purchases, and unused resource cleanup. It is free and automatically updated.

AWS Trusted Advisor (console.aws.amazon.com > Trusted Advisor) provides similar recommendations for AWS resources. The full version requires a Business or Enterprise support plan, but the basic version (free) includes some cost optimization checks.

These tools complement our scripts. The scripts find obvious waste (unused resources). The built-in advisors find optimization opportunities that require pattern analysis (right-sizing based on utilization trends, reserved instance recommendations based on usage history). For a deeper look at this topic, see our guide on CI/CD for Non-Software Companies: Automating Your Infrastructure Deployments.

Frequently Asked Questions

Will these scripts make any changes to my cloud resources?

No. Both scripts are completely read-only. They list resources and identify potential waste, but they do not modify, delete, or change anything. You review the findings and decide what action to take. This is by design since automated deletion of cloud resources is risky and should always involve human review.

How much can a typical small business save by auditing cloud costs?

Based on audits across Volusia County, most small businesses find 20-35% waste in their cloud spending. For a business spending $500 per month on cloud services, that translates to $100-175 per month in savings, or $1,200-2,100 per year. The biggest savings usually come from right-sizing oversized VMs and deleting orphaned storage.

Do I need programming experience to run these scripts?

No. You need Python installed (included on most systems) and either the Azure CLI or AWS CLI configured. The scripts run with a single command (python3 azure_cost_audit.py) and produce a plain-text report. If you can open a terminal and type a command, you can run the audit.

How often should I run a cloud cost audit?

Monthly is ideal. The first audit catches accumulated waste. Monthly follow-ups catch new zombie resources before they become expensive habits. Set up a cron job or scheduled task to automate the process, and review the results alongside your regular financial review.

Can I use these scripts if I am on both Azure and AWS?

Yes, run both scripts. Many businesses in Daytona Beach and Port Orange use Azure for Microsoft 365 and related services while running some workloads on AWS. Each script audits its respective platform independently. The findings and recommendations are platform-specific but the approach is the same: find unused resources, right-size what is running, and consider reservations for stable workloads.

Start Your Audit Today

Your cloud bill is probably higher than it needs to be. Not because the cloud is expensive, but because nobody has looked at what you are actually paying for. These scripts give you visibility in 30 minutes, and the savings they find will pay for the time you spent running them many times over.

Download the scripts, run them against your Azure or AWS account, and see what turns up. Most businesses in Deltona and across Volusia County find their first audit is an eye-opener. And once you see where the waste is, eliminating it is straightforward.

If you want help optimizing your cloud spending or planning a cost-effective migration, reach out. And for context on what cloud migration actually costs in the first place, check our guide on what cloud migration really costs for small businesses.
Free Discovery Call

Start With a Conversation, Not a Commitment

Every engagement begins with a free 30-minute discovery call. We'll map what's slowing your business down and tell you exactly what we'd fix first – no pitch deck, no obligation.