Azure Cost Optimization Deep Dive: Real-World Techniques for Smarter Cloud Spend

If you’ve ever opened your Azure bill and felt a mild heart attack coming on, you’re not alone. Cloud costs can balloon faster than a DevOps sprint backlog. The good news? Azure gives you powerful tools and strategies to rein things in, if you know where to look.

Whether you’re an Azure Administrator, DevOps engineer, or cloud architect, these cost optimization techniques will help you stop wasting money, tighten governance, and keep finance from showing up at your desk with “just a few questions.”


Why Azure Cost Optimization Matters

Let’s start with the “why.” Azure’s pay-as-you-go model is both a blessing and a curse. It offers flexibility and also a thousand ways to quietly overspend.

  • Financial Efficiency: Every unused VM, oversized disk, or forgotten test environment translates directly into wasted budget.
  • Resource Utilization: Azure has “infinite” resources, but your credit card doesn’t. Tracking usage ensures you’re not paying for compute that’s just sitting idle.
  • Governance and Compliance: Cost policies keep your organization’s cloud usage within approved standards and help you survive the next audit with dignity intact.

1. Pick the Right Pricing Model — Because Pay-As-You-Go Isn’t Always a Bargain

Pay-As-You-Go is great for experimentation but expensive for production workloads. Instead, explore these options:

  • Azure Reservations: Commit for one or three years and save up to 72%. Perfect for steady-state workloads like SQL databases or domain controllers that never sleep.
  • Spot VMs: Grab unused capacity at up to 90% off, but beware Azure can pull the plug at any time. Ideal for batch jobs, rendering, or other “it’s okay if it fails” tasks.
  • Azure Hybrid Benefit: Bring your existing Windows Server and SQL Server licenses and save up to 55%. It’s like reusing your own coffee mug at Starbucks, you still get your caffeine fix, but for less money (and a little eco-friendly credit, too).
  • Savings Plans for Compute: Commit to a steady hourly rate for one or three years for discounts up to 65%, across multiple VM types and regions.

2. Right-Size Everything — Stop Paying for VMs That Could Bench-Press a Truck

Over-provisioning is one of the most common (and costly) cloud sins. Many admins choose high-spec VMs “just in case” and forget to dial them back.

  • Use Azure Advisor to identify underutilized resources.
  • Scale down or switch to lower-tier VM series based on actual metrics.
  • If performance metrics look good after a week, congratulations! you just trimmed your Azure fat.

3. Shut Down or Delete Unused Resources — Not Everything Needs to Stay Running

Old dev environments, orphaned disks, unused storage accounts — they all add up.
Use Azure Cost Management + Billing or Azure Resource Graph to spot these zombie resources and reclaim your budget.

Pro tip: Implement auto-shutdown schedules for non-production VMs. Think of it as “turning off the lights when you leave the room,” but for your cloud.


4. Automate VM Autoscaling — Match Power to Demand

Azure’s autoscaling feature dynamically adjusts your VM count based on load. It’s the cloud equivalent of cruise control — smooth, efficient, and fuel-saving.

  • Scale up during business hours or traffic spikes.
  • Scale down when things quiet down.
  • Combine with Azure Monitor metrics like CPU or memory utilization for precision scaling.

Result: consistent performance, lower bills, and fewer weekend alerts.


5. Tag and Organize Like a Pro — Because “ResourceGroup1” Isn’t Helpful

Tags are your best friend for financial visibility. Use key-value pairs like:

  • Environment: Production
  • Department: Finance
  • Project: ERPModernization

This allows you to allocate costs accurately, automate governance policies, and keep finance from sending cryptic “Who owns this resource?” emails.

Pair this with Azure Policy to enforce tagging standards and prevent untracked sprawl.


6. Use Storage Tiers Wisely — Don’t Store Cold Data in a Hot Tier

Azure Storage offers three main tiers:

  • Hot: For data you need constantly.
  • Cool: For infrequently accessed data.
  • Archive: For long-term retention, super cheap but takes time to retrieve.

Use Azure Blob lifecycle management to automatically move data to cheaper tiers as it ages. Because paying hot-tier prices for archived logs is like buying concert tickets for a show that ended last year.


7. Leverage Azure Dev/Test Pricing — Save Up to 65% on Non-Production Environments

If you’re a Visual Studio subscriber, you get discounted rates for dev/test workloads.
Apply this to VMs, App Service, SQL Database, and even AKS. It’s one of the easiest wins in Azure cost optimization and yet many teams forget to enable it.


8. Monitor Continuously — Don’t Wait for the Bill Shock

