Azure waste

ExpressRoute gateway with no circuit connected to it

An ExpressRoute gateway that no connection object references, billing its full hourly rate with no circuit attached to it.
The agent reports this
Why it happens

An ExpressRoute gateway bills on provisioned uptime rather than traffic, so one with no circuit attached costs exactly what a saturated one costs. Measured live in East US the rates run from $0.19 an hour for Standard, about $138.70 a month, to $2.151 for ErGw3AZ, about $1,570.23 a month, which makes this among the most expensive single resources in the catalogue. The common shape is a migration that finished on one side: the circuit was released or moved and the gateway fronting it was left running. Attachment is decided by connection objects, which name the gateway they terminate, and the rule reports nothing at all if connections were not collected, because an uncollected type and an empty one are the same empty list and treating them alike would report every gateway in the estate as unused. The remedy needs care in a way most findings do not: an ExpressRoute gateway fronts a contracted circuit, so a gateway with nothing attached is either waste or a migration that is not finished, and deleting one can sever a link the customer is paying a carrier for separately. It also cannot be stopped, deleting it is the only way to end the charge, recreating it takes about 45 minutes, and the public IP is not preserved.

Paste this into Resource Graph Explorer in the Azure portal.

resources
| where type =~ 'microsoft.network/virtualnetworkgateways'
// 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')
// VPN and ExpressRoute gateways are the SAME ARM type, told apart only by gatewayType.
// Matched exactly rather than by prefix, so a value Azure adds later is left alone.
| where tostring(properties.gatewayType) =~ 'ExpressRoute'
| extend gwId = tolower(id)
// Resource Graph does not support `join kind=leftanti`, so the anti-join is a leftouter
// plus an emptiness test. Both ends are checked because a connection object names a
// gateway in either field and reading only the first reports the far end of a pair.
| join kind=leftouter (
    resources
    | where type =~ 'microsoft.network/connections'
    | project gwId = tolower(tostring(properties.virtualNetworkGateway1.id)), attachedNear = 'yes'
  ) on gwId
| join kind=leftouter (
    resources
    | where type =~ 'microsoft.network/connections'
    | project gwId = tolower(tostring(properties.virtualNetworkGateway2.id)), attachedFar = 'yes'
  ) on gwId
| where isempty(attachedNear) and isempty(attachedFar)
| project name, resourceGroup, location, sku = tostring(sku.name)
| order by name asc

Each row is an ExpressRoute gateway that no connection object references, so no circuit terminates on it. It bills provisioned uptime rather than traffic, which means an unattached gateway costs exactly what a saturated one costs: measured live in East US, $0.19 an hour for Standard (about $138.70 a month) up to $2.151 for ErGw3AZ (about $1,570.23 a month). This is the same ARM type as a VPN gateway and the two are told apart only by properties.gatewayType, so the filter is exact rather than a prefix. Run the VPN check separately; it is a different rule with a different price list, because ExpressRoute gateway meters are not published under the VPN Gateway service at all. Resource Graph does not support join kind=leftanti, so the anti-join here is a leftouter join plus an emptiness test. Both gateway fields are checked, because a connection object names a gateway in either one and reading only the first reports the far end of every pair as unused. A row here is not automatically something to delete, and this is the finding where that matters most. An ExpressRoute gateway fronts a contracted circuit, so one with nothing attached is either left over or a migration that has not finished, and the agent cannot tell which. It also cannot be stopped: deleting it is the only way to end the charge, recreating it takes about 45 minutes, the public IP is not preserved, and the circuit itself may be a separate contract with a carrier.

Applies to microsoft.network/virtualnetworkgateways
What the agent does about it

An ExpressRoute gateway that no connection object references, billing its full hourly rate with no circuit attached to it.

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.