Home / Azure

AzureCheat sheetAdvanced

az rest, az graph query and JMESPath: an Azure CLI automation cheat sheet for fleets

Patterns for scripting Azure at scale with the CLI - JMESPath that works, az rest against ARM and Microsoft Graph, pagination, Resource Graph paging, bulk actions with --ids and handling throttling.

Deepak Bhardwaj7 min read

Applies toAzure CLI 2.x (Bash and PowerShell), resource-graph extension, Azure Resource Manager and Microsoft Graph REST APIs

Scripts that work on one subscription often fail on fifty. They run slowly, get throttled, miss resources because nobody followed nextLink, or break because a JMESPath filter returned an empty list without an error. This cheat sheet collects the patterns that hold up at fleet scale.

Ground rules

Rule Why
Use az graph query for inventory, not az <service> list per subscription One call covers every subscription you can read, and the data is already indexed
Use az rest when a command group lacks a property or API version You get the raw API and can pin the api-version
Run --debug once to see the REST call any command makes Shows the URL and api-version to reuse in az rest
Pin output: -o tsv for loops, -o json plus jq for structure Table output is for people and gets truncated
Add --only-show-errors in pipelines Warnings on stderr confuse parsers and log scanners
Never rely on list order CLI output is not guaranteed to be ordered; sort explicitly
az config set core.only_show_errors=true
az config set extension.use_dynamic_install=yes_without_prompt

JMESPath that works

Need Expression
Rename and pick fields [].{name:name, rg:resourceGroup, size:hardwareProfile.vmSize}
Filter on a value [?location=='westeurope'].name
Filter on a boolean [?isDefault].id and [?!isDefault].id
Substring / prefix / suffix [?contains(name, 'prod')], [?starts_with(name, 'vm-')], [?ends_with(id, '-01')]
Combine conditions [?location=='westeurope' && tags.env=='prod']
Sort sort_by([], &name)[].name
Count length([?powerState=='VM running'])
Filter after projecting [].{n:name, sku:sku.name} | [?sku=='Standard_LRS']
Tag with special characters [].tags."cost-center"
Join to one string join(',', [].name)
Missing value instead of error Fields that don't exist return null; filter with [?tags.owner == null]

Quoting traps:

  • String literals in filters take single quotes. Double quotes inside a predicate are treated as an identifier and match nothing, without an error.
  • Number and boolean literals need backticks: [?diskSizeGb > `512`]. In Bash, escape them inside double quotes or put the whole query in single quotes. In PowerShell, double the backticks: ``512``.
  • PowerShell treats | and & inside double quotes as plain characters, which is fine. Avoid $ inside double quotes, because PowerShell expands it.
# Disks over 512 GiB that are unattached, sorted by size (Bash)
az disk list --query 'sort_by([?diskState==`Unattached` && diskSizeGb > `512`], &diskSizeGb)[].{name:name, rg:resourceGroup, gb:diskSizeGb}' -o table

az rest against ARM

# Relative resource ID: the ARM endpoint and {subscriptionId} are filled in for you
az rest --method get \
  --url "/subscriptions/{subscriptionId}/resourceGroups/rg-app/providers/Microsoft.Web/sites/app-orders?api-version=2023-12-01" \
  --query "properties.siteConfig"

# PATCH one property without touching the rest (Bash quoting)
az rest --method patch \
  --url "https://management.azure.com/subscriptions/$SUB/resourceGroups/rg-app/providers/Microsoft.Storage/storageAccounts/stapp01?api-version=2023-05-01" \
  --body '{"properties":{"allowSharedKeyAccess":false}}'

# Body from a file, query string parameters separately
az rest --method put --url "$ID?api-version=2023-05-01" --body @payload.json
az rest --url "https://management.azure.com/subscriptions/$SUB/resources" \
  --url-parameters api-version=2021-04-01 '$top=100'

Notes:

  • Content-Type: application/json is set automatically when --body is valid JSON. Add --headers for anything else.
  • The token audience is chosen from the URL host when it is a known Azure endpoint. For other hosts, pass --resource <audience>.
  • --output-file writes the raw response, which is useful for binary or very large payloads.
  • Check the API version a resource type supports with: az provider show -n Microsoft.Web --query "resourceTypes[?resourceType=='sites'].apiVersions[0:3]".

