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.
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, andCost
Where it goes#
Save it as Resolve-SyntheticConsumptionSplits.ps1. Use it as a local test
harness before adding live Cost Management queries.
# 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:
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.