Set up Azure Budgets and Alerts to keep tabs on spending. You can:

  • Define thresholds by subscription or resource group.
  • Get notified when spending approaches budget limits.
  • Automate remediation (like shutting down specific VMs) using Azure Logic Apps.

Continuous monitoring is the difference between proactive management and reactive damage control and your CFO will thank you for it.


9. Combine and Conquer — Hybrid Benefit + Reservations = Massive Savings

By stacking Azure Hybrid Benefit with Reservations, you can achieve up to 80% cost reduction compared to pay-as-you-go.
This combo works wonders for long-running workloads like databases or virtual desktops. Just make sure your licensing and terms align before flipping the switch.


10. Regional Pricing Differences — The “Hidden” Discount

Azure pricing isn’t uniform across regions. Running a workload in East US 2 might be 20–25% cheaper than East US.
If compliance allows, choose the cheaper region but always check latency and data residency requirements first.


11. Automate Cost Governance — Let Azure Policy Be Your Budget Bouncer

Use Azure Policy to enforce rules such as:

  • Restricting VM sizes to approved lists.
  • Enforcing tagging on new resources.
  • Blocking expensive SKUs in dev environments.

It’s like having a strict bouncer who refuses entry to anything not on the approved list except your cloud bill will thank you instead of your Friday night plans.


12. Real-Time Cost Anomaly Detection — Catch Surprises Before They Bite

Azure’s Anomaly Detector API can flag unexpected spending spikes, such as a misconfigured script launching 200 VMs at 3 AM. (Don’t laugh, it’s happened. 😐)

Early alerts mean you can fix the problem before it drains your monthly budget faster than a test run gone rogue.


13. Understand Cost Per Unit — The Secret Weapon of FinOps Teams

Don’t just look at total costs; analyze cost per user, per transaction, or per environment.
This “unit economics” approach helps you identify profitable workloads, adjust pricing models, and eliminate waste.


14. The Big Picture: Automate, Audit, Repeat

Azure cost optimization isn’t a one-and-done activity. It’s a continuous cycle of monitoring, analyzing, and fine-tuning.

Use native tools:

  • Azure Cost Management + Billing
  • Azure Advisor
  • Azure Pricing Calculator
  • Azure Resource Graph
  • Azure Monitor Workbooks

Together, they give you the full visibility and control needed to run a lean, efficient, and predictable Azure environment.


Final Thoughts: Treat Azure Like a Utility Bill

Think of Azure like your home electricity. You wouldn’t leave the lights, oven, and hairdryer running 24/7, right? (At least, we hope not.)
Similarly, unused or oversized cloud resources quietly burn through budget until you notice usually during end-of-month reconciliation.

So take charge:

  • Schedule regular cost audits.
  • Review Azure Advisor recommendations.
  • Automate shutdowns and budget alerts.
  • Educate your teams on the real cost of every deployed resource.

Thank you for stopping by. ✌️

Understanding Entra Enterprise Applications: Why Permissions Matter and How to Audit Them with PowerShell

1. What Are Entra Enterprise Applications?

In Microsoft Entra ID (formerly Azure AD), Enterprise Applications represent the service principals, the actual instances of applications that live inside your tenant.
When you integrate an app (like Salesforce, ServiceNow, or a custom internal API) with Entra, you’re essentially giving it an identity so it can authenticate and access resources securely.

Think of it like this:

  • App registrations define what the app is.
  • Enterprise applications define how that app behaves in your tenant, including what it can access and who can use it. Sometimes called an enterprise application, or enterprise app, it provides the application with an identity to take action within a tenant where the application is added.

They hold the permissions (delegated or application-level), consent records, and configuration that determine how data flows between your tenant and external systems.


2. Why Securing Enterprise Applications Is a Big Deal

Enterprise apps can be your best friend or your biggest liability, depending on how well you control their permissions.

a. Over-Privileged Apps = Attack Surface

It’s common for apps to be granted broad permissions like Directory.ReadWrite.All or User.Read.All “just to make it work.”
But every unnecessary permission is an open door for attackers, especially if a compromised app or account token can be abused to exfiltrate sensitive data.

b. Forgotten Apps Don’t Forget You

Old or unused apps often linger in tenants with valid credentials and active permissions. These “zombie apps” are dangerous, they fly under the radar and can still access data months after their owners have left the company.

c. OAuth Consent Abuse

Attackers love phishing users into consenting to malicious apps. With delegated permissions, a compromised consent can give attackers persistent access without needing a password.

d. Compliance and Visibility

