Snippet

Split synthetic subscription cost by resource group

A complete PowerShell script that matches synthetic resource-group cost rows to app slices and reconciles the remainder.

powershellOct 6, 2026MIT

AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy

What it does#

Splits one subscription’s synthetic monthly cost into app rows by matching resource-group names. Anything unmatched goes to a remainder row, and the script verifies that all rows add back to the original subscription total.

You need#

  • PowerShell 7.6.2 or newer
  • No Azure modules
  • Synthetic or already-approved cost rows with these fields: Subscription, ResourceGroup, Month, and Cost

Where it goes#

Save it as Resolve-SyntheticConsumptionSplits.ps1. Use it as a local test harness before adding live Cost Management queries.

Resolve-SyntheticConsumptionSplits.ps1
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Sarang Datye
#Requires -Version 7.4
[CmdletBinding()]
param()

function Test-ResourceGroupMatch {
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [string[]]$Patterns
    )

    foreach ($pattern in $Patterns) {
        if ($Name -like $pattern) {
            return $true
        }
    }

    return $false
}

function Resolve-ConsumptionSplits {
    param(
        [Parameter(Mandatory)]
        [object[]]$CostRows,

        [Parameter(Mandatory)]
        [object[]]$SplitRules
    )

    $resolved = New-Object System.Collections.Generic.List[object]

    foreach ($subscriptionGroup in ($CostRows | Group-Object Subscription, Month)) {
        $rows = @($subscriptionGroup.Group)
        $subscription = $rows[0].Subscription
        $month = $rows[0].Month
        $total = ($rows | Measure-Object -Property Cost -Sum).Sum
        $rule = $SplitRules | Where-Object { $_.Subscription -eq $subscription } | Select-Object -First 1

        if (-not $rule) {
            $resolved.Add([pscustomobject]@{
                Subscription = $subscription
                Month        = $month
                App          = 'Unsplit subscription'
                Slice        = 'subscription'
                Cost         = [decimal]$total
            })
            continue
        }

        $assignedGroups = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
        $assignedTotal = [decimal]0

        foreach ($slice in $rule.Slices) {
            $sliceRows = @(
                $rows | Where-Object {
                    -not $assignedGroups.Contains($_.ResourceGroup) -and
                        (Test-ResourceGroupMatch -Name $_.ResourceGroup -Patterns $slice.Match)
                }
            )

            foreach ($sliceRow in $sliceRows) {
                [void]$assignedGroups.Add($sliceRow.ResourceGroup)
            }

            $sliceCost = [decimal](($sliceRows | Measure-Object -Property Cost -Sum).Sum)
            $assignedTotal += $sliceCost

            $resolved.Add([pscustomobject]@{
                Subscription = $subscription
                Month        = $month
                App          = $slice.App
                Slice        = $slice.Slug
                Cost         = $sliceCost
            })
        }

        $remainderCost = [decimal]$total - $assignedTotal
        $resolved.Add([pscustomobject]@{
            Subscription = $subscription
            Month        = $month
            App          = $rule.RemainderApp
            Slice        = 'remainder'
            Cost         = $remainderCost
        })
    }

    return $resolved
}

$syntheticCostRows = @(
    [pscustomobject]@{ Subscription = '<subscription-id-1>'; ResourceGroup = 'rg-api-prod-001'; Month = '2026-01'; Cost = 1200.00 }
    [pscustomobject]@{ Subscription = '<subscription-id-1>'; ResourceGroup = 'rg-worker-prod-001'; Month = '2026-01'; Cost = 800.00 }
    [pscustomobject]@{ Subscription = '<subscription-id-1>'; ResourceGroup = 'rg-shared-prod-001'; Month = '2026-01'; Cost = 300.00 }
)

$splitRules = @(
    @{
        Subscription = '<subscription-id-1>'
        Portfolio    = 'Core Platform'
        RemainderApp = 'Shared Platform'
        Slices       = @(
            @{ Slug = 'api'; App = 'API Service'; Match = @('rg-api-*') }
            @{ Slug = 'worker'; App = 'Worker Service'; Match = @('rg-worker-*', 'rg-api-*') }
        )
    }
)

$resolvedRows = Resolve-ConsumptionSplits -CostRows $syntheticCostRows -SplitRules $splitRules
$sourceTotal = ($syntheticCostRows | Measure-Object -Property Cost -Sum).Sum
$resolvedTotal = ($resolvedRows | Measure-Object -Property Cost -Sum).Sum

if ([decimal]$sourceTotal -ne [decimal]$resolvedTotal) {
    throw "Split rows do not reconcile. Source: $sourceTotal Resolved: $resolvedTotal"
}

$resolvedRows | Sort-Object App | Format-Table -AutoSize

Check that it works#

Run:

powershell
pwsh -NoProfile -File ./Resolve-SyntheticConsumptionSplits.ps1

You should see rows for API Service, Worker Service, and Shared Platform. Their total should match the original synthetic subscription total.