"Which node pools have more than two nodes?" "Which resource groups have no owner tag?" The answer is somewhere in a big JSON document, and you want just that piece, in one command, the same way every time. --query does it.
What you need to know already: 22.3 (az output formats, tsv), 7.11 (JSON: objects, lists, fields; jq filters).
--query takes a JMESPath expression and evaluates it over the JSON before printing. JMESPath is a small query language for JSON - the same job as jq (Ch 7), different syntax, built into az. It is small, and the half that people skip is the half that bites.
A reminder of two JSON words: an object is { "key": value, ... }, a list (array) is [ value, value, ... ]. A field is one key of an object.
Fields and indexes
name one field
networkProfile.networkPlugin nested field (a field inside a field)
agentPoolProfiles[0] first element of a list (counting from 0)
agentPoolProfiles[-1] last element
agentPoolProfiles[0:2] a slice: elements 0 and 1
$ az aks show -g rg-oncall-lab -n aks-sysop --query networkProfile.networkPlugin -o tsv
azure
Projections: [] and [*]
[] (flatten) or [*] (wildcard) over a list starts a projection: the rest of the expression runs once per element, and nulls are dropped. Think of it as "for each element, take ...".
$ az aks show -g rg-oncall-lab -n aks-sysop --query "agentPoolProfiles[].name"
[
"system",
"user"
]
"For each node pool profile, take its name."
List commands already return a list, so their queries start with [] or [?:
az group list --query "[].name" -o tsv
Filters: [?condition]
[?condition] keeps only the elements where the condition is true - like jq's select.
$ az group list --query "[?location=='westeurope'].name" -o tsv
"For each group whose location is westeurope, take its name."
The quoting is the whole trick. In JMESPath:
'westeurope' a string literal (raw string)
"westeurope" a FIELD called westeurope (quoted identifier)
`2` a JSON literal: the number 2
So [?location=="westeurope"] compares location with a field that does not exist, gets null, matches nothing, and prints nothing, without an error. It is the single most common --query bug. Wrap the whole query in double quotes for bash and use single quotes inside - or the other way round.
Numbers need backticks:
$ az aks nodepool list -g rg-oncall-lab --cluster-name aks-sysop --query '[?count > `2`].name' -o tsv
user
[?count > '2'] compares a number with a string, which is null in JMESPath, so it silently matches nothing. And a bare 2 without backticks is a parse error. Because of the backticks, put numeric filters in single quotes so bash does not treat them as command substitution (bash runs whatever is between backticks as a command, like $( )).
Combine conditions with && (and), || (or), ! (not):
--query "[?mode=='User' && enableAutoScaling].name"
--query "[?!(tags.env=='prod')].name"
A field on its own (enableAutoScaling) counts as true when it is true.
Multiselect: build your own rows
$ az aks show -g rg-oncall-lab -n aks-sysop --query "agentPoolProfiles[].{Name:name, Count:count, Size:vmSize, Spot:scaleSetPriority}" -o table
Name Count Size Spot
------ ----- ---------------- ----
system 2 Standard_D4ds_v5
user 3 Standard_D8ds_v5
{Key:expr, ...} (a hash multiselect) makes a new object per element with the keys you name; [a, b] (a list multiselect) makes a list, which is what you want for tsv. With --query, the table uses your keys (first letter capitalised: name becomes Name) in your order. This is the way to get nested fields into a table. (Spot is empty because neither pool uses spot VMs - Ch 23.)
Functions
length(@) how many
contains(name, 'shop') substring, or list membership
starts_with(name, 'MC_')
sort_by(@, &name) & makes an expression reference
max_by(@, &count).name
join(', ', [].name)
to_number(pretaxCost) strings to numbers
@ is the current value - in a list command, the whole result list. &name is an expression reference: you hand sort_by the expression name to evaluate on each element, instead of its value now.
$ az group list --query "[?starts_with(name, 'MC_')] | length(@)"
2
"Keep the groups whose name starts with MC_, then count them." (The MC_ groups are created by Azure for each cluster's own machines - Ch 23.)
Pipes stop projections
This is the second-most common bug:
[?starts_with(name,'rg-')].name[0] # [0] applies to EACH name: "r", "r", ...
[?starts_with(name,'rg-')].name | [0] # first of the filtered list: "rg-oncall-lab"
A projection keeps going through every later .x and [n]. | ends it and applies the right-hand side to the whole result. Whenever you want "the first one" after a filter, you want | [0].
A few real ones
# the client ID of the cluster's kubelet identity (lesson 22.9)
az aks show -g rg-oncall-lab -n aks-sysop --query identityProfile.kubeletidentity.clientId -o tsv
# roles a principal holds, anywhere (lesson 22.13)
az role assignment list --all --query "[?principalId=='<oid>'].roleDefinitionName" -o tsv
# every subnet with its address range, sorted
az network vnet subnet list -g rg-oncall-lab --vnet-name vnet-sysop \
--query "sort_by(@, &name)[].[name, addressPrefix]" -o tsv
# resource groups without an owner tag
az group list --query "[?tags.owner == null].name" -o tsv
Tags are free-form key=value labels you put on resources (env=prod, owner=team-shop); Azure itself ignores them, people and cost reports use them.
When not to use it
JMESPath cannot group or sum across items (no "sum cost by service"). That is a job for -o tsv | awk (chapter 7) or jq. Use each for what it is good at.
What you can now do
- Pull one field, a filtered list, or your own table out of any
azoutput. - Spot the two silent bugs:
"field"vs'string', and[0]vs| [0].