In large environments, hundreds of enterprise applications exist, many connected by departments outside central IT. Without continuous reviews, it’s impossible to guarantee compliance or prove security posture to auditors.


3. Common Security Best Practices

Here are some proven guardrails for managing Entra Enterprise Applications securely:

  • Principle of Least Privilege: Grant only the permissions the app actually needs.
  • Admin Consent Policies: Limit who can consent to high-impact permissions.
  • Lifecycle Management: Regularly review and remove unused applications.
  • App Ownership: Assign clear owners and enforce accountability.
  • Monitor Sign-ins: Enable logging for app sign-ins and failures to detect anomalies.

These controls should be part of your Entra security baseline, not optional extras.


4. Reviewing Permissions with PowerShell

You can’t secure what you can’t see.
That’s why I built a PowerShell script — GetEnterpriseAppPermissions.ps1 — to automate the collection of enterprise application permissions and present them in a clear, exportable report.

What the Script Does

  • Connects to Microsoft Graph via Connect-MgGraph
  • Enumerates all enterprise applications (Get-MgServicePrincipal)
  • Collects delegated and application permissions (OAuth2PermissionGrants, AppRoleAssignments)
  • Outputs the results into a clean CSV or table format, showing which apps have which permissions, consent type, and who consented

Key Columns in the Report

ColumnDescription
App Display NameThe name of the enterprise application
App IDThe unique identifier (Service Principal ID)
Permission TypeDelegated or Application
Permission NameThe Graph API or resource permission assigned
Consent TypeWhether admin or user consent was provided
Granted ByWho consented to the permission
Created DateWhen the permission was granted

Why It’s Useful

  • Provides a snapshot of your current exposure
  • Helps identify overly permissive applications
  • Supports quarterly security reviews
  • Can feed into Power BI dashboards for ongoing monitoring

In short: it’s visibility at scale. The kind of visibility that keeps you out of post-breach “how did we miss this?” meetings.


5. Using the Script

  1. Save the script as GetEnterpriseAppPermissions.ps1
  2. Install the Microsoft Graph PowerShell SDK (if not already): Install-Module Microsoft.Graph -Scope CurrentUser
  3. Run the script: .\GetEnterpriseAppPermissions.ps1
  4. When prompted, sign in with an account that has Directory.Read.All permissions.
  5. The script will generate a CSV file (e.g., EnterpriseAppPermissionsReport.csv) in the same folder.

6. What to Do After You Have the Report

  • Review for risky permissions: Look for *All scopes like Mail.ReadWrite.All or Directory.ReadWrite.All.
  • Check stale apps: Identify applications with no sign-ins or activity for 90+ days.
  • Tighten consent: If a user consented to a permission that should require admin approval, correct it.
  • Remove what’s not needed: Disable or delete unused enterprise applications.

7. Closing Thoughts

Microsoft Entra Enterprise Applications are powerful they enable integration, automation, and innovation.
But with great OAuth comes great responsibility.

If you manage an Entra tenant, treat application permissions with the same scrutiny you’d give to privileged admin accounts. Regular reviews using automation — like this PowerShell script — are not optional; they’re your early warning system.

Run it quarterly.
Report the findings.
Tighten access.
And sleep a little easier knowing you’re not one OAuth consent away from chaos.

Thank you for stopping by. ✌️

Microsoft 365 Admins: August 2025 Ushers in Major Retirements, AI-Powered Features & Key Compliance Shifts – Here’s Your Definitive Guide

If you thought July was intense, buckle up, August 2025 is a heavyweight month for Microsoft 365 changes. Between legacy retirements, AI-driven security enhancements, and new controls across Teams, Outlook, and Purview, this is not the month to sleep on your Message Center.

Whether you’re managing governance, fine-tuning DLP, or trying to avoid last-minute fire drills, this guide breaks it all down into what’s retiring, what’s new, and what needs your immediate attention.

August at a Glance

CategoryCount
🔻 Retirements4
🆕 New Features7
🔧 Enhancements3
🔄 Changes in Functionality1
⚠️ Action Needed3

Retirements: Say Farewell to These Legacy Tools

1. Classic eDiscovery in Microsoft Purview

August 1, 2025 — Say goodbye to Classic eDiscovery, including Content Search, eDiscovery (Standard), and (Premium).
What to do: Migrate to the unified eDiscovery experience for better search, performance, and compliance.
🔗 Learn more

2. Project for the Web & Project in Teams

Early-August 2025 — Microsoft is sunsetting Project for the web. Users will be redirected to Planner and Portfolios.
What to do: Migrate Roadmap data to Portfolios and update any pinned tabs in Teams.
🔗 Details

