Azure waste

Dedicated workload profile nothing runs on

A Container Apps environment holding open a Dedicated workload profile that no app or job is assigned to.
The agent reports this
Reversible
Why it happens

A Container Apps Dedicated workload profile reserves a node and bills for the vCPU and memory provisioned in it, not for what runs on it. Microsoft states it plainly: you are billed for the total number of vCPUs and memory provisioned in the profile, per second that each instance is running. So a profile with a minimum count of one bills its whole node continuously even when nothing is scheduled on it. At eastus rates an idle D4 is about $225 a month and a D32 about $2,463, before the separate Dedicated plan management fee. This commonly happens after an app is moved to Consumption or deleted and the profile it used is left behind. Profiles with a minimum count of zero are ignored, because a profile that scales to zero costs nothing while it exists. Both container apps and jobs are checked, since a job can hold a profile just as an app can.

Paste this into Resource Graph Explorer in the Azure portal.

resources
| where type =~ 'microsoft.app/managedenvironments'
// Scope below matches CloudFinOpsPro policy DEFAULTS: exclusions.protectTag, environment.tagKeys,
// environment.allowedTagValues, and allowUntagged false so an untagged resource counts as production.
// If your policy.yaml differs, edit the three lines below or this will disagree with your own agent.
| where tolower(tostring(coalesce(tags['cfop:protect'], ''))) !in ('true', 'yes', '1')
| extend cfopEnv = tolower(trim(' ', tostring(coalesce(tags['Environment'], tags['environment'], tags['env'], ''))))
| where cfopEnv in ('dev', 'development', 'test', 'qa', 'sandbox', 'nonprod', 'non-prod', 'staging')
// One row per workload profile on the environment.
| mv-expand profile = properties.workloadProfiles
| extend profileName = tostring(profile.name),
         profileType = tostring(profile.workloadProfileType),
         minimumCount = toint(profile.minimumCount)
// Dedicated categories only. Flex carries 32 cores and bills per replica, so selecting on the
// presence of a size rather than the family would invent a four-figure monthly cost for it.
| where profileType startswith 'D' or profileType startswith 'E'
| where profileType !startswith 'Consumption'
// minimumCount 0 is not waste: a profile that scales to zero costs nothing while it exists.
| where minimumCount >= 1
// Anything assigned to the profile, apps AND jobs. A job holds a profile exactly as an app does.
| join kind=leftouter (
    resources
    | where type in~ ('microsoft.app/containerapps', 'microsoft.app/jobs')
    | project envId = tolower(tostring(properties.environmentId)),
              usedProfile = tostring(properties.workloadProfileName)
    | where isnotempty(usedProfile)
    | distinct envId, usedProfile
  ) on $left.assignKey == $right.assignKey
| extend assignKey = strcat(tolower(id), '|', profileName)
| summarize assigned = countif(isnotempty(usedProfile)) by id, name, resourceGroup, location, profileName, profileType, minimumCount
| where assigned == 0
| project name, resourceGroup, location, profileName, profileType, minimumCount

Container Apps publishes no per-profile cost meter, so this query lists the idle profiles and their sizes rather than pricing them. The agent prices them from the live Dedicated vCPU and memory meters for the environment's region.

Applies to microsoft.app/managedenvironments
What the agent does about it

A Container Apps environment holding open a Dedicated workload profile that no app or job is assigned to.

It will not fix this one

It reports this and takes no action. A check only earns the right to change something once the action behind it can re-read live state before it fires and knows the way back.