This chapter covers Azure Resource Graph (ARG), a powerful Azure service that enables efficient exploration and querying of Azure resources across subscriptions. For the AZ-104 exam, Resource Graph queries appear in approximately 5-10% of questions, often integrated with Azure Policy, cost management, and automation scenarios. Understanding how to write KQL queries, use projections, filters, and joins, and interpret the schema of the 'resources' table is critical for answering scenario-based questions. This chapter provides a deep dive into ARG's architecture, query mechanics, and practical exam-focused patterns.
Jump to a section
Imagine you are a facility manager responsible for thousands of buildings across a city. Each building has its own filing cabinet with blueprints, maintenance logs, and asset inventories. To find all buildings with a specific type of HVAC system, you would normally need to walk to each building, open its cabinet, and manually search through folders. This is like querying individual Azure resources via the Azure portal or REST API—each query returns data from a single resource or resource group. Now, suppose you have a centralized search engine that indexes every document from every building into a single, unified database. You can type a query like "find all buildings with HVAC model X and Y" and get results in seconds. This search engine updates automatically as new documents are added or changed. Azure Resource Graph (ARG) works exactly like this: it indexes Azure resource properties across all subscriptions into a single data store, enabling fast, complex queries using the Kusto Query Language (KQL). Instead of making hundreds of API calls to list resources in each subscription, you run one KQL query against the Resource Graph endpoint, which returns results from millions of resources in milliseconds. The index is refreshed continuously, so query results are eventually consistent but typically within minutes of any change.
What is Azure Resource Graph and Why Does It Exist?
Azure Resource Graph (ARG) is a service in Azure that provides a unified, scalable, and efficient way to query resource metadata across subscriptions, management groups, and tenants. It is built on top of Azure Resource Manager (ARM) and uses the same resource provider data, but it stores that data in an optimized, indexed, and queryable store separate from the control plane. The primary goal of ARG is to replace the inefficient practice of enumerating resources via ARM API calls—each of which returns only a single resource or a page of resources from one subscription—with a single query that can filter, group, and aggregate across millions of resources in seconds.
On the AZ-104 exam, you will encounter ARG in several contexts: as a tool for inventory and compliance (often combined with Azure Policy), as a data source for dashboards (via Azure Workbooks), and as a mechanism for triggering automation (e.g., via Azure Logic Apps or PowerShell). The exam expects you to know the KQL syntax for common operations, the schema of the resources table, and the differences between ARG and other query tools like the Azure CLI az resource list or the REST API.
How It Works Internally
ARG is not a live query against the ARM control plane. Instead, it maintains a separate, continuously updated index of resource properties. This index is stored in a distributed, replicated data store that supports Kusto Query Language (KQL)—the same query language used by Azure Data Explorer and Azure Monitor Logs.
When you submit a query to ARG, the following occurs:
Authentication and Authorization: The request is authenticated via Azure AD and authorized using Azure RBAC. The service evaluates your effective permissions at the management group, subscription, or resource group level. ARG only returns resources you have read access to.
Query Parsing and Optimization: The KQL query is parsed, optimized, and planned. The optimizer can push down filters (where clauses) and projections (project clauses) to the storage layer, reducing the amount of data scanned.
Index Lookup: The query is executed against the indexed store. The store is organized by resource types and properties, so queries that filter on type, resourceGroup, subscriptionId, or tags are extremely fast because these fields are indexed.
Result Aggregation and Paging: Results are returned in pages (default 1000, max 5000 per page). If the query includes aggregation (summarize), the aggregation is performed server-side. You can use the $skip and $top parameters or the limit operator in KQL to control paging.
Consistency: ARG is eventually consistent. Changes to resources (create, update, delete) are reflected in ARG within minutes (typically 5-10 minutes). For critical scenarios where up-to-the-second accuracy is required, you should use the ARM API directly.
Key Components, Values, Defaults, and Timers
Tables: The primary table for resource metadata is resources. Other tables include resourcecontainers (for subscriptions, resource groups, and management groups) and policyresources (for Azure Policy compliance state).
Schema of `resources` table: Key columns include id (resource ID), name, type (e.g., microsoft.compute/virtualmachines), location, resourceGroup, subscriptionId, tenantId, tags (dynamic), properties (dynamic), sku, plan, zones, extendedLocation, identity, kind, managedBy, and createdTime/changedTime.
Query limits: Maximum query timeout is 30 seconds. Maximum result size per page is 5000 rows. Maximum query length is 4096 characters (for URL-based queries; SDKs can send larger via POST).
Rate limits: 1000 requests per hour per user per tenant (soft limit, can be increased).
KQL operators: Common operators include where, project, extend, summarize, join, mv-expand, order by, limit, and distinct.
Projections: Use project to select specific columns and reduce result size. Use project-away to exclude columns.
Filtering on dynamic properties: Use bracket notation, e.g., where properties['provisioningState'] == 'Succeeded'. For nested properties: where properties.storageProfile.osDisk.osType == 'Windows'.
Working with tags: Tags are stored as a dynamic object. To filter by tag: where tags['Environment'] == 'Production'. To list all tags: project tags.
Joins: You can join the resources table with itself or with resourcecontainers. For example, to get subscription name alongside resources: resources | join kind=leftouter (resourcecontainers | where type == 'microsoft.resources/subscriptions' | project subscriptionName=name, subscriptionId) on subscriptionId.
Time filters: Use where createdTime > ago(7d) or where changedTime > datetime(2023-01-01).
Azure Policy integration: Use policyresources table to query policy states. Common columns: policyAssignmentId, policyDefinitionId, complianceState, resourceId. Example: policyresources | where complianceState == 'NonCompliant'.
Configuration and Verification Commands
ARG is a read-only service; there is no configuration to enable it—it is always on for all Azure subscriptions. However, you can control access via RBAC. To query ARG, you need at least Reader permission on the resources you want to query.
Using Azure CLI:
az graph query -q "resources | where type =~ 'Microsoft.Compute/virtualMachines' | project name, location, resourceGroup | limit 10"Using PowerShell:
Search-AzGraph -Query "resources | where type =~ 'Microsoft.Compute/virtualMachines' | project name, location, resourceGroup | limit 10"Using REST API:
POST https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01
Content-Type: application/json
Authorization: Bearer <token>
{
"query": "resources | where type =~ 'Microsoft.Compute/virtualMachines' | project name, location, resourceGroup | limit 10",
"subscriptions": ["sub1", "sub2"]
}Using SDK (Python):
from azure.mgmt.resourcegraph import ResourceGraphClient
from azure.mgmt.resourcegraph.models import QueryRequest
from azure.identity import DefaultAzureCredential
client = ResourceGraphClient(DefaultAzureCredential())
query = QueryRequest(query="resources | limit 5", subscriptions=["sub-id"])
result = client.resources(query)Testing queries in Azure portal: Navigate to Azure Resource Graph Explorer (https://portal.azure.com/#blade/HubsExtension/ArgQueryBlade). This provides a UI to write, run, and save queries, and export results to CSV or JSON.
How It Interacts with Related Technologies
Azure Policy: ARG is the underlying engine for Azure Policy's compliance state. When you run a policy evaluation, the policy engine uses ARG to get the current resource state. You can also query policyresources table to see compliance details.
Azure Workbooks: Workbooks can use ARG as a data source. You can write KQL queries to build interactive dashboards that show resource counts, cost distribution, or compliance status.
Azure Logic Apps / Power Automate: You can trigger workflows based on ARG query results. For example, run a scheduled Logic App that queries for resources missing a specific tag and then applies the tag via ARM.
Azure CLI / PowerShell: These tools use ARG under the hood when you use az graph query or Search-AzGraph. The az resource list command still uses ARM API directly.
Azure Monitor Logs: While both use KQL, ARG queries resource metadata (properties, tags, types), whereas Azure Monitor Logs queries operational logs (events, performance counters). The schema and tables are different.
Advanced Query Patterns
Find orphaned disks:
resources
| where type =~ 'Microsoft.Compute/disks'
| where managedBy == ''
| project name, resourceGroup, diskSizeGB, sku.nameCount resources by type across subscriptions:
resources
| summarize count() by type, subscriptionId
| order by count_ descGet all VMs with public IPs:
resources
| where type =~ 'Microsoft.Network/publicIPAddresses'
| join kind=inner (
resources
| where type =~ 'Microsoft.Network/networkInterfaces'
| mv-expand ipconfigurations = properties.ipConfigurations
| project nicId = id, publicIpId = tostring(ipconfigurations.properties.publicIPAddress.id)
) on $left.id == $right.publicIpId
| project name, ipAddress = properties.ipAddressFind resources with no tags:
resources
| where tags == '' or isempty(tags)
| project name, type, resourceGroupGet resources changed in last 24 hours:
resources
| where changedTime > ago(24h)
| project name, type, changedTime, changeTypeOpen Azure Resource Graph Explorer
Navigate to the Azure portal and open the Resource Graph Explorer tool. You can find it by searching 'Resource Graph Explorer' in the top search bar, or go to All Services > Resource Graph Explorer. This tool provides a web-based interface to write and execute KQL queries against the ARG data store. The query editor supports syntax highlighting, auto-complete, and a results pane. You can also save queries for later use. The explorer uses the same underlying REST API as the CLI and SDKs.
Write a basic query to list resources
Start with a simple query: `resources | limit 10`. This returns the first 10 resources from the `resources` table. The `resources` table contains one row per Azure resource. The default columns include `id`, `name`, `type`, `location`, `resourceGroup`, `subscriptionId`, `tags`, and `properties`. The `limit` operator restricts the number of rows returned. Without a filter, the query may take longer if you have many resources, but the index helps. Always use `where` filters to narrow down the scope for better performance.
Apply filters using 'where' clause
Use the `where` operator to filter resources by type, location, subscription, or tags. For example: `resources | where type =~ 'Microsoft.Compute/virtualMachines' and location == 'eastus'`. The `=~` operator is case-insensitive. For dynamic properties, use bracket notation: `where properties['provisioningState'] == 'Running'`. Filters on indexed columns (type, subscriptionId, resourceGroup) are very fast. The query engine pushes these filters to the storage layer.
Project specific columns
Use the `project` operator to select only the columns you need. This reduces the result size and improves performance. Example: `resources | where type =~ 'Microsoft.Compute/virtualMachines' | project name, location, resourceGroup, properties.storageProfile.osDisk.osType`. You can also rename columns: `project VMName=name, OS=properties.storageProfile.osDisk.osType`. Use `project-away` to exclude columns instead.
Use aggregation and grouping
The `summarize` operator allows you to count, sum, or average values across groups. For example, to count VMs by OS type: `resources | where type =~ 'Microsoft.Compute/virtualMachines' | summarize VMCount=count() by OS=properties.storageProfile.osDisk.osType`. You can also use `count()` with `by` for grouping. For complex aggregations, use `make_list` or `make_set` to create arrays. Aggregation happens server-side.
Join tables for cross-referencing
You can join the `resources` table with itself or with `resourcecontainers`. For example, to list resources with their subscription name: `resources | join kind=leftouter (resourcecontainers | where type == 'microsoft.resources/subscriptions' | project subscriptionName=name, subscriptionId) on subscriptionId | project name, type, subscriptionName`. The `join` operator supports different kinds: `inner`, `leftouter`, `rightouter`, `fullouter`. Use `kind=leftouter` to preserve all rows from the left table.
Enterprise Scenario 1: Cloud Inventory and Compliance Reporting
A large enterprise with 500+ subscriptions and 50,000 resources needs to generate a weekly report of all resources that are missing mandatory tags (e.g., 'CostCenter', 'Environment'). Previously, the team used a PowerShell script that looped through each subscription and called Get-AzResource, which took over 6 hours to complete. They switched to Azure Resource Graph. The query resources | where tags['CostCenter'] == '' or isempty(tags['CostCenter']) | project name, type, subscriptionId, resourceGroup runs in under 10 seconds across all subscriptions. The results are exported to CSV and emailed via a Logic App scheduled trigger. The team also set up an Azure Workbook that uses ARG queries to display real-time compliance dashboards. The main challenge was ensuring the RBAC permissions were correct—the service principal running the query needed Reader role on all subscriptions, which was managed via Azure Lighthouse or a management group scope.
Enterprise Scenario 2: Automated Resource Cleanup
A DevOps team manages a non-production environment with 20 subscriptions. Developers often create and forget to delete resources, leading to cost overruns. The team uses ARG to identify 'orphaned' resources—disks not attached to VMs, NICs not attached to VMs, and public IPs not associated with any resource. A daily Azure Automation runbook runs ARG queries like resources | where type =~ 'Microsoft.Compute/disks' | where managedBy == '' | project name, resourceGroup, subscriptionId. The runbook then deletes these resources after sending a notification. The challenge was handling the eventual consistency of ARG: a disk that was just attached might still appear as orphaned for up to 5 minutes. To mitigate, the runbook adds a delay or double-checks via ARM API before deletion. Performance is not an issue because the query returns in milliseconds.
Enterprise Scenario 3: Security and Compliance Auditing
A financial services company must ensure that no virtual machines are exposed to the internet with open management ports (e.g., RDP on 3389, SSH on 22). They use ARG to query all Network Security Groups (NSGs) and their rules. The query joins resources for NSGs with their subnets or NICs, then filters for rules allowing inbound traffic on those ports from 'Any' or 'Internet'. The query uses mv-expand to flatten the security rules array. The results are fed into a security dashboard and trigger alerts via Azure Sentinel. The biggest challenge was the complexity of the KQL query—nested properties and arrays require careful use of mv-expand. The team also had to account for NSGs applied at both subnet and NIC levels, which required a union of two queries.
What AZ-104 Tests on Azure Resource Graph
The AZ-104 exam objective '3.4 Manage compute resources' includes Azure Resource Graph queries as a tool for inventory and compliance. Specifically, you should be able to:
Write KQL queries to retrieve resource metadata (type, location, tags, properties).
Filter resources using where clauses on indexed columns (type, subscriptionId, resourceGroup).
Use project to shape output.
Use summarize for counting/grouping.
Join the resources table with resourcecontainers to get subscription names.
Query Azure Policy compliance state using policyresources table.
Identify orphaned resources (disks, NICs, public IPs).
Most Common Wrong Answers and Why Candidates Choose Them
Choosing Azure CLI `az resource list` over ARG: Many candidates assume az resource list is faster because it's simpler. However, az resource list makes individual API calls per subscription and is slow for large environments. The exam expects you to know that ARG is the correct tool for cross-subscription queries.
Using `Get-AzResource` in PowerShell: Similar to above, candidates familiar with PowerShell might default to Get-AzResource. The exam wants you to recognize that Search-AzGraph (or the Az.ResourceGraph module) is the appropriate cmdlet for ARG queries.
Misunderstanding KQL syntax: Common errors include using == instead of =~ for case-insensitive string comparison, forgetting to use tostring() for dynamic properties, or using where after summarize (should use having). The exam may present a query with a deliberate syntax error and ask you to identify the fix.
Confusing ARG with Azure Monitor Logs: Some candidates think ARG queries can access log data like performance counters. ARG only queries resource metadata, not operational logs. The exam may have a scenario asking for a tool to query VM performance data—that would be Azure Monitor Logs, not ARG.
Specific Numbers, Values, and Terms That Appear on the Exam
Default page size: 1000 rows, max 5000 rows per page.
Query timeout: 30 seconds.
Rate limit: 1000 requests per hour per user per tenant.
Tables: resources, resourcecontainers, policyresources.
KQL operators: where, project, summarize, join, mv-expand, limit, distinct, order by.
Case-insensitive comparison: =~.
Dynamic property access: properties['key'] or properties.key.
Tag filtering: tags['TagName'].
Time filters: ago(7d), datetime(2023-01-01).
Edge Cases and Exceptions the Exam Loves to Test
Empty tags: where tags == '' or isempty(tags).
Null properties: Use isnotnull(properties).
Arrays in properties: Use mv-expand to flatten, e.g., mv-expand properties.ipConfigurations.
Case sensitivity: Type names are case-insensitive in ARG, but property names may be case-sensitive. Use =~ for type.
Management group scope: You can query across all subscriptions in a management group by specifying the management group ID in the query request (in CLI: --management-groups).
How to Eliminate Wrong Answers Using the Underlying Mechanism
If a question asks for the fastest way to get a list of all VMs across 100 subscriptions with a specific tag, think: ARG uses an indexed store and can answer in seconds. Any option that suggests looping through subscriptions (e.g., using az vm list in a script) is slower and should be eliminated. If the question involves querying past log data (e.g., CPU usage), ARG cannot do that—the answer must involve Azure Monitor Logs. If the question involves policy compliance, the policyresources table is the correct source. By understanding what ARG stores (current resource metadata, not historical logs), you can quickly eliminate wrong answers.
Azure Resource Graph uses Kusto Query Language (KQL) to query resource metadata across subscriptions.
The primary table is 'resources'; other tables include 'resourcecontainers' and 'policyresources'.
Default page size is 1000 rows, max 5000 rows per page; query timeout is 30 seconds.
Use 'where type =~' for case-insensitive filtering on resource type.
Access dynamic properties using bracket notation: properties['key'].
Tags are stored as a dynamic object; filter with tags['TagName'].
Use 'summarize count() by' for grouping and counting.
Join 'resources' with 'resourcecontainers' to get subscription names.
Query policy compliance using 'policyresources' table.
ARG is eventually consistent; for up-to-date data, use ARM API.
These come up on the exam all the time. Here's how to tell them apart.
Azure Resource Graph (ARG)
Queries across multiple subscriptions in a single call
Uses KQL for complex filtering, grouping, and joining
Returns results in milliseconds for large environments
Eventually consistent (up to 5 min delay)
Supports server-side aggregation (summarize)
Azure CLI az resource list
Queries one subscription per call; requires scripting for multiple
Simple filtering with --query parameter (JMESPath)
Slower for large environments due to per-subscription API calls
Returns live data from ARM
No server-side aggregation; must process results client-side
Mistake
Azure Resource Graph returns live, up-to-the-second data.
Correct
ARG is eventually consistent. Changes to resources may take up to 5-10 minutes to appear. For real-time data, use ARM REST API directly.
Mistake
ARG can query any data in Azure, including logs and metrics.
Correct
ARG only queries resource metadata (properties, tags, types). It does not contain operational logs, metrics, or activity logs. Those are in Azure Monitor Logs and Azure Activity Log.
Mistake
You need special permissions beyond Reader to use ARG.
Correct
ARG respects Azure RBAC. You need at least Reader permission on the resources you query. No additional ARG-specific role is required.
Mistake
ARG queries are limited to a single subscription.
Correct
ARG can query across multiple subscriptions, management groups, and even tenants (with appropriate permissions). You specify the list of subscriptions or management groups in the query request.
Mistake
The 'resources' table contains all properties of a resource, including nested JSON structures.
Correct
The 'properties' column is a dynamic JSON object that contains the same properties as the ARM resource object. However, not all properties are indexed; some may be missing if they are not exposed by the resource provider. You can access nested properties using dot notation or bracket notation.
Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.
Azure Resource Manager (ARM) is the control plane for managing Azure resources—it handles create, read, update, and delete operations. Azure Resource Graph (ARG) is a read-only service that provides an indexed, queryable view of resource metadata. ARM is live and transactional; ARG is eventually consistent but much faster for cross-subscription queries. Use ARM for single-resource operations, ARG for inventory and compliance.
Use the 'policyresources' table. Example query: 'policyresources | where complianceState == 'NonCompliant' | project resourceId, policyAssignmentId, policyDefinitionId'. You can join with 'resources' to get more details. Note that the 'policyresources' table is populated only after policy evaluation.
Yes, but you need to authenticate with a user or service principal that has access to each tenant. In the query request, you can specify an array of subscriptions from different tenants. The REST API and SDKs support cross-tenant queries. In the portal, you can switch directories.
There is no hard limit, but the query timeout is 30 seconds. For very large numbers of subscriptions (e.g., 1000+), the query may time out. It's recommended to use management groups to aggregate subscriptions. The maximum number of subscriptions per query is not documented but practically limited by performance.
Use the 'mv-expand' operator to flatten arrays. For example, to expand IP configurations on a NIC: 'resources | where type =~ 'Microsoft.Network/networkInterfaces' | mv-expand ipconfigurations = properties.ipConfigurations | project ipconfigurations.properties.privateIPAddress'. You can then filter or project individual elements.
Yes, in the Azure portal's Resource Graph Explorer, you can save queries to your favorites or share them via a link. You can also export the query as a JSON file and import it later. Saved queries are stored per user.
Yes, if the resource type exposes a provisioning state or power state in its properties. For VMs, you can query: 'resources | where type =~ 'Microsoft.Compute/virtualMachines' | where properties.extended.instanceView.powerState.code != 'PowerState/running''. Note that power state may not be available for all resources and may require additional permissions.
You've just covered Azure Resource Graph Queries — now see how well it sticks with free AZ-104 practice questions. Full explanations included, no account needed.
Done with this chapter?