3. Outlook for Mac: Legacy Switch Retires

Mid-August 2025 — New Outlook becomes default for Mac (v16.100+). Admin toggle to revert will be retired.
What to do: Prepare users for permanent shift by October 2025 (v16.102).
🔗 More info

4. Speaker Coach in Microsoft Teams

Mid-August 2025 — The preview feature providing real-time feedback during meetings will be retired.
What to do: Inform users and explore alternatives like Copilot-generated meeting recaps.
🔗 Announcement

New Features: Worth Your Immediate Attention

AI-Powered Data Security Investigations in Purview

An all-new AI-driven tool for visualizing data risk, investigating incidents, and refining policies, now built into Microsoft Purview.
🔗 Details

Advanced Mail Merge in Outlook for Web & New Outlook

August 2025 — Personalize email templates with dynamic fields, custom formatting, and preview features.
🔗 Roadmap

Copilot Blocked from Processing Labeled Emails via DLP

August 2025 — Microsoft Purview DLP will block Copilot from interacting with labeled content in chat.
🔗 Read more

Risky AI Usage Detection in Insider Risk Management

Early-August 2025 — Detect prompts, intents, and AI-generated content using Microsoft 365 Copilot, Copilot Studio, and ChatGPT Enterprise.
🔗 More info

Silent Test Calls in Teams for Network Diagnostics

Early-August 2025 — Run silent test calls via Teams Premium to proactively check network readiness.
🔗 Message Center

Rule-Based Management of Certified Teams Apps

Mid-August 2025 — Automatically manage apps based on permission access and publisher trust status.
🔗 Roadmap

Independent DLP Email Notification Settings

August 2025 — Decouple policy tips and notifications in SharePoint/OneDrive DLP settings.
🔗 Roadmap

Enhancements: Quiet but Important

  • Updated Audit Logs in Purview – Better granularity and new Pre/Post Execution messages for role group changes.
    🔗 Read more
  • Microsoft Fabric Workspace User Limit – Enforcing a max of 1,000 users/groups per workspace role.
    🔗 Details
  • Apple/Google Sign-In on Teams Web – New SSO methods are coming for consumer users (preview).
    🔗 Message Center

Functionality Change: Stay Updated

Updated Sender for Teams DLP Incident Emails

August 20, 2025 — Teams DLP GIR emails will only come from no-reply@teams.mail.microsoft.com.
What to do: Update inbox rules and alert filters if needed.
🔗 Message Center

Action Needed: These Deadlines Are Not Flexible

Entra ID Retention Policy for Access Reviews

August 15, 2025 — Only 12 months of access review data will be available via UI/API.
What to do:

  • Export old data using Graph API
  • Store reports securely
  • Create an annual backup process
    🔗 More info

Legacy Message Trace Retires in Exchange Online

August 31, 2025 — New Message Trace UI and V2 cmdlets become the default.
What to do: Update any scripts to use Get-MessageTraceV2 and Get-MessageTraceDetailV2.
🔗 Read more

Azure AD Graph API Retirement

August 31, 2025 — Azure AD Graph API officially ends; apps using it will stop working.
What to do: Migrate to Microsoft Graph API. Use Entra admin center to identify impacted apps.
🔗 Migration Help

Final Thoughts

August 2025 is a pivotal month between the rise of AI-enhanced compliance tools and the retirement of legacy Microsoft features, the Microsoft 365 ecosystem is evolving fast.

If you’re responsible for security, collaboration, or compliance, now’s the time to document changes, communicate with your teams, and adjust scripts and policies. Waiting until the last minute will put you behind both operationally and reputationally.

  • Bookmark this.
  • Share it with your team.
  • Knock out the action items before they knock on your door.

Thank you for stopping by. ✌️

Microsoft 365 Admins: July 2025 Brings Major Retirements, Game-Changing Features & Critical Actions – Here’s Your Definitive Guide

Alright admins, deep breath. July is rolling in hot with some of the biggest Microsoft 365 updates, retirements, and must-do tasks of the year. Whether you’re wrangling SharePoint, securing sensitive data, or prepping Teams for your org, this month has something that will definitely land on your radar and maybe on your weekend schedule if you don’t plan ahead.

Consider this your field guide to navigate July 2025 without missing a beat.

July at a Glance

CategoryCount
🔻 Retirements7
🆕 New Features11
🔧 Enhancements8
🔄 Changes in Functionality5
⚠️ Action Needed7