az rest against Microsoft Graph

# App registrations and the expiry dates of their client secrets (first page only, see pagination below)
az rest --url "https://graph.microsoft.com/v1.0/applications?\$select=displayName,appId,passwordCredentials" \
  --query "value[].{app:displayName, id:appId, expires:passwordCredentials[].endDateTime}"

The caller needs the right Graph permissions. Signing in with az login as a user gives the CLI's own delegated consent. For automation, use a managed identity or a workload identity with application permissions granted.

Do not assume az rest returns everything. ARM list APIs return a nextLink, and Graph returns @odata.nextLink. Loop until it is empty:

url="https://management.azure.com/subscriptions/$SUB/resources?api-version=2021-04-01"
while [ -n "$url" ]; do
  page=$(az rest --url "$url" -o json)
  echo "$page" | jq -r '.value[] | [.name, .type, .location] | @tsv'
  url=$(echo "$page" | jq -r '.nextLink // empty')
done
$url = "https://graph.microsoft.com/v1.0/servicePrincipals?`$select=displayName,appId&`$top=999"
$all = while ($url) {
  $page = az rest --url $url -o json | ConvertFrom-Json
  $page.value
  $url = $page.'@odata.nextLink'
}
$all.Count

Resource Graph at scale

az graph query -q "Resources
  | where type =~ 'microsoft.compute/virtualmachines'
  | extend size = tostring(properties.hardwareProfile.vmSize)
  | summarize count() by size, location
  | order by count_ desc" --first 1000 -o table

--first accepts 1-1000. For larger result sets, page with the skip_token the CLI returns:

token=""
while :; do
  if [ -z "$token" ]; then
    out=$(az graph query -q "Resources | project id, type, location | order by id asc" --first 1000 -o json)
  else
    out=$(az graph query -q "Resources | project id, type, location | order by id asc" --first 1000 --skip-token "$token" -o json)
  fi
  echo "$out" | jq -r '.data[] | [.id, .type, .location] | @tsv'
  token=$(echo "$out" | jq -r '.skip_token // empty')
  [ -z "$token" ] && break
done

Use order by on a stable column (such as id) when paging, so results come back in a consistent order. Scope the query with --management-groups <mg> --allow-partial-scopes to cover large hierarchies without failing on one subscription you can't read. Resource Graph data can lag by a short time after a write. For decisions made immediately after a change, read the resource directly.

Bulk actions with --ids

Most commands accept --ids with many resource IDs and run them in one invocation:

# Deallocate every running VM tagged env=dev, from one Resource Graph query
ids=$(az graph query -q "Resources
  | where type =~ 'microsoft.compute/virtualmachines' and tags.env =~ 'dev'
  | where properties.extended.instanceView.powerState.code =~ 'PowerState/running'
  | project id" --first 1000 --query "data[].id" -o tsv)
[ -n "$ids" ] && az vm deallocate --ids $ids --no-wait

Caution

Bulk commands built from a query act on whatever the query returns. Run the query on its own first, check the count and a sample, and keep --no-wait bulk deletes out of unattended jobs unless there is an approval step. An empty or wrong tag filter can select the whole fleet.

Throttling and parallelism

  • ARM throttles per principal and per subscription. Splitting work across subscriptions in parallel is usually safe; hammering one subscription is not.
  • Use bounded parallelism such as xargs -P 4, not unbounded background jobs.
  • On 429, back off. az rest returns a non-zero exit code. Retry with an increasing delay, and honour Retry-After if you call the API yourself.
  • Prefer one Resource Graph query over many show calls. Resource Graph has its own quota, which is reported in response headers, so check the current limits for heavy reporting jobs.
az account list --query "[?state=='Enabled'].id" -o tsv |
  xargs -P 4 -I{} sh -c 'az vm list --subscription {} --query "length(@)" -o tsv | sed "s/^/{} /"'

Prevent it next time

  • Keep a shared library of tested queries and pagination helpers instead of ad-hoc one-liners in every runbook.
  • Run fleet scripts as a managed identity with Reader at the management group, and grant write roles only to the jobs that need them.
  • Make every bulk script support a dry run that prints the target IDs, and make dry run the default.
  • Pin api-version in az rest calls and review them when the API versions in use are retired.