Your company runs its platform on Azure, Microsoft's cloud: a huge set of data centres where you rent machines, networks, databases and clusters by the hour instead of buying hardware. Someone asks "which Kubernetes clusters do we run in prod, and who can change them?" Clicking through a web page for that takes an hour and proves nothing. One command answers it in a second. This chapter teaches that command line, and the identity and permission model behind every "Forbidden" you will ever get from Azure.
What you need to know already: 1.1 (a shell and its prompt), 7.11 (JSON and jq), 12.1 (Terraform describes infrastructure as code; its azurerm provider talks to Azure), 15.1 (a Kubernetes cluster runs pods on nodes).
Words first
- Cloud - computers, disks and networks in someone else's data centre that you create and delete through an API (a way for programs to ask a service to do something, here over HTTPS). You pay for what exists, by the hour or GB.
- Resource - one thing you created in Azure: a virtual network, a Kubernetes cluster, a Key Vault, a storage account. Everything below is about resources.
- Portal - Azure's website (portal.azure.com). Buttons and forms over the same API.
- Azure CLI - the command-line tool, called
az. Everyazcommand sends HTTPS requests to Azure and prints the answer. - Region - a group of data centres in one geography, like
westeurope(Netherlands) ornortheurope(Ireland). Every resource lives in a region.
The rule for this block
Nothing from the portal. Everything in Terraform. The portal is for verification only.
Why? A click in the portal leaves no record of why and cannot be repeated exactly. Terraform (Ch 12-14) keeps the whole setup in git, reviewed and repeatable.
So why learn az at all? Because the CLI is how you look at Azure quickly and precisely, how you script the checks a pipeline runs, and how you do break-glass work (an emergency fix by hand, when the normal path is broken) at 3am. Every resource you create with az in these missions has a Terraform twin (azurerm_*), and chapter 12's engine already describes the same subscription with the same IDs.
The hierarchy
Azure organises everything in nested boxes:
Entra ID tenant 11111111-2222-3333-4444-555555555555 (identities live here)
└─ management group optional, for policy and RBAC across subscriptions
└─ subscription 00000000-1111-2222-3333-444444444444 (billing + RBAC boundary)
└─ resource group rg-oncall-lab (lifecycle container)
└─ resource vnet-sysop, aks-sysop, kv-shop-prod ...
The long numbers are GUIDs (globally unique IDs): 32 hex digits in five groups. Azure gives every tenant, subscription and identity one.
- The tenant is your organisation's directory in Entra ID (Microsoft's identity service, formerly Azure Active Directory): users, groups, apps. It says who exists. It is not a place resources live.
- A management group is an optional folder above subscriptions, so one rule can apply to many of them.
- A subscription is a billing account plus a container for resources. It trusts exactly one tenant for its logins. Dev and prod are usually separate subscriptions, because a subscription is the blast radius (how far a mistake can spread) you can actually enforce.
- A resource group is a folder for resources that live and die together: delete it and everything in it goes. Resources in one group can be in different regions.
RBAC (role-based access control) is Azure's permission system: who gets which role on which box. It is lesson 22.13; for now, just know that permissions are granted on one of these boxes and flow down.
Resource IDs - learn to read them
Every resource has one ID, a path through the boxes:
/subscriptions/00000000-1111-2222-3333-444444444444
/resourceGroups/rg-shop-prod
/providers/Microsoft.KeyVault
/vaults/kv-shop-prod
Read it as: subscription, resource group, then the provider (the Azure service that owns this kind of resource, here Microsoft.KeyVault), the resource type (vaults) and the name.
/subscriptions/<id>/resourceGroups/<rg>/providers/<Namespace>/<type>/<name>, and child resources keep going: .../virtualNetworks/vnet-sysop/subnets/snet-aks. IDs are case-insensitive - you will see resourceGroups and resourcegroups in different outputs for the same thing, and Key Vault errors lowercase the whole path. So never compare them with a case-sensitive grep; use grep -i.
Control plane and data plane
az talks to Azure Resource Manager (ARM, at https://management.azure.com) for almost everything: create a vault, list subnets, scale a node pool. ARM is the front door for managing resources. That is the control plane.
Some services also have a data plane: their own address where you use the contents of the resource, with its own permission check:
control plane management.azure.com create / configure the Key Vault
data plane kv-shop-prod.vault.azure.net read and write the secrets in it
data plane stsysoplab.blob.core.windows.net read and write files (blobs)
data plane <cluster>.hcp.westeurope.azmk8s.io the Kubernetes API itself
An analogy: the control plane is the building's management office (build a safe, move it, give out keys); the data plane is opening the safe. Being the building's owner does not mean you know the combination. This split is the root of half the permission surprises in this chapter (lesson 22.23).
Signing in
Before az can do anything it needs a token: a short-lived signed string from Entra ID that says "this is learner, valid for the next hour". az login gets one.
az login # opens a browser; on a server it cannot
az login --use-device-code # prints a code, you enter it on another device
az login --service-principal -u <appId> -p <secret> --tenant <tenant>
az login --identity # only on Azure machines with a managed identity
--use-device-code- the device code flow: the CLI prints a URL and a code; you open the URL on your laptop's browser, type the code, and the CLI on the server receives the token.--service-principal- log in as an app instead of a person (lesson 22.8);-uits app ID,-pits secret,--tenantwhich directory.--identity- log in as the machine's own Azure identity (lesson 22.9).
On a headless box (no screen, no browser) like oncall-lab, the device code flow is the one you use:
# the next mission installs azure-cli and signs you in
az login --use-device-code
To sign in, use a web browser to open the page https://microsoft.com/devicelogin
and enter the code F7K2QXP9R to authenticate.
Recent CLI versions then show a tenant and subscription picker instead of dumping every subscription as JSON. Enter keeps the default.
Pipelines should not log in with a password at all. They use federated credentials (a trust rule: "tokens from this CI system for this repo count as this identity"), so no secret is stored anywhere. Lesson 22.21 builds one.
Where the credentials go
~/.azure/azureProfile.json the subscriptions you can see
~/.azure/msal_token_cache.json access AND refresh tokens
~/.azure/config az config settings
An access token works for about an hour. A refresh token is used to get new access tokens without logging in again, for days. So the token cache is a credential: anyone who can read it is you until the refresh token expires. Never bake ~/.azure into a container image or copy it to a shared box.
Subscriptions
az account commands are about where you are working:
az account show # where am I?
az account list -o table # what can I see?
az account set --subscription sub-oncall-lab # switch (name or id)
az group list --subscription sub-platform-prod # one command, other sub
show- the current subscription and tenant.list- every subscription you have any role in.-o tableprints it as a table instead of JSON (lesson 22.3 covers-o).set --subscription- make another one current, by name or GUID.--subscriptionon any command - run just that command against another one.
# once signed in
az account list -o table
Name CloudName SubscriptionId TenantId State IsDefault
----------------- ---------- ------------------------------------ ------------------------------------ ------- ---------
sub-oncall-lab AzureCloud 00000000-1111-2222-3333-444444444444 11111111-2222-3333-4444-555555555555 Enabled True
sub-platform-prod AzureCloud 00000000-1111-2222-3333-555555555555 11111111-2222-3333-4444-555555555555 Enabled False
Two subscriptions, same tenant. IsDefault True marks the current one - every command without --subscription goes there.
The most expensive CLI mistake is running a destructive command against the subscription you thought you had selected. az account show before anything that writes, or pass --subscription explicitly in scripts.
Errors look like this
ERROR: (AuthorizationFailed) The client '[email protected]' with object id
'99999999-8888-7777-6666-555555555555' does not have authorization to perform action
'Microsoft.Resources/subscriptions/resourcegroups/write' over scope '...' or the scope is
invalid. If access was recently granted, please refresh your credentials.
Code: AuthorizationFailed
Read it in order: (Code) first - that is what you search for. Then who (the client and its object id, the GUID of your identity in Entra ID), the action it tried (.../resourcegroups/write = create or change a resource group) and the scope (which box).
Exit status is 1 for a service error and 2 for a CLI argument error (missing -g, bad -o value) - scripts can tell them apart.
Defaults
az config set defaults.group=rg-oncall-lab # -g becomes optional
az config set core.output=table # default output format
az config unset defaults.group
-g (long form --resource-group) is the flag almost every az command uses to say which resource group. az config set stores a default for it in ~/.azure/config; unset removes it.
Convenient, and a trap: a default group silently applies to every command that accepts --resource-group, including az role assignment list, which then lists only that group's permissions. Use defaults interactively, never in scripts.
In this lab the tenant and its subscriptions are simulated (simulator); the commands, output shapes and error codes are the real CLI's.
What you can now do
- Read the tenant / subscription / resource group / resource hierarchy and a resource ID.
- Sign in from a headless server and check which subscription you are on before you write.
- Read an AuthorizationFailed error: who, which action, which scope.