Retirements: Say Goodbye to These

  1. Microsoft 365 Business Premium & Office 365 E1 Grants for Non-Profits
    Retiring July 1, 2025 — Non-profits must move to Microsoft 365 Business Basic grants or discounted plans.
    ➡️ Learn more
  2. Viva Engage Private Content Mode
    Retiring June 30, 2025 — All tenants will lose access to Private Content Mode across Viva Engage, Teams, and Outlook.
    ➡️ Details
  3. Monitor Action in Defender Safe Attachments Policies
    Gone Early-July 2025 — Monitor mode will be switched to Block; evaluate Safe Attachments settings now.
    ➡️ More info
  4. SharePoint Alerts
    Phased retirement starts July 2025 — Power Automate or SharePoint Rules recommended as replacements.
    ➡️ Guidance
  5. OneNote .DOC Export Option
    Ending July 28, 2025 — Shift to modern formats like .docx now.
    ➡️ Message Center
  6. Organization Data Type in Excel
    Retiring July 31, 2025 — Switch to Get Data > From Power BI or custom data types via add-ins.
    ➡️ Learn more
  7. TLS 1.1 & Older on Fabric Platform
    Deprecated July 31, 2025 — Update systems to TLS 1.2+ to avoid data connectivity issues.
    ➡️ Blog post

New Features: Hot Off the Press

  • Native Forms in SharePoint Libraries — Build forms directly inside document libraries for smoother file uploads.
    ➡️ Roadmap
  • Cold File Scanning for Sensitive Info — Microsoft Purview now scans old, untouched files in SharePoint/OneDrive.
    ➡️ Details
  • Unit-Level Backup Deletion in Microsoft 365 Backup — Delete backups for specific OneDrive, SharePoint, or Exchange units.
    ➡️ Roadmap
  • External Chat File Attachments in Teams — Finally attach files in 1:1 and group chats with external users.
    ➡️ Message Center
  • Detailed Audit Logs for Screen Sharing in Teams — Gain full transparency over Give/Take Control and sharing events.
    ➡️ Read more
  • Facilitator Agent in Teams — Automated meeting summaries and real-time note collaboration (Copilot license required).
    ➡️ Details
  • Multi-Admin Notifications for M365 Backup — Configure centralized alerts for backup events.
    ➡️ Roadmap
  • AI Posture Management in Purview — Manage security of AI activity across Copilot and other AI apps.
    ➡️ Message Center
  • Drag & Drop Between Accounts in New Outlook — Attach emails/files across accounts or shared mailboxes seamlessly.
    ➡️ Details
  • Network-Level Detection of AI Activity in Insider Risk Management — Identify sensitive data shared with cloud/AI apps.
    ➡️ Message Center
  • Scoped AD Domain Access in Defender for Identity — Apply RBAC at the AD domain level for tighter security.
    ➡️ Details

Enhancements: Small Changes, Big Impact

  • Attachment Previews in Purview Content Explorer — View flagged attachments directly in the console.
    ➡️ Details
  • Recording & Transcription by Default in Teams Calls — Enabled by default for new tenants and global policies.
    ➡️ More info
  • New Outlook: S/MIME Signature Inheritance Setting — Control signature behavior in replies via NoSignOnReply.
    ➡️ Message Center
  • User Activity Timeline in Purview Compliance Portal — See flagged user interactions on a single timeline.
    ➡️ Details
  • IRM + Data Security Investigation Integration — Launch investigations faster with combined tools.
    ➡️ Message Center
  • Secure by Default Settings in Microsoft 365 — Block legacy auth and enforce admin consent by default.
    ➡️ Details
  • Best Practice Dashboard Expansion in Teams Admin Center — Monitor new meeting-related issues.
    ➡️ Read more
  • On-Demand File Classification — Discover/classify old files in SharePoint/OneDrive (pay-as-you-go).
    ➡️ Details

Existing Functionality Changes: Adjust Your Ops

  • Teams Live Event Assistance Becomes Paid — LEAP moves under Unified as a paid service on July 1, 2025.
    ➡️ More info
  • Insider Risk Policy Limits Increased — Up to 100 total active policies across templates.
    ➡️ Roadmap
  • Outlook Blocks More File Types — .library-ms and .search-ms added to the blocked list.
    ➡️ Details
  • Improved B2B Guest Sign-In — Guests redirected to their home org’s sign-in page for clarity.
    ➡️ Message Center
  • Unified Teams App Management Paused — Rollout delay with updates expected by late July.
    ➡️ Details

