For me, one of the most powerful features in the field of Azure Governance is the Azure Resource Graph. But, it can take some practice to master it. But once you do, you'll be able to write queries to evaluate your policies, use the Change History functionality and query across resources, tenants and subscriptions (Resource Graph is supported by Azure Lighthouse and Delegated Resource Management). Fairly easy and really fast.
The documentation around the Azure Resource Graph is structured nicely but for me it still required some trial and error to get everything working and understand what was happening. I'm seeing a lot of people who are struggling to go from "basic examples" to full scale. And that's understandable. Even though the more advanced examples illustrate perfectly what you can achieve using the Resource Graph, in practice (and when you're learning) you're probably starting with a very small basic query and try to build on top of that. This blog post is not intended to go over all functions that the Azure Resource Graph queries provide (such as counting, summarizing, etc). Instead, it is focused on helping you get started building simple queries and going from there (you need results before you can count them).
What I want to illustrate is how you can go from querying a single resource to evaluating whether a specific property is set on a resource type, across tenants. For me this is a really big use case when using the Azure Resource Graph.
Patience you must have, my young padawan.
What you'll learn is that it takes some patience to get used to querying the Resource Graph. Queries can become complicated but if you structure them well, you will get past the learning curve pretty quickly. Once in a while you will stare at your complicated query and figure it's probably best to start over again. I've had this happen many times and this was often the result of creative copy/paste actions from the documentation. But if you truly want to master querying the Azure Resource Graph, it's all about the basics.
One could argue that a lot of the information in the examples could be retrieved by point and clicking through the Azure Portal. And that is true. But when queries become more complex you will learn that the capabilities of Azure Resource Graph go far beyond what the Azure Portal can provide.
Building queries by example
The Resource Graph Query Language is based on the Kusto query language which is documented really well. The difference between the two is that the Resource Graph Query Language supports a subset of operators and functions (read here). Now reading up on the language before getting started helps but, I prefer to read up on the "manual" as I go along and learn from my mistakes (this is where patience comes in).
Running your first query requires you to install the Resource Graph module for AzureCLI or Azure PowerShell. But, if you want to get started quickly. Running the Azure Resource Graph Explorer from the Azure Portal will do just fine. Alternatively you can use the Azure SDK.
Let's start with some basic queries ran from the command line. I'm using the PowerShell module here but the query will be identical when using the Azure CLI. We'll look into the Resource Graph Explorer in a bit, as this will greatly help you in exploring properties, structuring queries and to understand where your results are coming from.
One of the first examples as provided by Microsoft consists of requesting a list of your resources. This is done through adding a "project" statement to your query. This does exactly as one would expect "project whatever you are requesting". Of course these properties must be available and supported by the Resource Providers but it's safe to assume that every resource has a name, type and location. Keep in mind that properties you are projecting are case sensitive.
Search-AzGraph -Query "project name, type, location | order by name asc"
The result would be similar to the following:
Pretty cool but let's look at specifying the type of resource so we can filter out the stuff we don't need. I'm going with App Services for this. We can use the "where" statement for this and look for a specific resource type using "type" property.
Search-AzGraph -Query "project name, type, location | where type =~ 'Microsoft.Web/Sites'"
We will receive a similar response as before, just scoped to App Services.
From the naming of the resource we can tell that some resources are Web Apps and some are Azure Functions. To provide a clearer view, I would like to add what kind of App Service we're talking about to the result. And maybe filter on something more specific, like only returning just the Azure Functions. How would one go about building that query?
Let's first request a single resource from the previous example by leaving out the "project" statement and limiting the result to "1". I'm converting the output to Json for readability (you read that right) as it will expand the properties.
Search-AzGraph -Query "where type =~ 'Microsoft.Web/Sites' | limit 1" |ConvertTo-Json
This will return a Json output of the properties our App Service resource has to offer. You can scroll through the results and take a note of any properties you want to filter on. For now we'll go with what we need to filter out: the Function Apps. As you can a property "kind" with the value "functionapp" is returned. Pretty sure that's what we need.
On a side note: We're going beyond the basic properties such as Resource Name, types, Ids, etc. Azure Resource Manager also supports the more detailed properties we're going after right now. But, not all Resource Providers already support this.
To quote Microsoft Docs:
"With Azure Resource Graph, you can access these properties the resource providers return without needing to make individual calls to each resource provider. For a list of supported resource types, look for a Yes in the Resources for complete mode deployments table."
Back to the example. Let's add the "kind" property to our query.
Search-AzGraph -Query "project name, type, location, kind | where type =~ 'Microsoft.Web/Sites'"
This will add the "kind" as a fourth field to our output.
Because we're looking for just Azure Functions, let's filter on just function apps to achieve our goal. We can achieve this by adding and "and" operator to the query like so:
Search-AzGraph -Query "project name, type, location, kind | where type =~ 'Microsoft.Web/Sites' and kind =~ 'functionapp'"
This will return the output we need. It's still pretty basic, but we're getting there and options such as "maybe we can do this across tenants" and "maybe we can filter on specific properties" come to mind.
Before we move on, let's look at case sensitivity for a minute. As we've been using the "where" operator for pretty much every filtering operation. It's good to understand when and why case sensitivity becomes important. We have been using the "string operator =~" up until now. This indicates we are looking for something that is "equal" on both sides. But, not case sensitive. This means "Hello World =~ "hello world" would be "true".
This is important to understand when building your Resource Graph queries as choosing the wrong string operator will return different results. The Resource Graph by default returns resource types as a lower case but if we're requesting aliases (keep reading) the result contains upper case characters as well. A pretty good reason to go with "=~". Read up on string operators here and familiarize yourself with the capabilities.
Try running the following example to see the effects of using "=~" and "=="
# Correct use of lower case when defining the resource type and matching case sensitive with "==" Search-AzGraph -Query "project name, location, type| where type == 'microsoft.web/sites' | limit 1" # Correct use of lower and upper case when defining the resource type and matching case insensitive with "=~" Search-AzGraph -Query "project name, location, type| where type =~ 'mIcRoSoFt.WeB/sItEs' | limit 1" # Incorrect use of lower and upper case when defining the resource type and matching case sensitive with "==" Search-AzGraph -Query "project name, location, type| where type == 'mIcRoSoFt.WeB/sItEs' | limit 1"
Going cross-tenant (Azure Lighthouse)
Azure Resource Graph supports cross-tenant management. And this is where things get interesting. Running queries across tenants makes this really powerful. If you want to know something about the configuration of resources in one environment (let's go with the httpsOnly example), it's likely that you want to know this for the same type of resources in all your tenants.
By default if subscriptions are delegated to you, your query can run across tenants. Since Azure Lighthouse was released a "tenantId" property was added to resources to determine what tenant the resource resides in. To visualize this, all you need to do is add the "tenantId" property to your "project" statement and run the query again.
Search-AzGraph -Query "project name, type, location, kind, tenantId, subscriptionId | where type =~ 'Microsoft.Web/Sites' and kind =~ 'functionapp'" |ft
As mentioned before, please note that the properties requested through "project" are case sensitive. Therefor "tenantid" would return an error, whereas "tenantId" would return a result. To create a complete view, I've added the subscriptionId as well.
Now that we have the basic query done, let's go for something useful, like checking whether the App is configured to only allow HTTPS traffic. If it's not, we want to know about it. But why would you do this using the Azure Resource Graph when we already have Azure Policy that can basicly audit the same.
Deploying an Azure Policy would indeed help you achieve the same result. But, when managing multiple tenants and subscriptions you would have to apply the Azure Policy to each subscription as Management Groups and Azure Policy both don't support applying a Policy over multiple subscriptions in different tenants. We're also going for speed here. Where it may take some time for an Azure Policy to return a result, Azure Resource Graph works instantly.
From the previous example where we requested all the properties, we can see that there is a field called "httpsOnly" nested under "properties".
We can request this field by adjusting the query and projecting "properties.httpsOnly".
Search-AzGraph -Query "project name, type, location, kind, tenantId, subscriptionId, properties.httpsOnly | where type =~ 'Microsoft.Web/Sites' and kind =~ 'functionapp'" |ft
Which adds an additional field to the result indicating whether the property "httpsOnly" is set to true or false.
What we need is to only return the results when a Function App is not configured to only allow HTTPS traffic. As we've identified the property we can now add a filter for that as well.
Search-AzGraph -Query "project name, type, location, kind, tenantId, subscriptionId, properties.httpsOnly | where type =~ 'Microsoft.Web/Sites' and kind =~ 'functionapp' and properties_httpsOnly == 'false'" |ft
What we've ended up with is a result of all the Function Apps across tenants and subscriptions that do not have the HTTPS only property configured. Something you probably want to know when you're responsible for managing multiple environments.
You're next step would be to decide whether you want to configure an Azure Policy to continuously evaluate the property or help you remediate non-compliance.
Going from Resource Graph to Azure Policy - Using Aliases
To go from Azure Resource Graph to Azure Policy you would need to transform / translate the property you had requested in the previous examples into an "alias".
Within Azure Policy, "Property Aliases" are used to access specific properties for a resource type (https://docs.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure).
Now if you goal is to author an Azure Policy from the start, you might want to consider starting with building your query based on aliases.
To request the aliases available for the resource type you're looking into you can project them just as you would project properties of a resource.
Search-AzGraph -Query "where type=~'microsoft.web/sites' | limit 1 | project aliases" |convertTo-Json
projecting "aliases" will return an extensive list of the supported aliases that you can add to your query.
As you can see, there is an alias for the httpsOnly property which we can use. You can easily adjust your query to include this by extending your query and including the alias. Now this sounds complicated by basicly what you're doing is saying
"Hey query, I would like to add a new column called "httpsOnly" of alias "Microsoft.Web/sites/httpsOnly and it's contents to the input of my query".
Once you have done that, the query structure changes. If you want to evaluate "httpsOnly" you first need to "extend" it. Therefor, "Project" and any evaluation has to come after the extension. The query would turn into something similar to:
Search-AzGraph -Query "extend httpsOnly = aliases['Microsoft.Web/sites/httpsOnly'] | where type=~'Microsoft.Web/Sites' and kind == 'functionapp' and httpsOnly == 'false'| project name, type, location, kind, tenantId, subscriptionId, httpsOnly" |ft
Now, why is this useful? If you get results here using the alias, it means you can build an Azure Policy for it. The Aliases you just used can be added directly to the "field" property of the Azure Policy you are building (https://docs.microsoft.com/en-us/azure/governance/policy/tutorials/create-custom-policy-definition).
Using the Azure Resource Graph Explorer (Azure Portal)
If all that command-line ninja stuff is not your thing, or you do want to count, summarize and build some nice charts to support your findings, then the Resource Graph Explorer within the Azure Portal is going to be your thing. You can find it by going to "All Services" and look for "Resource Graph Explorer" or just search for it using the search bar.
The beauty here is that we can input the exact same query and get the same result. Yes, you do need to strip off the PowerShell or Azure CLI specifics but that's it. Basically, anything between the double quotes will work. So we'll go with the following:
extend httpsOnly = aliases['Microsoft.Web/sites/httpsOnly'] | where type=~'Microsoft.Web/Sites' and kind == 'functionapp' and httpsOnly == 'false' | project name, type, location, kind, tenantId, subscriptionId, httpsOnly
Note that if you want to run your queries across tenants you do need to adjust your scope in the subscription filter to include all the tenants and subscriptions.
If you're struggling with the command line interface and the data it returns, you can always go back to the Resource Graph Explorer (or start there). As it also provides you with the ability to browse through the properties of a specific Resource Provider and add them to your query. Mind you, they will be added as a "where" statement but you can easily adjust that to whatever you need.
Now does that make you (or me) a master in querying the Resource Graph? Maybe not but if you understand these basic principles and you practice often (how about going a week without using the Azure Portal) you will become way more efficient in querying large amounts of resources, across tenants and when building Azure Policies. Additionally, your understanding of how Azure works under the hood will greatly improve and that's never a bad thing.