Detecting unused Azure resources
Tags: azure, governance, kql, cloud
Published: 2022-06-23 15:19:01

If you run a large number of Azure Subscriptions where each team is responsible for their deployment, you will find, after a while, some orphan and unused objects left on subscriptions. Users created VM   and forget to remove linked resources after the deletion. They may have created, also, an APP service plan that was left after all applications were deleted. 
This can be challenging; these resources are still billed. 
You can find 3 types of resources that can be left alone in subscriptions

- Public IP
- Disk
- App Service Plan 

How do detect them? When talking about more than one subscription the most effective tool is Azure Resource Graph. It will query Azure metadata instead of querying resources subscription per subscription.
So, let's see how to create a query for each of the resources 

**For disk**

```
resources
| where type == "microsoft.compute/disks"
``` 

But we need to have only disks not attached to a VM we will need to test the _diskState_ properties and if the value is Unattached.

``` 
resources
| where type == "microsoft.compute/disks"
| where properties.diskState == "Unattached"
```

**For Public IP **

```
resources
| where type == "microsoft.network/publicipaddresses"
```

To have standalone Public IP, IP not attached to a resource you will need to test if there is no _IPconfiguration ID_

```
resources
| where type == "microsoft.network/publicipaddresses"
| where isnull(properties.ipConfiguration.id)
```

**For the App service plan**
We need to find plans not used by Azure functions

```
resources
| where type == "microsoft.web/serverfarms"
| where properties.kind <> "functionapp"
```

To get only App service without any attached website you need to look at the _mumbeOfSites_ Properties.

```
resources
| where type == "microsoft.web/serverfarms"
| where properties.kind <> "functionapp" 
| where properties.numberOfSites == 0
``` 

But we can create a single query to have all these objects at the same time. 
For that, we need to standardize the output. We need to have the same col in the 3 queries: the ResourceID, the SubscriptionID, the Resource Group, and the name of the resource, we also need to have the SKU for public IP or App Service Plan or the disk size for the plan. 

The new queries look like 

```
resources
| where type == "microsoft.network/publicipaddresses"
| where isnull(properties.ipConfiguration.id)
| project id, subscriptionId, resourceGroup, resourceName = name, type, resourceValue = sku.name
``` 


```
resources
| where type == "microsoft.compute/disks"
| where properties.diskState == 'Unattached'
| project id, subscriptionId,resourceGroup, resourceName = name, type, resourceValue = properties.diskSizeBytes
``` 


```
resources
| where type == "microsoft.web/serverfarms"
| where properties.kind <> "functionapp" 
| where properties.numberOfSites == 0 
| project id, subscriptionId,resourceGroup, resourceName = name, resourceValue = sku.tier
```



To create a unique query, the solution is to use the UNION, it combines two tables into a single result. You can combine up to 3 queries with the union operator. 

```
resources
| where type == "microsoft.network/publicipaddresses"
| where isnull(properties.ipConfiguration.id)
| project id, subscriptionId, resourceGroup, resourceName = name, type, resourceValue = sku.name 
| union (resources
    | where type == "microsoft.compute/disks"
    | where properties.diskState == 'Unattached'
    | project id, subscriptionId,resourceGroup, resourceName = name, type, resourceValue = properties.diskSizeBytes)
|union (resources
    | where type == "microsoft.web/serverfarms"
    | where properties.kind <> "functionapp" 
    | where properties.numberOfSites == 0 
    | project id, subscriptionId,resourceGroup, resourceName = name, resourceValue = sku.tier)
```