Action Needed: Don’t Procrastinate

  • Azure AD PowerShell Retirement After July 1 — Migrate scripts to Microsoft Graph or Entra PowerShell ASAP.
    ➡️ Details
  • DNS Provision Change — Update automation scripts to retrieve MX records via Graph API to avoid mail flow issues.
    ➡️ Message Center
  • Classic Teams App Retirement — All users must move to New Teams or web app by July 1, 2025.
    ➡️ Details
  • Reshare SharePoint Content Post-Entra B2B — External users lose access to pre-integration OTP shares. Reshare content now.
    ➡️ Message Center
  • Teams Android Devices Must Update Apps — Move to supported versions by Dec 31, 2025, to enable modern auth.
    ➡️ Details
  • Graph Beta API Permissions Update — Adjust apps to use new permissions for device management by July 31, 2025.
    ➡️ Message Center

Final Thoughts

July 2025 is a make-or-break month for Microsoft 365 admins. There’s a mountain of changes, but staying ahead means no late-night incidents, no broken workflows, and definitely no panicked calls from leadership.

Bookmark this guide, share it with your team, and start planning now. Because in IT, the only thing worse than unexpected downtime is knowing you could’ve avoided it.

Thank you for stopping by. ✌️

Generate Multi-Subscription Azure Cost Reports Using REST API and PowerShell

Managing cloud costs is like trying to diet at a buffet. Tempting services everywhere, and one bad decision can blow your budget wide open. So, I was tasked for a breakdown of Azure usage across 50+ subscriptions for the month of June, I knew this wasn’t going to be a quick Azure Portal copy-paste job.

Instead, I rolled up my sleeves and built a PowerShell script that uses the Azure REST API to automatically:

  • Query all accessible subscriptions
  • Fetch usage-based cost data for a given time range
  • Export it into a clean Excel report

And I made it smart enough to handle throttling too. Here’s how it all came together.

Goals

  • Pull Azure cost data from multiple subscriptions
  • Offer flexible time range selection (this month, last month, custom, etc.)
  • Authenticate securely with Entra ID (Service Principal)
  • Export to Excel in a way leadership can digest (bonus points if it opens without errors)

Authentication with Entra ID

I created a Service Principal and assigned it the “Global Billing Reader” role at the billing account level. The script uses the client_credentials flow to authenticate and obtain an access token.

Yes, I temporarily stored the client secret in a plain text variable $clientSecretPlain = 'ENTER_SECRET' because I was still prototyping. Don’t judge me. But for production? Vault it or a managed identity.

Handling Throttling (429 Errors)

Azure’s APIs like to throw shade when you hit them too hard. I added retry logic with exponential backoff and jitter.

PowerShell Script

# Author: Kumaran Alagesan

# Requires: Az CLI, ImportExcel module (Install-Module -Name ImportExcel)
# Authenticate using Entra Application (Service Principal)

$clientId = 'ENTER_APP_ID'
$tenantId = 'ENTER_Tenant_ID'
$clientSecretPlain = 'ENTER_SECRET'

# Get access token using Service Principal
$body = @{
    grant_type    = "client_credentials"
    client_id     = $clientId
    client_secret = $clientSecretPlain
    scope         = "https://management.azure.com/.default"
}
$tokenResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body $body -ContentType "application/x-www-form-urlencoded"
if (-not $tokenResponse.access_token) {
    Write-Host "Failed to acquire token. Check credentials." -ForegroundColor Red
    exit 1
}
$token = @{ accessToken = $tokenResponse.access_token }


$selection = $null
while (-not $selection) {
    $selection = Read-Host "Select time range: `n1) This month`n2) Last month`n3) This quarter`n4) Last quarter`n5) This year`n6) Last 6 months`n7) Last 12 months`n8) Custom`nEnter number"
    if ($selection -notmatch '^[1-8]$') {
        Write-Host "Invalid selection. Please enter a number from the list (1-8)." -ForegroundColor Yellow
        $selection = $null
    }
}

