Azure waste

App Service Environments with no plans

An isolated hosting environment with nothing in it
The agent tells you how to fix it
Why it happens

Microsoft charges an empty environment as though one machine were running in it. The environment is easy to forget because deleting the last app does not delete it.

Microsoft charges an empty environment as though one Windows I1v2 instance were deployed in it, so an environment running nothing still bills. Deleting the last app does not delete the environment underneath it.

Paste this into Resource Graph Explorer in the Azure portal.

resources
| where type =~ 'microsoft.web/hostingenvironments'
// 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')
// Dedicated host environments bill for two hosts plus a per-vCore rate, which is a different
// model, so they are out of scope rather than priced by a figure that is not theirs.
| where toint(coalesce(properties.dedicatedHostCount, 0)) == 0
| extend aseId = tolower(id)
// An App Service plan names its environment. Resource Graph supports only innerunique, inner,
// leftouter and fullouter, so this is leftouter plus a null test rather than leftanti: the
// environments no plan matched are the ones where the joined column came back empty.
| join kind=leftouter (
    resources
    | where type =~ 'microsoft.web/serverfarms'
    | extend aseId = tolower(tostring(properties.hostingEnvironmentProfile.id))
    | where isnotempty(aseId)
    | distinct aseId
    | extend planFound = aseId
) on aseId
| where isempty(planFound)
| project name, resourceGroup, location,
          aseKind = tostring(kind),
          zoneRedundant = tostring(properties.zoneRedundant)

Microsoft's App Service Environment overview states it directly: if the environment is empty you are charged as if you have one instance of Windows I1v2. Verified live in East US, that rate is $0.547 an hour. That figure is the v3 one, and it is the only one that can apply: Microsoft's retirement notice states App Service Environment v1 and v2 are retired as of 31 August 2024, because they ran on Cloud Services (classic). Dedicated host environments are excluded because Microsoft bills them for two dedicated hosts plus a per-vCore rate instead.

Applies to microsoft.web/hostingenvironments
What the agent does about it

An App Service Environment hosting no App Service plans.

It will not fix this one

It finds it and tells you what to change. The change itself is yours to make, because it is not one the agent should make on your behalf.