$today = Get-Date
switch ($selection) {
    '1' { # This month
        $startDate = Get-Date -Year $today.Year -Month $today.Month -Day 1
        $endDate = $today
    }
    '2' { # Last month
        $lastMonth = $today.AddMonths(-1)
        $startDate = Get-Date -Year $lastMonth.Year -Month $lastMonth.Month -Day 1
        $endDate = (Get-Date -Year $lastMonth.Year -Month $lastMonth.Month -Day 1).AddMonths(1).AddDays(-1)
    }
    '3' { # This quarter
        $quarter = [math]::Ceiling($today.Month / 3)
        $startMonth = (($quarter - 1) * 3) + 1
        $startDate = Get-Date -Year $today.Year -Month $startMonth -Day 1
        $endDate = $today
    }
    '4' { # Last quarter
        $currentQuarter = [math]::Ceiling($today.Month / 3)
        if ($currentQuarter -eq 1) {
            $lastQuarterYear = $today.Year - 1
            $lastQuarter = 4
        } else {
            $lastQuarterYear = $today.Year
            $lastQuarter = $currentQuarter - 1
        }
        $startMonth = (($lastQuarter - 1) * 3) + 1
        $startDate = Get-Date -Year $lastQuarterYear -Month $startMonth -Day 1
        $endDate = (Get-Date -Year $lastQuarterYear -Month $startMonth -Day 1).AddMonths(3).AddDays(-1)
    }
    '5' { # This year
        $startDate = Get-Date -Year $today.Year -Month 1 -Day 1
        $endDate = $today
    }
    '6' { # Last 6 months
        $startDate = $today.AddMonths(-5)
        $startDate = Get-Date -Year $startDate.Year -Month $startDate.Month -Day 1
        $endDate = $today
    }
    '7' { # Last 12 months
        $startDate = $today.AddMonths(-11)
        $startDate = Get-Date -Year $startDate.Year -Month $startDate.Month -Day 1
        $endDate = $today
    }
    '8' { # Custom
        $startDate = Read-Host "Enter start date (yyyy-MM-dd)"
        $endDate = Read-Host "Enter end date (yyyy-MM-dd)"
        try {
            $startDate = [datetime]::ParseExact($startDate, 'yyyy-MM-dd', $null)
            $endDate = [datetime]::ParseExact($endDate, 'yyyy-MM-dd', $null)
        } catch {
            Write-Host "Invalid date format. Exiting." -ForegroundColor Red
            exit 1
        }
    }
}

$startDateStr = $startDate.ToString("yyyy-MM-dd")
$endDateStr = $endDate.ToString("yyyy-MM-dd")

# Set headers for REST calls using the service principal token
$headers = @{
    'Authorization' = "Bearer $($token.accessToken)"
    'Content-Type'  = 'application/json'
}

# Get all subscriptions
$subsUrl = "https://management.azure.com/subscriptions?api-version=2020-01-01"
$subscriptions = Invoke-RestMethod -Uri $subsUrl -Headers $headers -Method Get | Select-Object -ExpandProperty value

Write-Host "Fetching cost data for $($subscriptions.Count) subscriptions: " -NoNewline

$totalCost = 0
$results = @()

foreach ($sub in $subscriptions) {
    $costQueryBody = @{
        type       = "Usage"
        timeframe  = "Custom"
    timePeriod = @{
        from = $startDateStr
        to   = $endDateStr
    }
    dataSet    = @{
        granularity = "None"
        aggregation = @{
            totalCost = @{
                name     = "Cost"
                function = "Sum"
            }
        }
    }
} | ConvertTo-Json -Depth 10

    $costUrl = "https://management.azure.com/subscriptions/$($sub.subscriptionId)/providers/Microsoft.CostManagement/query?api-version=2024-08-01"

    $maxRetries = 7
    $retryDelay = 5
    $attempt = 0
    $success = $false

    while (-not $success -and $attempt -lt $maxRetries) {
        try {
            $costData = Invoke-RestMethod -Uri $costUrl -Headers $headers -Method Post -Body $costQueryBody

            $subscriptionCost = 0
            if ($costData.properties.rows -and $costData.properties.rows.Count -gt 0) {
                $subscriptionCost = $costData.properties.rows[0][0]
            }

            $results += [PSCustomObject]@{
                'Subscription Name' = $sub.displayName
                'Total Cost'        = [math]::Round([double]$subscriptionCost, 2)
            }

            $totalCost += $subscriptionCost
            Write-Host "." -NoNewline
            $success = $true
        }
        catch {
            if ($_.Exception.Response.StatusCode.value__ -eq 429 -and $attempt -lt ($maxRetries - 1)) {
                # Add random jitter to delay
                $jitter = Get-Random -Minimum 1 -Maximum 5
                $sleepTime = $retryDelay + $jitter
                Write-Host "`n429 received, retrying in $sleepTime seconds..." -ForegroundColor Yellow
                Start-Sleep -Seconds $sleepTime
                $retryDelay *= 2
                $attempt++
            }
            else {
                Write-Host "x" -NoNewline
                Write-Host "`nError getting cost for subscription $($sub.displayName): $($_.Exception.Message)" -ForegroundColor Red
                $success = $true
            }
        }
    }
}

# Export results to Excel
$excelPath = Join-Path -Path $PSScriptRoot -ChildPath ("AzureCostReport_{0}_{1}.xlsx" -f $startDateStr, $endDateStr)
if ($results.Count -gt 0) {
    # Do not pre-format 'Total Cost' as string; keep as number for Excel formatting

    # Check if file is locked
    $fileLocked = $false
    if (Test-Path $excelPath) {
        try {
            $stream = [System.IO.File]::Open($excelPath, 'Open', 'ReadWrite', 'None')
            $stream.Close()
        } catch {
            $fileLocked = $true
        }
    }
    if ($fileLocked) {
        Write-Host "Excel file is open or locked: $excelPath. Please close it and run the script again." -ForegroundColor Red
    } else {
        $results | Export-Excel -Path $excelPath -WorksheetName 'CostReport' -AutoSize -TableName 'CostSummary' -Title "Azure Cost Report ($startDateStr to $endDateStr)" -TitleBold -ClearSheet
        Write-Host "Excel report saved to: $excelPath"
        # Optionally open the file
        if ($IsWindows) {
            Start-Sleep -Seconds 2
            Invoke-Item $excelPath
        }
    }
}

If you want to email the output as a table in the body to a mailbox, you can replace the ‘Export results to Excel’ section with the code below. Yup! I know Send-MailMessage is obsolete and ideally I’d run this script with in an Azure automation account and set app permissions for the identity to be able to send emails. I’ll cover it in a later post.

# Prepare HTML table for email
if ($results.Count -gt 0) {
    # Add $ symbol to each Total Cost value
    $resultsWithDollar = $results | ForEach-Object {
        $_ | Add-Member -NotePropertyName 'Total Cost ($)' -NotePropertyValue ('$' + [math]::Round([double]$_.('Total Cost'), 2)) -Force
        $_
    }

    $htmlTable = $resultsWithDollar | Select-Object 'Subscription Name', 'Total Cost ($)' | ConvertTo-Html -Property 'Subscription Name', 'Total Cost ($)' -Head "<style>table{border-collapse:collapse;}th,td{border:1px solid #ccc;padding:5px;}</style>" -Title "Azure Cost Report"
    $htmlBody = @"
<h2>Azure Cost Report ($startDateStr to $endDateStr)</h2>
$htmlTable
<p><b>Total Cost (all subscriptions):</b> $([string]::Format('${0:N2}', [math]::Round([double]$totalCost,2)))</p>
<p style='color:gray;font-size:small;'>This is an automatically generated email - Please do not reply.</p>
"@

    # Email parameters (update these as needed)
    $smtpServer = "smtpserver@domain.com"
    $smtpPort = 587
    $from = "alerts@domain.com"
    $to = "emailaddress@domain.com"
    $subject = "Azure Cost Report ($startDateStr to $endDateStr)"

    Send-MailMessage -From $from -To $to -Subject $subject -Body $htmlBody -BodyAsHtml -SmtpServer $smtpServer -Port $smtpPort
    Write-Host "Cost report sent via email to $to"
} else {
    Write-Host "No results to send."
}

What You’ll Get

The final Excel report displays each subscription’s name alongside its total cost for your chosen time period. Whether you’re reviewing it manually or feeding it into FinOps tools, the format is designed for quick analysis and clean presentation.

Practical Applications

ScenarioHow It Helps
Automation and schedulingSupports routine reporting via scheduled tasks or DevOps flows
Multi-subscription environmentsConsolidates cost data across departments or teams
Governance and FinOpsEnables proactive budget tracking and reporting

With just a PowerShell script and the Azure Cost Management API, you can unlock instant insights into your cloud spend across all Azure subscriptions. Whether you’re part of a DevOps team, driving FinOps initiatives, or simply managing cloud budgets, this automation makes cost visibility one less thing to worry about.

Lessons Learned

  • Azure Cost Management API is powerful, but throttling is real.
  • Microsoft will be retiring the Consumption Usage Details API at some point in the future and does not recommend that you take a new dependency on this API.
  • Export-Excel is a lifesaver, especially when you want your report to actually be readable.

Room for Improvement

  • Add Azure MeterCategory per subscription in the email report to give a better idea of where the cost usage is
  • Move secrets to Azure Key Vault or use Managed Identity
  • Add monthly trend analysis and forecasting
  • Push the data to Power BI for richer dashboards

Final Thoughts

This script is now my go-to tool for quickly generating Azure cost reports across environments. It’s flexible, reliable, and gives my leadership team the visibility they need to make informed decisions, without logging into the portal.

Because let’s face it: if you’re managing Azure at scale, you shouldn’t be clicking through billing blades. You should be scripting your way to clarity.

Keep those costs in check, one API call at a time.

Thanks for stopping by. ✌